diff --git a/.github/workflows/ci-test-integration.yml b/.github/workflows/ci-test-integration.yml index abaa2eace..9666b2ad0 100644 --- a/.github/workflows/ci-test-integration.yml +++ b/.github/workflows/ci-test-integration.yml @@ -8,33 +8,33 @@ on: push: branches: [master, dev] paths: - - '**/*.rs' - - '**/Cargo.toml' - - '**/Cargo.lock' - - '**/build.rs' - - '**/*.proto' - - 'Makefile' - - 'test-integration/**' - - 'rust-toolchain.toml' - - 'test-integration/rust-toolchain.toml' - - '.github/workflows/ci-test-integration.yml' - - '.github/actions/setup-build-env/**' - - '.github/actions/setup-solana/**' + - "**/*.rs" + - "**/Cargo.toml" + - "**/Cargo.lock" + - "**/build.rs" + - "**/*.proto" + - "Makefile" + - "test-integration/**" + - "rust-toolchain.toml" + - "test-integration/rust-toolchain.toml" + - ".github/workflows/ci-test-integration.yml" + - ".github/actions/setup-build-env/**" + - ".github/actions/setup-solana/**" pull_request: types: [opened, reopened, synchronize, ready_for_review] paths: - - '**/*.rs' - - '**/Cargo.toml' - - '**/Cargo.lock' - - '**/build.rs' - - '**/*.proto' - - 'Makefile' - - 'test-integration/**' - - 'rust-toolchain.toml' - - 'test-integration/rust-toolchain.toml' - - '.github/workflows/ci-test-integration.yml' - - '.github/actions/setup-build-env/**' - - '.github/actions/setup-solana/**' + - "**/*.rs" + - "**/Cargo.toml" + - "**/Cargo.lock" + - "**/build.rs" + - "**/*.proto" + - "Makefile" + - "test-integration/**" + - "rust-toolchain.toml" + - "test-integration/rust-toolchain.toml" + - ".github/workflows/ci-test-integration.yml" + - ".github/actions/setup-build-env/**" + - ".github/actions/setup-solana/**" jobs: # These producer jobs exist purely to produce reusable artifacts so matrix @@ -67,7 +67,7 @@ jobs: - name: Build validator binary run: | RUSTFLAGS="-C link-arg=-fuse-ld=lld" \ - cargo build --locked --bin magicblock-validator -j "$HOST_CARGO_JOBS" + cargo build --locked --bin magicblock-validator -j "$HOST_CARGO_JOBS" --features query-filtering strip target/debug/magicblock-validator || true shell: bash working-directory: magicblock-validator @@ -176,6 +176,7 @@ jobs: -p test-pubsub \ -p test-schedule-intent \ -p test-task-scheduler \ + -p test-query-filtering \ > _integration_test_bins/no-run.jsonl RUSTFLAGS="-C link-arg=-fuse-ld=lld" \ CARGO_PROFILE_DEV_DEBUG=0 \ @@ -235,6 +236,7 @@ jobs: copy_package_tests test-integration/test-pubsub pubsub copy_package_tests test-integration/test-schedule-intent schedule_intents copy_package_tests test-integration/test-task-scheduler task-scheduler + copy_package_tests test-integration/test-query-filtering query-filtering find _integration_test_bins/artifacts -mindepth 2 -type f | wc -l du -sh _integration_test_bins/artifacts/* @@ -377,6 +379,15 @@ jobs: if-no-files-found: error compression-level: 0 + - name: Upload integration test binaries - query-filtering + uses: actions/upload-artifact@v4 + with: + name: integration-test-bins-query-filtering + path: magicblock-validator/_integration_test_bins/artifact-archives/query-filtering.tar.zst + retention-days: 1 + if-no-files-found: error + compression-level: 0 + run_integration_tests: needs: [build_validator, build_sbf, build_integration_tests] runs-on: ${{ matrix.runner || 'blacksmith-8vcpu-ubuntu-2404' }} @@ -447,6 +458,8 @@ jobs: test_bins_artifact: "committor-ix" - batch_tests: "committor_intent_executor" test_bins_artifact: "committor-intent-executor" + - batch_tests: "query_filtering" + test_bins_artifact: "query-filtering" # While we iterate on this branch we want a single shard failure to # cancel the rest of the matrix so we don't burn ~50 min of runner time # on every false start. Flip back to `false` once it's green & stable. diff --git a/Cargo.lock b/Cargo.lock index 4b4e94327..66c8ab504 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -264,7 +264,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -275,7 +275,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3190,6 +3190,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "k256" version = "0.13.4" @@ -3734,12 +3749,14 @@ dependencies = [ "magicblock-core", "magicblock-ledger", "magicblock-metrics", + "magicblock-query-filtering", "magicblock-version", "parking_lot", "rand 0.9.4", "reqwest 0.12.28", "scc", "serde", + "serde_json", "solana-account 3.4.0", "solana-account-decoder", "solana-fee-structure", @@ -3760,6 +3777,7 @@ dependencies = [ "tokio", "tokio-util", "tracing", + "url", ] [[package]] @@ -3774,6 +3792,7 @@ dependencies = [ "magicblock-account-cloner", "magicblock-accounts", "magicblock-accounts-db", + "magicblock-aml", "magicblock-aperture", "magicblock-chainlink", "magicblock-committor-service", @@ -3785,6 +3804,7 @@ dependencies = [ "magicblock-metrics", "magicblock-processor", "magicblock-program", + "magicblock-query-filtering", "magicblock-replicator", "magicblock-services", "magicblock-task-scheduler", @@ -4190,6 +4210,40 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "magicblock-query-filtering" +version = "0.11.3" +dependencies = [ + "base64 0.21.7", + "chrono", + "ed25519-dalek 2.2.0", + "jsonwebtoken", + "magicblock-accounts-db", + "magicblock-aml", + "magicblock-config", + "magicblock-core", + "rand 0.9.4", + "scc", + "serde", + "serde_json", + "sha2 0.10.9", + "solana-account 3.4.0", + "solana-account-decoder-client-types", + "solana-hash 3.1.0", + "solana-keypair", + "solana-message 3.1.0", + "solana-program-pack 3.1.0", + "solana-pubkey 3.0.0", + "solana-signature 3.4.0", + "solana-signer", + "solana-transaction", + "solana-transaction-status", + "spl-token-interface", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "magicblock-replicator" version = "0.11.3" @@ -4268,7 +4322,7 @@ dependencies = [ name = "magicblock-table-mania" version = "0.11.3" dependencies = [ - "ed25519-dalek 1.0.1", + "ed25519-dalek 2.2.0", "magicblock-metrics", "magicblock-rpc-client", "rand 0.9.4", @@ -4912,6 +4966,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -6511,6 +6575,18 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint 0.4.6", + "num-traits", + "thiserror 2.0.18", + "time", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -6546,7 +6622,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -10833,7 +10909,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 306a62982..99ac4c990 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ members = [ "magicblock-ledger", "magicblock-magic-program-api", "magicblock-metrics", + "magicblock-query-filtering", "magicblock-processor", "magicblock-replicator", "magicblock-rpc-client", @@ -34,7 +35,7 @@ members = [ "tools/genx", "tools/keypair-base58", "tools/ledger-stats", - "tools/magicblock-tui-client" + "tools/magicblock-tui-client", ] # This prevents a Travis CI error when building for Windows. @@ -69,7 +70,7 @@ chrono = "0.4" clap = "4.5.40" console-subscriber = "0.5.0" derive_more = "2.0" -ed25519-dalek = "1.0.1" +ed25519-dalek = "2.2.0" enum-iterator = "1.5.0" fastwebsockets = "0.10" fd-lock = "4.0.2" @@ -88,6 +89,7 @@ hyper-util = "0.1.15" isocountry = "0.3.2" itertools = "0.14" json = { package = "sonic-rs", version = "0.5.3" } +jsonwebtoken = "9.3.1" lazy_static = "1.4.0" libc = "0.2.153" libloading = "0.8" @@ -101,9 +103,11 @@ magicblock-accounts-db = { path = "./magicblock-accounts-db" } magicblock-aml = { path = "./magicblock-aml" } magicblock-aperture = { path = "./magicblock-aperture" } magicblock-api = { path = "./magicblock-api" } -magicblock-chainlink = { path = "./magicblock-chainlink", features = ["dev-context"] } +magicblock-chainlink = { path = "./magicblock-chainlink", features = [ + "dev-context", +] } magicblock-committor-program = { path = "./magicblock-committor-program", features = [ - "no-entrypoint" + "no-entrypoint", ] } magicblock-committor-service = { path = "./magicblock-committor-service" } magicblock-config = { path = "./magicblock-config" } @@ -112,6 +116,7 @@ magicblock-delegation-program-api = { git = "https://github.com/magicblock-labs/ magicblock-ledger = { path = "./magicblock-ledger" } magicblock-magic-program-api = { path = "./magicblock-magic-program-api" } magicblock-metrics = { path = "./magicblock-metrics" } +magicblock-query-filtering = { path = "./magicblock-query-filtering" } magicblock-processor = { path = "./magicblock-processor" } magicblock-program = { path = "./programs/magicblock" } magicblock-replicator = { path = "./magicblock-replicator" } @@ -145,6 +150,7 @@ serde = "1.0.217" serde_json = "1.0" serde_with = "3.16" serial_test = "3.2" +sha2 = "0.10.8" sha3 = "0.10.8" solana-account = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "b91ab7dad6642c63b9ea177fb318840b4c64c6cb" } solana-account-decoder = { version = "3.1" } @@ -203,7 +209,7 @@ solana-sysvar = { version = "3.0" } solana-timings = { package = "solana-svm-timings", version = "3.1" } solana-transaction = { version = "3.0" } solana-transaction-context = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "b91ab7dad6642c63b9ea177fb318840b4c64c6cb", features = [ - "dev-context-only-utils" + "dev-context-only-utils", ] } solana-transaction-error = { version = "3.0" } solana-transaction-status = { version = "=3.1.12" } @@ -222,7 +228,10 @@ tonic-prost-build = "0.14" tracing = "0.1" tracing-log = { version = "0.2", features = ["log-tracer"] } tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } -twox-hash = { version = "2.1", default-features = false, features = ["alloc", "xxhash3_64"] } +twox-hash = { version = "2.1", default-features = false, features = [ + "alloc", + "xxhash3_64", +] } url = "2.5.0" # SPL Token crates used across the workspace diff --git a/config.example.toml b/config.example.toml index 01d711499..edcfe93ca 100644 --- a/config.example.toml +++ b/config.example.toml @@ -258,42 +258,6 @@ remove-confined-accounts = false # Env: MBV_CHAINLINK__RESUBSCRIPTION_DELAY # resubscription-delay = "50ms" -# ------------------------------------------------------------------------------ -# Optional: Range Risk API validation for post-delegation actions signers -# ------------------------------------------------------------------------------ -# When enabled, all signers referenced by post-delegation actions are checked -# against Range API before actions are allowed to execute. -[chainlink.risk] -# Enables/disables Range risk checks. -# Default: false -# Env: MBV_CHAINLINK__RISK__ENABLED -enabled = false - -# Range API base URL. -# Default: "https://api.range.org/v1" -# Env: MBV_CHAINLINK__RISK__BASE_URL -base-url = "https://api.range.org/v1" - -# Range API bearer token. -# Default: None -# Env: MBV_CHAINLINK__RISK__API_KEY -api-key = "your-api-key" - -# Cache TTL. -# Default: "30 days" -# Env: MBV_CHAINLINK__RISK__CACHE_TTL -cache-ttl = "30 days" - -# HTTP timeout for Range API calls. -# Default: "5s" -# Env: MBV_CHAINLINK__RISK__REQUEST_TIMEOUT -request-timeout = "5s" - -# Risk score threshold to consider an address high risk, on scale of 0-10. -# Default: 5 -# Env: MBV_CHAINLINK__RISK__RISK_SCORE_THRESHOLD -# risk-score-threshold = 5 - # ============================================================================== # On-Chain Registration (Optional) # ============================================================================== @@ -340,6 +304,62 @@ failed-task-retention = "14d" # Env: MBV_TASK_SCHEDULER__FAILED_TASK_CLEANUP_INTERVAL failed-task-cleanup-interval = "1h" +# ============================================================================== +# Query Filtering +# ============================================================================== +[query-filtering] +# Apply local permission-aware response filtering in Aperture. +# Default: false +# Env: MBV_QUERY_FILTERING__ENABLED +enabled = false +# JWT secret used by the local query filtering login route. +# Default: your_jwt_secret_key +# Env: MBV_QUERY_FILTERING__JWT_SECRET +jwt-secret = "your_jwt_secret_key" +# Token expiration time in days. +# Default: 30 +# Env: MBV_QUERY_FILTERING__TOKEN_EXPIRY_DAYS +token-expiry-days = 30 +# Challenge expiration time in seconds. +# Default: 300 +# Env: MBV_QUERY_FILTERING__CHALLENGE_TTL_SECONDS +challenge-ttl-seconds = 300 + + +# ------------------------------------------------------------------------------ +# Optional: Range Risk API validation +# ------------------------------------------------------------------------------ +[aml-risk] +# Enables/disables Range risk checks. +# Default: false +# Env: MBV_AML_RISK__ENABLED +enabled = false + +# Range API base URL. +# Default: "https://api.range.org/v1" +# Env: MBV_AML_RISK__BASE_URL +base-url = "https://api.range.org/v1" + +# Range API bearer token. +# Default: None +# Env: MBV_AML_RISK__API_KEY +# api-key = "your-api-key" + +# Cache TTL. +# Default: "30 days" +# Env: MBV_AML_RISK__CACHE_TTL +cache-ttl = "30 days" + +# HTTP timeout for Range API calls. +# Default: "5s" +# Env: MBV_AML_RISK__REQUEST_TIMEOUT +request-timeout = "5s" + +# Risk score threshold to consider an address high risk, on scale of 0-10. +# Default: 5 +# Env: MBV_AML_RISK__RISK_SCORE_THRESHOLD +# risk-score-threshold = 5 + # ============================================================================== # Pre-loaded Programs # ============================================================================== diff --git a/magicblock-aml/src/lib.rs b/magicblock-aml/src/lib.rs index 26c9a6fbd..526f6d86f 100644 --- a/magicblock-aml/src/lib.rs +++ b/magicblock-aml/src/lib.rs @@ -6,7 +6,7 @@ use std::{ }; use futures_util::future::{try_join_all, BoxFuture, FutureExt, Shared}; -use magicblock_config::config::RiskConfig; +use magicblock_config::config::AmlRiskConfig; use reqwest::Client; use rusqlite::{params, Connection}; use serde_json::Value; @@ -72,7 +72,7 @@ pub struct RiskService { impl RiskService { pub fn try_from_config( - config: &RiskConfig, + config: &AmlRiskConfig, ledger_path: &Path, ) -> RiskResult> { if !config.enabled { @@ -441,8 +441,8 @@ mod tests { } } - fn make_risk_config(base_url: String) -> RiskConfig { - RiskConfig { + fn make_risk_config(base_url: String) -> AmlRiskConfig { + AmlRiskConfig { enabled: true, base_url, api_key: Some("test-api-key".to_string()), diff --git a/magicblock-aperture/Cargo.toml b/magicblock-aperture/Cargo.toml index fe71f4359..f87fa4b24 100644 --- a/magicblock-aperture/Cargo.toml +++ b/magicblock-aperture/Cargo.toml @@ -7,6 +7,15 @@ homepage.workspace = true license.workspace = true edition.workspace = true +[features] +default = [] +# Permission-aware query filtering. When disabled, the optional +# `magicblock-query-filtering` dependency is not compiled in and the entire +# filtering/auth integration is removed from the request path. +query-filtering = ["dep:magicblock-query-filtering"] +# Full TEE integration: query filtering + TDX attestation quote. +tee = ["query-filtering", "magicblock-query-filtering/tee"] + [dependencies] # network http-body-util = { workspace = true } @@ -35,6 +44,7 @@ magicblock-config = { workspace = true } magicblock-core = { workspace = true } magicblock-ledger = { workspace = true } magicblock-metrics = { workspace = true } +magicblock-query-filtering = { workspace = true, optional = true } magicblock-version = { workspace = true } # solana @@ -59,9 +69,11 @@ base64 = { workspace = true } bincode = { workspace = true } bs58 = { workspace = true } json = { workspace = true } +serde_json = { workspace = true } serde = { workspace = true } tracing = { workspace = true } thiserror = { workspace = true } +url = { workspace = true } [dev-dependencies] rand = { workspace = true } diff --git a/magicblock-aperture/src/lib.rs b/magicblock-aperture/src/lib.rs index 7d05fccbd..a8594c070 100644 --- a/magicblock-aperture/src/lib.rs +++ b/magicblock-aperture/src/lib.rs @@ -1,8 +1,12 @@ use std::net::SocketAddr; +#[cfg(feature = "query-filtering")] +use std::sync::Arc; use error::{ApertureError, RpcError}; use magicblock_config::config::aperture::ApertureConfig; use magicblock_core::link::DispatchEndpoints; +#[cfg(feature = "query-filtering")] +use magicblock_query_filtering::QueryFilteringService; use processor::EventProcessor; use server::{http::HttpServer, websocket::WebsocketServer}; use state::SharedState; @@ -15,13 +19,22 @@ type ApertureResult = Result; pub async fn initialize_aperture( config: &ApertureConfig, + #[cfg(feature = "query-filtering")] query_filtering: Option< + Arc, + >, state: SharedState, dispatch: &DispatchEndpoints, cancel: CancellationToken, ) -> ApertureResult { - let server = - JsonRpcServer::new(config, state.clone(), dispatch, cancel.clone()) - .await?; + let server = JsonRpcServer::new( + config, + #[cfg(feature = "query-filtering")] + query_filtering, + state.clone(), + dispatch, + cancel.clone(), + ) + .await?; // Start event processors only after the server has bound its sockets so a // bind failure cannot leak background tasks during retries in tests/startup. EventProcessor::start(config, &state, dispatch, cancel)?; @@ -40,6 +53,9 @@ impl JsonRpcServer { /// Create a new instance of JSON-RPC server, hooked into validator via dispatch channels async fn new( config: &ApertureConfig, + #[cfg(feature = "query-filtering")] query_filtering: Option< + Arc, + >, state: SharedState, dispatch: &DispatchEndpoints, cancel: CancellationToken, @@ -66,7 +82,15 @@ impl JsonRpcServer { let cancel = cancel.clone(); WebsocketServer::new(ws, &state, cancel).await? }; - let http = HttpServer::new(http, state, cancel, dispatch).await?; + let http = HttpServer::new( + http, + #[cfg(feature = "query-filtering")] + query_filtering, + state, + cancel, + dispatch, + ) + .await?; Ok(Self { http, websocket, diff --git a/magicblock-aperture/src/requests/http/get_account_info.rs b/magicblock-aperture/src/requests/http/get_account_info.rs index 8edaf828d..eba56de9f 100644 --- a/magicblock-aperture/src/requests/http/get_account_info.rs +++ b/magicblock-aperture/src/requests/http/get_account_info.rs @@ -29,15 +29,35 @@ impl HttpDispatcher { debug!("Getting account info"); - // `read_account_with_ensure` guarantees the account is clone from chain if not in database. - let account = self - .read_account_with_ensure(&pubkey) + // Bundles the permission PDA into the same chainlink ensure when the + // request is authenticated so we don't pay a second round-trip just to + // run query filtering. + #[cfg_attr(not(feature = "query-filtering"), allow(unused_mut))] + let mut account = self + .read_account_with_ensure_for_user( + &pubkey, + request.authenticated_user.as_ref(), + ) .await .filter(|acc| !Self::account_should_render_as_null(acc)) // `LockedAccount` provides a race-free read of the account data before encoding. .map(|acc| { LockedAccount::new(pubkey, acc).ui_encode(encoding, slice) }); + #[cfg(feature = "query-filtering")] + if let Some(user) = &request.authenticated_user { + let permission = + magicblock_query_filtering::permission_for_account( + &*self.accountsdb, + &pubkey, + ) + .map_err(RpcError::internal)?; + account = magicblock_query_filtering::filter_account( + account, + &permission, + user, + ); + } let slot = self.blocks.block_height(); Ok(ResponsePayload::encode(&request.id, account, slot)) diff --git a/magicblock-aperture/src/requests/http/get_balance.rs b/magicblock-aperture/src/requests/http/get_balance.rs index 7d1f808be..2a8617c3b 100644 --- a/magicblock-aperture/src/requests/http/get_balance.rs +++ b/magicblock-aperture/src/requests/http/get_balance.rs @@ -13,11 +13,29 @@ impl HttpDispatcher { let pubkey_bytes = parse_params!(request.params()?, Serde32Bytes); let pubkey = some_or_err!(pubkey_bytes); - let balance = self - .read_account_with_ensure(&pubkey) + // Bundles the permission PDA into the same chainlink ensure when the + // request is authenticated. + #[cfg_attr(not(feature = "query-filtering"), allow(unused_mut))] + let mut balance = self + .read_account_with_ensure_for_user( + &pubkey, + request.authenticated_user.as_ref(), + ) .await .map(|a| a.lamports()) .unwrap_or_default(); // Default to 0 if account not found + #[cfg(feature = "query-filtering")] + if let Some(user) = &request.authenticated_user { + let permission = + magicblock_query_filtering::permission_for_account( + &*self.accountsdb, + &pubkey, + ) + .map_err(RpcError::internal)?; + if !permission.access_for(user).account { + balance = 0; + } + } let slot = self.blocks.block_height(); Ok(ResponsePayload::encode(&request.id, balance, slot)) diff --git a/magicblock-aperture/src/requests/http/get_multiple_accounts.rs b/magicblock-aperture/src/requests/http/get_multiple_accounts.rs index 03ea232d4..1d586ac03 100644 --- a/magicblock-aperture/src/requests/http/get_multiple_accounts.rs +++ b/magicblock-aperture/src/requests/http/get_multiple_accounts.rs @@ -28,9 +28,34 @@ impl HttpDispatcher { let encoding = config.encoding.unwrap_or(UiAccountEncoding::Base58); let slice = config.data_slice; + // Bundles each pubkey's permission PDA into the same chainlink ensure + // when the request is authenticated, so query-filtering doesn't pay + // an extra round-trip. + #[cfg_attr(not(feature = "query-filtering"), allow(unused_mut))] + let mut raw_accounts = self + .read_accounts_with_ensure_for_user( + &pubkeys, + request.authenticated_user.as_ref(), + ) + .await; + #[cfg(feature = "query-filtering")] + if let Some(user) = &request.authenticated_user { + let permissions = + magicblock_query_filtering::permissions_for_accounts( + &*self.accountsdb, + &pubkeys, + ) + .map_err(RpcError::internal)?; + raw_accounts = magicblock_query_filtering::filter_accounts( + raw_accounts, + &permissions, + user, + ); + } + let accounts = pubkeys .iter() - .zip(self.read_accounts_with_ensure(&pubkeys).await.into_iter()) + .zip(raw_accounts) .map(|(pubkey, acc)| { acc.filter(|account| { !Self::account_should_render_as_null(account) diff --git a/magicblock-aperture/src/requests/http/get_program_accounts.rs b/magicblock-aperture/src/requests/http/get_program_accounts.rs index be200dbae..ba09d2599 100644 --- a/magicblock-aperture/src/requests/http/get_program_accounts.rs +++ b/magicblock-aperture/src/requests/http/get_program_accounts.rs @@ -10,7 +10,7 @@ impl HttpDispatcher { /// customized with an optional configuration object to apply server-side data /// filters, specify the data encoding, request a slice of the account data, /// and control whether the result is wrapped in a context object. - pub(crate) fn get_program_accounts( + pub(crate) async fn get_program_accounts( &self, request: &mut JsonRequest, ) -> HandlerResult { @@ -29,6 +29,33 @@ impl HttpDispatcher { self.accountsdb.get_program_accounts(&program, move |a| { filters.matches(a.data()) })?; + #[cfg_attr(not(feature = "query-filtering"), allow(unused_mut))] + let mut accounts = accounts.collect::>(); + + #[cfg(feature = "query-filtering")] + if let Some(user) = &request.authenticated_user { + use magicblock_metrics::metrics::AccountFetchOrigin::*; + let account_pubkeys = accounts + .iter() + .map(|(pubkey, _)| *pubkey) + .collect::>(); + self.ensure_permission_accounts( + &account_pubkeys, + GetMultipleAccounts, + ) + .await?; + let permissions = + magicblock_query_filtering::permissions_for_accounts( + &*self.accountsdb, + &account_pubkeys, + ) + .map_err(RpcError::internal)?; + accounts = magicblock_query_filtering::filter_keyed_accounts( + accounts, + &permissions, + user, + ); + } let encoding = config .account_config @@ -38,6 +65,7 @@ impl HttpDispatcher { // Encode the filtered accounts for the RPC response. let accounts = accounts + .into_iter() .map(|(pubkey, account)| { // lock account to prevent data races with concurrently modifying // transaction executor threads (unlikely, but not impossible) diff --git a/magicblock-aperture/src/requests/http/get_signatures_for_address.rs b/magicblock-aperture/src/requests/http/get_signatures_for_address.rs index 3dedc3e0b..39ff9ca17 100644 --- a/magicblock-aperture/src/requests/http/get_signatures_for_address.rs +++ b/magicblock-aperture/src/requests/http/get_signatures_for_address.rs @@ -11,7 +11,7 @@ impl HttpDispatcher { /// Fetches a list of confirmed transaction signatures for a given address, /// sorted in reverse chronological order. The query can be paginated using /// the optional `limit`, `before`, and `until` parameters. - pub(crate) fn get_signatures_for_address( + pub(crate) async fn get_signatures_for_address( &self, request: &mut JsonRequest, ) -> HandlerResult { @@ -43,7 +43,8 @@ impl HttpDispatcher { limit, )?; - let signatures = signatures_result + #[cfg_attr(not(feature = "query-filtering"), allow(unused_mut))] + let mut signatures = signatures_result .infos .into_iter() .map(|info| { @@ -56,6 +57,23 @@ impl HttpDispatcher { rpc_status }) .collect::>(); + #[cfg(feature = "query-filtering")] + if let Some(user) = &request.authenticated_user { + use magicblock_metrics::metrics::AccountFetchOrigin::*; + self.ensure_permission_accounts(&[address], GetAccount) + .await?; + let permission = + magicblock_query_filtering::permission_for_account( + &*self.accountsdb, + &address, + ) + .map_err(RpcError::internal)?; + signatures = magicblock_query_filtering::filter_signatures( + signatures, + &permission, + user, + ); + } Ok(ResponsePayload::encode_no_context(&request.id, signatures)) } diff --git a/magicblock-aperture/src/requests/http/get_token_account_balance.rs b/magicblock-aperture/src/requests/http/get_token_account_balance.rs index bedf55e99..875880280 100644 --- a/magicblock-aperture/src/requests/http/get_token_account_balance.rs +++ b/magicblock-aperture/src/requests/http/get_token_account_balance.rs @@ -24,9 +24,42 @@ impl HttpDispatcher { let pubkey = parse_params!(request.params()?, Serde32Bytes); let pubkey: Pubkey = some_or_err!(pubkey); - // Fetch the target token account. + // Fetch the token account up-front and bundle its permission PDA into + // the same chainlink round-trip so the auth check below doesn't need + // a second network hop. + let token_account_opt = self + .read_account_with_ensure_for_user( + &pubkey, + request.authenticated_user.as_ref(), + ) + .await; + + #[cfg(feature = "query-filtering")] + if let Some(user) = &request.authenticated_user { + use solana_account_decoder::parse_token::UiTokenAmount; + let permission = + magicblock_query_filtering::permission_for_account( + &*self.accountsdb, + &pubkey, + ) + .map_err(RpcError::internal)?; + if !permission.access_for(user).account { + let slot = self.blocks.block_height(); + return Ok(ResponsePayload::encode( + &request.id, + UiTokenAmount { + ui_amount: None, + decimals: 0, + amount: "0".to_owned(), + ui_amount_string: "0.0".to_owned(), + }, + slot, + )); + } + } + let token_account: AccountSharedData = some_or_err!( - self.read_account_with_ensure(&pubkey).await, + token_account_opt, "token account not found or is not a token account" ); diff --git a/magicblock-aperture/src/requests/http/get_token_accounts_by_delegate.rs b/magicblock-aperture/src/requests/http/get_token_accounts_by_delegate.rs index e165b9dba..e551b4e03 100644 --- a/magicblock-aperture/src/requests/http/get_token_accounts_by_delegate.rs +++ b/magicblock-aperture/src/requests/http/get_token_accounts_by_delegate.rs @@ -13,7 +13,7 @@ impl HttpDispatcher { /// /// Fetches all token accounts delegated to a specific public key. The query /// must be further filtered by either a `mint` address or a `programId`. - pub(crate) fn get_token_accounts_by_delegate( + pub(crate) async fn get_token_accounts_by_delegate( &self, request: &mut JsonRequest, ) -> HandlerResult { @@ -59,6 +59,33 @@ impl HttpDispatcher { self.accountsdb.get_program_accounts(&program, move |a| { filters.matches(a.data()) })?; + #[cfg_attr(not(feature = "query-filtering"), allow(unused_mut))] + let mut accounts = accounts.collect::>(); + + #[cfg(feature = "query-filtering")] + if let Some(user) = &request.authenticated_user { + use magicblock_metrics::metrics::AccountFetchOrigin::*; + let account_pubkeys = accounts + .iter() + .map(|(pubkey, _)| *pubkey) + .collect::>(); + self.ensure_permission_accounts( + &account_pubkeys, + GetMultipleAccounts, + ) + .await?; + let permissions = + magicblock_query_filtering::permissions_for_accounts( + &*self.accountsdb, + &account_pubkeys, + ) + .map_err(RpcError::internal)?; + accounts = magicblock_query_filtering::filter_keyed_accounts( + accounts, + &permissions, + user, + ); + } let encoding = config.encoding.unwrap_or(UiAccountEncoding::Base58); let slice = config.data_slice; diff --git a/magicblock-aperture/src/requests/http/get_token_accounts_by_owner.rs b/magicblock-aperture/src/requests/http/get_token_accounts_by_owner.rs index dab9b1caf..bc9a677b4 100644 --- a/magicblock-aperture/src/requests/http/get_token_accounts_by_owner.rs +++ b/magicblock-aperture/src/requests/http/get_token_accounts_by_owner.rs @@ -13,7 +13,7 @@ impl HttpDispatcher { /// /// Fetches all token accounts owned by a specific public key. The query must /// be further filtered by either a `mint` address or a `programId`. - pub(crate) fn get_token_accounts_by_owner( + pub(crate) async fn get_token_accounts_by_owner( &self, request: &mut JsonRequest, ) -> HandlerResult { @@ -59,6 +59,33 @@ impl HttpDispatcher { self.accountsdb.get_program_accounts(&program, move |a| { filters.matches(a.data()) })?; + #[cfg_attr(not(feature = "query-filtering"), allow(unused_mut))] + let mut accounts = accounts.collect::>(); + + #[cfg(feature = "query-filtering")] + if let Some(user) = &request.authenticated_user { + use magicblock_metrics::metrics::AccountFetchOrigin::*; + let account_pubkeys = accounts + .iter() + .map(|(pubkey, _)| *pubkey) + .collect::>(); + self.ensure_permission_accounts( + &account_pubkeys, + GetMultipleAccounts, + ) + .await?; + let permissions = + magicblock_query_filtering::permissions_for_accounts( + &*self.accountsdb, + &account_pubkeys, + ) + .map_err(RpcError::internal)?; + accounts = magicblock_query_filtering::filter_keyed_accounts( + accounts, + &permissions, + user, + ); + } let encoding = config.encoding.unwrap_or(UiAccountEncoding::Base58); let slice = config.data_slice; diff --git a/magicblock-aperture/src/requests/http/get_transaction.rs b/magicblock-aperture/src/requests/http/get_transaction.rs index 41f9c5946..0e44e03f0 100644 --- a/magicblock-aperture/src/requests/http/get_transaction.rs +++ b/magicblock-aperture/src/requests/http/get_transaction.rs @@ -1,6 +1,10 @@ use json::{JsonContainerTrait, JsonValueMutTrait, JsonValueTrait}; use solana_rpc_client_api::config::RpcTransactionConfig; use solana_transaction_status::UiTransactionEncoding; +#[cfg(feature = "query-filtering")] +use solana_transaction_status::{ + ConfirmedTransactionWithStatusMeta, TransactionWithStatusMeta, +}; use super::prelude::*; @@ -9,7 +13,7 @@ impl HttpDispatcher { /// /// Fetches the details of a confirmed transaction from the ledger by its /// signature. Returns `null` if the transaction is not found. - pub(crate) fn get_transaction( + pub(crate) async fn get_transaction( &self, request: &mut JsonRequest, ) -> HandlerResult { @@ -22,9 +26,26 @@ impl HttpDispatcher { let config = config.unwrap_or_default(); // Fetch the complete transaction details from the persistent ledger. - let transaction = + #[cfg_attr(not(feature = "query-filtering"), allow(unused_mut))] + let mut transaction = self.ledger.get_complete_transaction(signature, u64::MAX)?; + #[cfg(feature = "query-filtering")] + if let (Some(user), Some(confirmed)) = + (&request.authenticated_user, transaction.as_mut()) + { + use magicblock_metrics::metrics::AccountFetchOrigin::*; + let touched = collect_static_account_keys(confirmed); + self.ensure_permission_accounts(&touched, GetMultipleAccounts) + .await?; + magicblock_query_filtering::filter_confirmed_transaction( + &*self.accountsdb, + confirmed, + user, + ) + .map_err(RpcError::internal)?; + } + let encoding = config.encoding.unwrap_or(UiTransactionEncoding::Json); // This implementation supports all transaction versions, so we pass a max version number. let max_version = Some(u8::MAX); @@ -56,6 +77,38 @@ fn value_from_serializable( json::to_value(value).ok() } +/// Collects every account key the confirmed transaction touches — static keys +/// plus any address-table lookups already resolved into the meta. Used to +/// determine which permission PDAs must be fetched before filtering. +#[cfg(feature = "query-filtering")] +fn collect_static_account_keys( + confirmed: &ConfirmedTransactionWithStatusMeta, +) -> Vec { + let mut keys = Vec::new(); + match &confirmed.tx_with_meta { + TransactionWithStatusMeta::Complete(complete) => { + keys.extend( + complete + .transaction + .message + .static_account_keys() + .iter() + .copied(), + ); + keys.extend( + complete.meta.loaded_addresses.writable.iter().copied(), + ); + keys.extend( + complete.meta.loaded_addresses.readonly.iter().copied(), + ); + } + TransactionWithStatusMeta::MissingMetadata(legacy) => { + keys.extend(legacy.message.account_keys.iter().copied()); + } + } + keys +} + fn normalize_failed_transaction_balance_arrays(value: &mut json::Value) { if value["meta"]["err"].is_null() { return; diff --git a/magicblock-aperture/src/requests/http/mod.rs b/magicblock-aperture/src/requests/http/mod.rs index 990b2a37c..43025a408 100644 --- a/magicblock-aperture/src/requests/http/mod.rs +++ b/magicblock-aperture/src/requests/http/mod.rs @@ -143,37 +143,67 @@ impl HttpDispatcher { async fn read_account_with_ensure( &self, pubkey: &Pubkey, + ) -> Option { + self.read_account_with_ensure_for_user(pubkey, None).await + } + + /// Like [`read_account_with_ensure`], but additionally ensures the + /// permission PDA when the request is authenticated — both fetches share a + /// single chainlink round-trip so the query-filtering path doesn't pay a + /// second network hop. Pass `None` to skip the permission ensure. + /// + /// If the data account is already known to be a permission account + /// itself, its meta-permission PDA is skipped — permission accounts are + /// always public and never filtered, so fetching their meta-perm would be + /// wasted bandwidth. + #[instrument(skip_all)] + async fn read_account_with_ensure_for_user( + &self, + pubkey: &Pubkey, + authenticated_user: Option<&Pubkey>, ) -> Option { if !self.needs_onchain_interactions() { return self.accountsdb.get_account(pubkey); } - let mark_empty_if_not_found = [*pubkey]; + #[cfg_attr(not(feature = "query-filtering"), allow(unused_mut))] + let mut to_ensure = vec![*pubkey]; + #[cfg(feature = "query-filtering")] + if authenticated_user.is_some() && !self.is_permission_account(pubkey) { + to_ensure.push(magicblock_query_filtering::types::Permission::pda( + pubkey, + )); + } + #[cfg(not(feature = "query-filtering"))] + let _ = authenticated_user; let _timer = ENSURE_ACCOUNTS_TIME .with_label_values(&["account"]) .start_timer(); let _ = self .chainlink .ensure_accounts( - &[*pubkey], - Some(&mark_empty_if_not_found), + &to_ensure, + Some(&to_ensure), AccountFetchOrigin::GetAccount, ) .await .inspect_err(|e| { - // There is nothing we can do if fetching the account fails - // Log the error and return whatever is in the accounts db debug!(error = ?e, "Failed to ensure account"); }); self.accountsdb.get_account(pubkey) } - /// Fetches multiple account's data from the `AccountsDb` filling them in from chain - /// as needed. + /// Fetches multiple accounts' data from the `AccountsDb`, filling them in + /// from chain as needed, and bundles each pubkey's permission PDA into the + /// same chainlink ensure when the request is authenticated. Single network + /// round-trip for both data and permission fetches. Permission accounts + /// known locally are skipped — they are public and never filtered. Pass + /// `None` for `authenticated_user` to skip the permission ensure. #[instrument(skip(self, pubkeys), fields(pubkey_count = pubkeys.len()))] - async fn read_accounts_with_ensure( + async fn read_accounts_with_ensure_for_user( &self, pubkeys: &[Pubkey], + authenticated_user: Option<&Pubkey>, ) -> Vec> { if !self.needs_onchain_interactions() { return pubkeys @@ -182,6 +212,19 @@ impl HttpDispatcher { .collect(); } + #[cfg_attr(not(feature = "query-filtering"), allow(unused_mut))] + let mut to_ensure: Vec = pubkeys.to_vec(); + #[cfg(feature = "query-filtering")] + if authenticated_user.is_some() { + to_ensure.extend( + pubkeys + .iter() + .filter(|pubkey| !self.is_permission_account(pubkey)) + .map(magicblock_query_filtering::types::Permission::pda), + ); + } + #[cfg(not(feature = "query-filtering"))] + let _ = authenticated_user; trace!("Ensuring accounts"); let _timer = ENSURE_ACCOUNTS_TIME .with_label_values(&["multi-account"]) @@ -189,14 +232,12 @@ impl HttpDispatcher { let _ = self .chainlink .ensure_accounts( - pubkeys, - Some(pubkeys), + &to_ensure, + Some(&to_ensure), AccountFetchOrigin::GetMultipleAccounts, ) .await .inspect_err(|e| { - // There is nothing we can do if fetching the accounts fails - // Log the error and return whatever is in the accounts db warn!(error = ?e, "Failed to ensure accounts"); }); pubkeys @@ -205,6 +246,17 @@ impl HttpDispatcher { .collect() } + /// Cheap local check: does the address already resolve to an account + /// owned by the permission program? Used to short-circuit meta-permission + /// ensures for accounts we know to be permission accounts themselves. + #[cfg(feature = "query-filtering")] + fn is_permission_account(&self, pubkey: &Pubkey) -> bool { + self.accountsdb.get_account(pubkey).is_some_and(|account| { + account.owner() + == &magicblock_query_filtering::PERMISSION_PROGRAM_ID + }) + } + /// Decodes, validates, and sanitizes a transaction from its string representation. /// /// This is a crucial pre-processing step for both `sendTransaction` and @@ -263,6 +315,82 @@ impl HttpDispatcher { }) } + /// Enforces send/simulate transaction admission against query-filtering + /// permissions when the caller is authenticated. No-op when query + /// filtering is disabled or the request has no authenticated user. + #[cfg(feature = "query-filtering")] + pub(crate) async fn enforce_transaction_admission( + &self, + transaction: &SanitizedTransaction, + request: &super::JsonHttpRequest, + ) -> RpcResult<()> { + if request.authenticated_user.is_none() { + return Ok(()); + } + let message = transaction.message(); + let account_keys: Vec = + message.account_keys().iter().copied().collect(); + self.ensure_permission_accounts( + &account_keys, + AccountFetchOrigin::SendTransaction(*transaction.signature()), + ) + .await?; + magicblock_query_filtering::check_transaction_admission( + &*self.accountsdb, + &account_keys, + message.instructions(), + ) + .map_err(|err| match err { + magicblock_query_filtering::service::QueryFilteringError::AccessDenied => { + RpcError::invalid_request(err) + } + other => RpcError::internal(other), + }) + } + + /// Ensures the permission PDAs for the given data accounts are present in + /// the local `AccountsDb`. Without this step the query-filtering layer + /// would treat a not-yet-cloned permission account as missing, silently + /// rendering a restricted account as unrestricted on first access. + /// + /// Accounts already known locally to be permission accounts themselves + /// are skipped — permission accounts are public and never filtered, so + /// fetching their meta-permission would be wasted bandwidth. + /// + /// `mark_empty_if_not_found` is set so PDAs that don't exist on chain are + /// cached as empty locally — subsequent reads short-circuit through the + /// `permission_for_account` owner gate without re-fetching. + #[cfg(feature = "query-filtering")] + pub(crate) async fn ensure_permission_accounts( + &self, + accounts: &[Pubkey], + fetch_origin: AccountFetchOrigin, + ) -> RpcResult<()> { + if !self.needs_onchain_interactions() || accounts.is_empty() { + return Ok(()); + } + let permission_pdas: Vec = accounts + .iter() + .filter(|pubkey| !self.is_permission_account(pubkey)) + .map(magicblock_query_filtering::types::Permission::pda) + .collect(); + if permission_pdas.is_empty() { + return Ok(()); + } + let _timer = ENSURE_ACCOUNTS_TIME + .with_label_values(&["permission"]) + .start_timer(); + let _ = self + .chainlink + .ensure_accounts( + &permission_pdas, + Some(&permission_pdas), + fetch_origin, + ) + .await; + Ok(()) + } + /// Ensures all accounts required for a transaction are present in the `AccountsDb`. #[instrument(skip_all)] async fn ensure_transaction_accounts( diff --git a/magicblock-aperture/src/requests/http/send_transaction.rs b/magicblock-aperture/src/requests/http/send_transaction.rs index 2a002533d..60462370a 100644 --- a/magicblock-aperture/src/requests/http/send_transaction.rs +++ b/magicblock-aperture/src/requests/http/send_transaction.rs @@ -42,7 +42,17 @@ impl HttpDispatcher { return Err(TransactionError::AlreadyProcessed.into()); } - self.ensure_transaction_accounts(&transaction.txn).await?; + #[cfg(not(feature = "query-filtering"))] + { + self.ensure_transaction_accounts(&transaction.txn).await?; + } + #[cfg(feature = "query-filtering")] + { + tokio::try_join!( + self.ensure_transaction_accounts(&transaction.txn), + self.enforce_transaction_admission(&transaction.txn, request), + )?; + } // Based on the preflight flag, either execute and await the result, // or schedule (fire-and-forget) for background processing. diff --git a/magicblock-aperture/src/requests/http/simulate_transaction.rs b/magicblock-aperture/src/requests/http/simulate_transaction.rs index 15e70377b..ba8032872 100644 --- a/magicblock-aperture/src/requests/http/simulate_transaction.rs +++ b/magicblock-aperture/src/requests/http/simulate_transaction.rs @@ -48,8 +48,29 @@ impl HttpDispatcher { .inspect_err(|err| { debug!(error = ?err, "Failed to prepare transaction to simulate") })?; - self.ensure_transaction_accounts(&transaction.txn).await?; - let number_of_accounts = transaction.txn.message().account_keys().len(); + + #[cfg(not(feature = "query-filtering"))] + { + self.ensure_transaction_accounts(&transaction.txn).await?; + } + #[cfg(feature = "query-filtering")] + { + tokio::try_join!( + self.ensure_transaction_accounts(&transaction.txn), + self.enforce_transaction_admission(&transaction.txn, request), + )?; + } + // Captured before the transaction is moved into the scheduler so the + // response redaction below can look up each touched account's + // permission. + let account_keys: Vec = transaction + .txn + .message() + .account_keys() + .iter() + .copied() + .collect(); + let number_of_accounts = account_keys.len(); let replacement_blockhash = config .replace_recent_blockhash @@ -102,28 +123,53 @@ impl HttpDispatcher { .map_err(RpcError::invalid_params) }) .collect::, _>>()?; - let current_accounts = - self.read_accounts_with_ensure(&pubkeys).await; + let current_accounts = self + .read_accounts_with_ensure_for_user( + &pubkeys, + request.authenticated_user.as_ref(), + ) + .await; let post_simulation_accounts = post_simulation_accounts .into_iter() .collect::>(); - Some( - pubkeys - .into_iter() - .zip(current_accounts) - .map(|(pubkey, account)| { - post_simulation_accounts - .get(&pubkey) - .cloned() - .or(account) - .map(|account| { - LockedAccount::new(pubkey, account) - .ui_encode(accounts_encoding, None) - }) - }) - .collect(), - ) + let encoded = pubkeys + .iter() + .zip(current_accounts) + .map(|(pubkey, account)| { + post_simulation_accounts + .get(pubkey) + .cloned() + .or(account) + .map(|account| { + LockedAccount::new(*pubkey, account) + .ui_encode(accounts_encoding, None) + }) + }) + .collect::>(); + + // Filter post-simulation account data the authenticated user + // isn't permitted to read. Without this, `simulateTransaction` + // would expose restricted account state that `getAccountInfo` + // and `getMultipleAccounts` redact. + #[cfg(feature = "query-filtering")] + let encoded = if let Some(user) = &request.authenticated_user { + let permissions = + magicblock_query_filtering::permissions_for_accounts( + &*self.accountsdb, + &pubkeys, + ) + .map_err(RpcError::internal)?; + magicblock_query_filtering::filter_accounts( + encoded, + &permissions, + user, + ) + } else { + encoded + }; + + Some(encoded) } } else { None @@ -153,7 +199,8 @@ impl HttpDispatcher { .collect::>() }); - let result = RpcSimulateTransactionResult { + #[cfg_attr(not(feature = "query-filtering"), allow(unused_mut))] + let mut result = RpcSimulateTransactionResult { logs, accounts, units_consumed: Some(units_consumed), @@ -170,6 +217,37 @@ impl HttpDispatcher { loaded_addresses: None, }; + // Redact execution metadata the authenticated user lacks the flags to + // see, mirroring the per-flag redaction `getTransaction` applies to + // confirmed transactions. Logs and return data are gated on the `logs` + // flag (execution observability); inner instructions on the `message` + // flag. Permission PDAs for these accounts were already ensured by + // `enforce_transaction_admission` above. + #[cfg(feature = "query-filtering")] + if let Some(user) = &request.authenticated_user { + let permissions = + magicblock_query_filtering::permissions_for_accounts( + &*self.accountsdb, + &account_keys, + ) + .map_err(RpcError::internal)?; + let (logs_visible, message_visible) = permissions + .iter() + .map(|permission| permission.access_for(user)) + .fold((true, true), |(logs, message), access| { + (logs && access.logs, message && access.message) + }); + if !logs_visible { + if result.logs.is_some() { + result.logs = Some(Vec::new()); + } + result.return_data = None; + } + if !message_visible { + result.inner_instructions = None; + } + } + let slot = self.blocks.block_height(); Ok(ResponsePayload::encode(&request.id, result, slot)) } diff --git a/magicblock-aperture/src/requests/mod.rs b/magicblock-aperture/src/requests/mod.rs index 3f9b223a9..b1aeff014 100644 --- a/magicblock-aperture/src/requests/mod.rs +++ b/magicblock-aperture/src/requests/mod.rs @@ -1,4 +1,5 @@ use json::{Array, Deserialize, Value}; +use solana_pubkey::Pubkey; use crate::{error::RpcError, RpcResult}; @@ -14,6 +15,9 @@ pub(crate) struct JsonRequest { pub(crate) method: M, /// An optional array of positional parameter values for the method. pub(crate) params: Option, + /// Authenticated query-filtering user, populated by the HTTP dispatcher. + #[serde(skip)] + pub(crate) authenticated_user: Option, } /// Represents either a single JSON-RPC request or a batch of multiple requests. pub enum RpcRequest { diff --git a/magicblock-aperture/src/server/http/dispatch.rs b/magicblock-aperture/src/server/http/dispatch.rs index 3df77678d..ac2174230 100644 --- a/magicblock-aperture/src/server/http/dispatch.rs +++ b/magicblock-aperture/src/server/http/dispatch.rs @@ -1,4 +1,6 @@ use core::str; +#[cfg(feature = "query-filtering")] +use std::{collections::HashMap, str::FromStr}; use std::{convert::Infallible, sync::Arc}; use futures::{stream::FuturesOrdered, StreamExt}; @@ -20,7 +22,15 @@ use magicblock_ledger::Ledger; use magicblock_metrics::metrics::{ RPC_REQUESTS_COUNT, RPC_REQUEST_HANDLING_TIME, }; +#[cfg(feature = "query-filtering")] +use magicblock_query_filtering::{ + auth::AuthError, quote, service::QueryFilteringError, types::LoginRequest, + QueryFilteringService, +}; +use solana_pubkey::Pubkey; +#[cfg(feature = "query-filtering")] +use crate::requests::http::Data; use crate::{ requests::{ http::{extract_bytes, parse_body, HandlerResult}, @@ -58,6 +68,9 @@ pub(crate) struct HttpDispatcher { /// A handle to the transaction scheduler for processing /// `sendTransaction` and `simulateTransaction`. pub(crate) transactions_scheduler: TransactionSchedulerHandle, + /// Query filtering service and local authentication state. + #[cfg(feature = "query-filtering")] + pub(crate) query_filtering: Option>, } impl HttpDispatcher { @@ -66,6 +79,9 @@ impl HttpDispatcher { /// This constructor clones the necessary handles from the global `SharedState` and /// `DispatchEndpoints`, making it cheap to create multiple `Arc` pointers. pub(super) fn new( + #[cfg(feature = "query-filtering")] query_filtering: Option< + Arc, + >, state: SharedState, channels: &DispatchEndpoints, ) -> Arc { @@ -77,6 +93,8 @@ impl HttpDispatcher { transactions: state.transactions.clone(), blocks: state.blocks.clone(), transactions_scheduler: channels.transaction_scheduler.clone(), + #[cfg(feature = "query-filtering")] + query_filtering, }) } @@ -96,9 +114,49 @@ impl HttpDispatcher { self: Arc, request: Request, ) -> Result, Infallible> { - if let Some(response) = Self::handle_special_request(&request) { - return Ok(response); - } + let request = match self.handle_special_request(request).await { + Ok(response) => return Ok(response), + Err(request) => request, + }; + + // Extract the authenticated user from the request. With query filtering + // compiled out there is no authentication and every request is anonymous. + #[cfg(not(feature = "query-filtering"))] + let authenticated_user: Option = None; + #[cfg(feature = "query-filtering")] + let authenticated_user = + if let Some(query_filtering) = &self.query_filtering { + let Some(token) = extract_token(&request) else { + let response = cors_json_response( + StatusCode::UNAUTHORIZED, + "missing auth token", + ); + return Ok(response); + }; + let pubkey = match query_filtering.verify_token(&token) { + Ok(pubkey) => pubkey, + Err(err) => { + let response = cors_json_response( + StatusCode::UNAUTHORIZED, + &err.to_string(), + ); + return Ok(response); + } + }; + match pubkey.parse::() { + Ok(pubkey) => Some(pubkey), + Err(err) => { + let response = cors_json_response( + StatusCode::UNAUTHORIZED, + &err.to_string(), + ); + return Ok(response); + } + } + } else { + None + }; + // A local macro to simplify error handling. If a Result is an Err, // it immediately formats it into a JSON-RPC error response and returns. macro_rules! unwrap { @@ -131,6 +189,7 @@ impl HttpDispatcher { // Resolve the handler for request and process it let (response, id) = match request { RpcRequest::Single(mut r) => { + r.authenticated_user = authenticated_user; let response = self.process(&mut r).await; (response, Some(r.id)) } @@ -140,6 +199,7 @@ impl HttpDispatcher { const CLOSE_BR: u8 = b']'; let mut jobs = FuturesOrdered::new(); for mut r in requests { + r.authenticated_user = authenticated_user; let j = async { let response = self.process(&mut r).await; (response, r) @@ -195,12 +255,14 @@ impl HttpDispatcher { GetLargestAccounts => self.get_largest_accounts(request), GetLatestBlockhash => self.get_latest_blockhash(request), GetMultipleAccounts => self.get_multiple_accounts(request).await, - GetProgramAccounts => self.get_program_accounts(request), + GetProgramAccounts => self.get_program_accounts(request).await, GetRecentPerformanceSamples => { self.get_recent_performance_samples(request) } GetSignatureStatuses => self.get_signature_statuses(request), - GetSignaturesForAddress => self.get_signatures_for_address(request), + GetSignaturesForAddress => { + self.get_signatures_for_address(request).await + } GetSlot => self.get_slot(request), GetSlotLeader => self.get_slot_leader(request), GetSlotLeaders => self.get_slot_leaders(request), @@ -209,14 +271,14 @@ impl HttpDispatcher { self.get_token_account_balance(request).await } GetTokenAccountsByDelegate => { - self.get_token_accounts_by_delegate(request) + self.get_token_accounts_by_delegate(request).await } GetTokenAccountsByOwner => { - self.get_token_accounts_by_owner(request) + self.get_token_accounts_by_owner(request).await } GetTokenLargestAccounts => self.get_token_largest_accounts(request), GetTokenSupply => self.get_token_supply(request), - GetTransaction => self.get_transaction(request), + GetTransaction => self.get_transaction(request).await, GetTransactionCount => self.get_transaction_count(request), GetVersion => self.get_version(request), GetVoteAccounts => self.get_vote_accounts(request), @@ -232,22 +294,144 @@ impl HttpDispatcher { } } - fn handle_special_request( - request: &Request, - ) -> Option> { + async fn handle_special_request( + &self, + request: Request, + ) -> Result, Request> { if request.method() == Method::OPTIONS { let mut response = Response::new(JsonBody::from("")); Self::set_access_control_headers(&mut response); - return Some(response); + return Ok(response); } else if request.uri() == "/health/primary" { let mut response = Response::new(JsonBody::from("")); Self::set_access_control_headers(&mut response); if CoordinationMode::current() != CoordinationMode::Primary { *response.status_mut() = StatusCode::SERVICE_UNAVAILABLE } - return Some(response); + return Ok(response); + } + + // The quote/auth endpoints below are only compiled in (and only + // available) when query filtering is enabled. + #[cfg(feature = "query-filtering")] + { + let Some(query_filtering) = &self.query_filtering else { + return Err(request); + }; + + if request.method() == Method::GET + && request.uri().path() == "/quote" + { + return Ok(Self::quote_response(&request, false).await); + } + if request.method() == Method::GET + && request.uri().path() == "/fast-quote" + { + return Ok(Self::quote_response(&request, true).await); + } + if request.method() == Method::GET + && request.uri().path() == "/auth/challenge" + { + return Ok(self.challenge_response(query_filtering, &request)); + } + if request.method() == Method::POST + && request.uri().path() == "/auth/login" + { + return Ok(self.login_response(query_filtering, request).await); + } + } + + Err(request) + } + + #[cfg(feature = "query-filtering")] + async fn quote_response( + request: &Request, + fast: bool, + ) -> Response { + let Some(challenge) = query_params(request).remove("challenge") else { + let mut response = + json_response(StatusCode::BAD_REQUEST, "missing challenge"); + Self::set_access_control_headers(&mut response); + return response; + }; + + let mut response = if fast { + match quote::fast_quote(&challenge).await { + Ok(body) => quote_json_response(&body), + Err(err) => { + json_response(quote_error_status(&err), &err.to_string()) + } + } + } else { + match quote::quote(&challenge).await { + Ok(body) => quote_json_response(&body), + Err(err) => { + json_response(quote_error_status(&err), &err.to_string()) + } + } + }; + Self::set_access_control_headers(&mut response); + response + } + + #[cfg(feature = "query-filtering")] + fn challenge_response( + &self, + query_filtering: &QueryFilteringService, + request: &Request, + ) -> Response { + let Some(pubkey) = query_params(request).remove("pubkey") else { + return cors_json_response( + StatusCode::BAD_REQUEST, + "missing pubkey", + ); + }; + + // Check if the pubkey is valid + if let Err(err) = Pubkey::from_str(&pubkey) { + return cors_json_response( + auth_error_status(&err.clone().into()), + &err.to_string(), + ); + }; + + cors_quote_json_response(&query_filtering.get_challenge(&pubkey)) + } + + #[cfg(feature = "query-filtering")] + async fn login_response( + &self, + query_filtering: &QueryFilteringService, + request: Request, + ) -> Response { + let body = match extract_bytes(request).await { + Ok(body) => body, + Err(err) => { + return cors_json_response( + StatusCode::BAD_REQUEST, + &err.to_string(), + ) + } + }; + let body = body_data_to_vec(body); + let request = match serde_json::from_slice::(&body) { + Ok(request) => request, + Err(err) => { + return cors_json_response( + StatusCode::BAD_REQUEST, + &err.to_string(), + ) + } + }; + + match query_filtering.login(request).await { + Ok(body) => cors_quote_json_response(&body), + Err(err) => cors_json_response( + query_filtering_auth_error_status(&err), + &err.to_string(), + ), } - None } /// Set CORS/Access control related headers (required by explorers/web apps) @@ -264,3 +448,160 @@ impl HttpDispatcher { headers.insert(ACCESS_CONTROL_MAX_AGE, hv("86400")); } } + +#[cfg(feature = "query-filtering")] +fn query_params(req: &Request) -> HashMap { + req.uri() + .query() + .map(|q| { + url::form_urlencoded::parse(q.as_bytes()) + .into_owned() + .collect() + }) + .unwrap_or_default() +} + +#[cfg(feature = "query-filtering")] +fn extract_token(req: &Request) -> Option { + query_params(req).get("token").cloned().or_else(|| { + req.headers() + .get("Authorization") + .and_then(|value| value.to_str().ok()) + .and_then(|value| { + let mut parts = value.split_whitespace(); + match (parts.next(), parts.next(), parts.next()) { + (Some(scheme), Some(token), None) + if scheme.eq_ignore_ascii_case("Bearer") => + { + Some(token.to_owned()) + } + _ => None, + } + }) + }) +} + +#[cfg(feature = "query-filtering")] +fn json_response(status: StatusCode, error: &str) -> Response { + let body = serde_json::json!({ "error": error }); + Response::builder() + .status(status) + .header("content-type", "application/json") + .body(JsonBody(serde_json::to_vec(&body).unwrap_or_default())) + .unwrap_or_else(|_| Response::new(JsonBody(Vec::new()))) +} + +#[cfg(feature = "query-filtering")] +fn quote_json_response(body: &T) -> Response { + match serde_json::to_vec(body) { + Ok(body) => Response::builder() + .status(StatusCode::OK) + .header("content-type", "application/json") + .body(JsonBody(body)) + .unwrap_or_else(|_| Response::new(JsonBody(Vec::new()))), + Err(err) => json_response( + StatusCode::INTERNAL_SERVER_ERROR, + &format!("failed to serialize quote response: {err}"), + ), + } +} + +#[cfg(feature = "query-filtering")] +fn cors_json_response(status: StatusCode, error: &str) -> Response { + let mut response = json_response(status, error); + HttpDispatcher::set_access_control_headers(&mut response); + response +} + +#[cfg(feature = "query-filtering")] +fn cors_quote_json_response( + body: &T, +) -> Response { + let mut response = quote_json_response(body); + HttpDispatcher::set_access_control_headers(&mut response); + response +} + +#[cfg(feature = "query-filtering")] +fn body_data_to_vec(body: Data) -> Vec { + match body { + Data::Empty => Vec::new(), + Data::SingleChunk(bytes) => bytes.to_vec(), + Data::MultiChunk(bytes) => bytes, + } +} + +#[cfg(feature = "query-filtering")] +fn quote_error_status(error: "e::QuoteError) -> StatusCode { + match error { + quote::QuoteError::InvalidChallenge | quote::QuoteError::Base64(_) => { + StatusCode::BAD_REQUEST + } + quote::QuoteError::Disabled => StatusCode::NOT_IMPLEMENTED, + #[cfg(feature = "tee")] + quote::QuoteError::TdxGuest(_) | quote::QuoteError::Join(_) => { + StatusCode::INTERNAL_SERVER_ERROR + } + } +} + +#[cfg(feature = "query-filtering")] +fn auth_error_status(error: &AuthError) -> StatusCode { + match error { + AuthError::Json(_) + | AuthError::ParsePubkey(_) + | AuthError::ParseSignature(_) + | AuthError::InvalidChallengeDate + | AuthError::InvalidChallengeFormat + | AuthError::InvalidChallengeUserPubkey + | AuthError::InvalidChallengeTtlSeconds => StatusCode::BAD_REQUEST, + AuthError::ChallengeExpired + | AuthError::SignatureVerification + | AuthError::TokenExpired => StatusCode::UNAUTHORIZED, + AuthError::Jwt(_) | AuthError::Risk(_) => { + StatusCode::INTERNAL_SERVER_ERROR + } + } +} + +#[cfg(feature = "query-filtering")] +fn query_filtering_auth_error_status( + error: &QueryFilteringError, +) -> StatusCode { + match error { + QueryFilteringError::Auth(error) => auth_error_status(error), + QueryFilteringError::Permission { .. } => { + StatusCode::INTERNAL_SERVER_ERROR + } + QueryFilteringError::AccessDenied => StatusCode::UNAUTHORIZED, + QueryFilteringError::Disabled => StatusCode::NOT_IMPLEMENTED, + } +} + +#[cfg(all(test, feature = "query-filtering"))] +mod tests { + use hyper::header::AUTHORIZATION; + + use super::*; + + #[test] + fn extract_token_uses_query_param() { + let request = Request::builder() + .uri("/?token=query-token") + .body(()) + .unwrap(); + + assert_eq!(extract_token(&request).as_deref(), Some("query-token")); + } + + #[test] + fn extract_token_uses_bearer_header() { + let request = Request::builder() + .uri("/") + .header(AUTHORIZATION, "Bearer header-token") + .body(()) + .unwrap(); + + assert_eq!(extract_token(&request).as_deref(), Some("header-token")); + } +} diff --git a/magicblock-aperture/src/server/http/mod.rs b/magicblock-aperture/src/server/http/mod.rs index 2c3d4591c..c6f02a991 100644 --- a/magicblock-aperture/src/server/http/mod.rs +++ b/magicblock-aperture/src/server/http/mod.rs @@ -7,6 +7,8 @@ use hyper_util::{ server::conn, }; use magicblock_core::link::DispatchEndpoints; +#[cfg(feature = "query-filtering")] +use magicblock_query_filtering::QueryFilteringService; use tokio::net::{TcpListener, TcpStream}; use tokio_util::sync::CancellationToken; use tracing::{info, instrument}; @@ -30,13 +32,21 @@ impl HttpServer { /// Initializes the HTTP server by binding to an address and setting up shutdown signaling. pub(crate) async fn new( socket: TcpListener, + #[cfg(feature = "query-filtering")] query_filtering: Option< + Arc, + >, state: SharedState, cancel: CancellationToken, dispatch: &DispatchEndpoints, ) -> RpcResult { Ok(Self { socket, - dispatcher: HttpDispatcher::new(state, dispatch), + dispatcher: HttpDispatcher::new( + #[cfg(feature = "query-filtering")] + query_filtering, + state, + dispatch, + ), cancel, }) } diff --git a/magicblock-aperture/tests/setup.rs b/magicblock-aperture/tests/setup.rs index 7da1718e5..ff8027e93 100644 --- a/magicblock-aperture/tests/setup.rs +++ b/magicblock-aperture/tests/setup.rs @@ -127,10 +127,16 @@ impl RpcTestEnv { listen: "127.0.0.1:0".parse().unwrap(), ..Default::default() }; - let server = - initialize_aperture(&config, state, &execution.dispatch, cancel) - .await - .expect("failed to initialize aperture test server"); + let server = initialize_aperture( + &config, + #[cfg(feature = "query-filtering")] + None, + state, + &execution.dispatch, + cancel, + ) + .await + .expect("failed to initialize aperture test server"); let rpc_url = format!("http://{}", server.http_addr()); let pubsub_url = format!("ws://{}", server.ws_addr()); diff --git a/magicblock-api/Cargo.toml b/magicblock-api/Cargo.toml index cdef84a27..a747ac708 100644 --- a/magicblock-api/Cargo.toml +++ b/magicblock-api/Cargo.toml @@ -7,6 +7,17 @@ homepage.workspace = true license.workspace = true edition.workspace = true +[features] +default = [] +# Permission-aware query filtering. When disabled, the optional +# `magicblock-query-filtering` dependency is not compiled in. +query-filtering = [ + "dep:magicblock-query-filtering", + "magicblock-aperture/query-filtering", +] +# Full TEE integration: query filtering + TDX attestation quote. +tee = ["query-filtering", "magicblock-query-filtering/tee"] + [dependencies] anyhow = { workspace = true } borsh = "1.5.3" @@ -18,6 +29,7 @@ magic-domain-program = { workspace = true } magicblock-account-cloner = { workspace = true } magicblock-accounts = { workspace = true } magicblock-accounts-db = { workspace = true } +magicblock-aml = { workspace = true } magicblock-aperture = { workspace = true } magicblock-chainlink = { workspace = true } magicblock-committor-service = { workspace = true } @@ -28,6 +40,7 @@ magicblock-magic-program-api = { workspace = true } magicblock-metrics = { workspace = true } magicblock-processor = { workspace = true } magicblock-program = { workspace = true } +magicblock-query-filtering = { workspace = true, optional = true } magicblock-services = { workspace = true } magicblock-replicator = { workspace = true } magicblock-task-scheduler = { workspace = true } diff --git a/magicblock-api/src/errors.rs b/magicblock-api/src/errors.rs index 86c2c8834..042b4b759 100644 --- a/magicblock-api/src/errors.rs +++ b/magicblock-api/src/errors.rs @@ -110,6 +110,13 @@ pub enum ApiError { #[error("Replication service failed: {0}")] Replication(#[from] magicblock_replicator::Error), + + #[error("Risk service failed: {0}")] + Risk(#[from] magicblock_aml::RiskError), + + #[cfg(feature = "query-filtering")] + #[error("QueryFiltering service failed: {0}")] + QueryFiltering(#[from] magicblock_query_filtering::QueryFilteringError), } impl From for ApiError { diff --git a/magicblock-api/src/magic_validator.rs b/magicblock-api/src/magic_validator.rs index 48a3f159f..8b7ace1db 100644 --- a/magicblock-api/src/magic_validator.rs +++ b/magicblock-api/src/magic_validator.rs @@ -14,6 +14,7 @@ use magicblock_accounts::{ ScheduledCommitsProcessor, }; use magicblock_accounts_db::{traits::AccountsBank, AccountsDb}; +use magicblock_aml::RiskService; use magicblock_aperture::{ initialize_aperture, state::{NodeContext, SharedState}, @@ -57,6 +58,8 @@ use magicblock_program::{ validator::{self, validator_authority}, TransactionScheduler as ActionTransactionScheduler, }; +#[cfg(feature = "query-filtering")] +use magicblock_query_filtering::auth::QueryFilteringService; use magicblock_replicator::{nats::Broker, ReplicationService}; use magicblock_services::actions_callback_service::ActionsCallbackService; use magicblock_task_scheduler::{SchedulerDatabase, TaskSchedulerService}; @@ -120,6 +123,8 @@ pub struct MagicValidator { _metrics: (MetricsService, tokio::task::JoinHandle<()>), claim_fees_task: ClaimFeesTask, task_scheduler: Option, + #[cfg(feature = "query-filtering")] + query_filtering: Option>, transaction_execution: thread::JoinHandle<()>, replication_handle: Option>>, @@ -156,6 +161,27 @@ impl MagicValidator { info!("Ledger initialized"); let ledger_path = ledger.ledger_path(); + let step_start = Instant::now(); + let risk_service = + RiskService::try_from_config(&config.aml_risk, ledger_path)? + .map(Arc::new); + log_timing("startup", "risk_service_init", step_start); + + #[cfg(feature = "query-filtering")] + let query_filtering = { + let step_start = Instant::now(); + let query_filtering = if config.query_filtering.enabled { + Some(Arc::new(QueryFilteringService::try_new( + &config.query_filtering, + risk_service.clone(), + )?)) + } else { + None + }; + log_timing("startup", "query_filtering_init", step_start); + query_filtering + }; + let step_start = Instant::now(); Self::sync_validator_keypair_with_ledger( ledger_path, @@ -237,6 +263,7 @@ impl MagicValidator { &ledger.latest_block().clone(), &accountsdb, shared_chain_slot.clone(), + risk_service.clone(), ) .await?, ); @@ -381,6 +408,8 @@ impl MagicValidator { let step_start = Instant::now(); let rpc = initialize_aperture( &config.aperture, + #[cfg(feature = "query-filtering")] + query_filtering.clone(), shared_state, &dispatch, token.clone(), @@ -442,6 +471,8 @@ impl MagicValidator { identity: validator_pubkey, transaction_scheduler: dispatch.transaction_scheduler, task_scheduler: Some(task_scheduler), + #[cfg(feature = "query-filtering")] + query_filtering, transaction_execution, replication_handle: None, mode_tx, @@ -494,6 +525,7 @@ impl MagicValidator { latest_block: &LatestBlock, accountsdb: &Arc, chain_slot: Option>, + risk_service: Option>, ) -> ApiResult { if Self::replication_mode_uses_disabled_chainlink( &config.validator.replication_mode, @@ -554,6 +586,7 @@ impl MagicValidator { &config.chainlink, config.storage.as_path(), chain_slot.unwrap_or_default(), + risk_service, ) .await?; @@ -1051,6 +1084,10 @@ impl MagicValidator { let step_start = Instant::now(); let _ = self.rpc_handle.join(); log_timing("shutdown", "rpc_thread_join", step_start); + #[cfg(feature = "query-filtering")] + if let Some(service) = self.query_filtering.take() { + service.stop() + } if let Some(handle) = self.slot_ticker { let step_start = Instant::now(); let _ = handle.await; diff --git a/magicblock-chainlink/src/chainlink/mod.rs b/magicblock-chainlink/src/chainlink/mod.rs index db3da1262..7d453f4c6 100644 --- a/magicblock-chainlink/src/chainlink/mod.rs +++ b/magicblock-chainlink/src/chainlink/mod.rs @@ -322,7 +322,8 @@ impl accounts_bank, cloner, config, - chainlink_config + chainlink_config, + risk_service ))] pub async fn try_new_from_endpoints( endpoints: &Endpoints, @@ -334,6 +335,7 @@ impl chainlink_config: &ChainLinkConfig, ledger_path: &Path, chain_slot: Arc, + risk_service: Option>, ) -> ChainlinkResult< InnerChainlink< ChainRpcClientImpl, @@ -356,11 +358,6 @@ impl .await?; let fetch_cloner = if let Some(provider) = account_provider { let provider = Arc::new(provider); - let risk_service = RiskService::try_from_config( - &chainlink_config.risk, - ledger_path, - )? - .map(Arc::new); let fetch_cloner = FetchCloner::new( &provider, accounts_bank, diff --git a/magicblock-config/src/config/aml_risk.rs b/magicblock-config/src/config/aml_risk.rs new file mode 100644 index 000000000..0d61dea79 --- /dev/null +++ b/magicblock-config/src/config/aml_risk.rs @@ -0,0 +1,40 @@ +use std::time::Duration; + +use serde::{Deserialize, Serialize}; + +use crate::consts; + +/// Configuration for checking account risk with the Range API and a local sqlite cache. +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] +pub struct AmlRiskConfig { + /// Enables post-delegation address risk checks. + pub enabled: bool, + /// Base URL for the Range API. + pub base_url: String, + /// API key used for Range API authorization. + pub api_key: Option, + /// TTL for cache entries. + #[serde(with = "humantime")] + pub cache_ttl: Duration, + /// Request timeout for Range API calls. + #[serde(with = "humantime")] + pub request_timeout: Duration, + /// Threshold on a scale of 0-10 for the risk score. + pub risk_score_threshold: u64, +} + +impl Default for AmlRiskConfig { + fn default() -> Self { + Self { + enabled: false, + base_url: consts::DEFAULT_RISK_BASE_URL.to_string(), + api_key: None, + cache_ttl: Duration::from_secs(consts::DEFAULT_RISK_CACHE_TTL_SEC), + request_timeout: Duration::from_secs( + consts::DEFAULT_RISK_REQUEST_TIMEOUT_SEC, + ), + risk_score_threshold: consts::DEFAULT_RISK_SCORE_THRESHOLD, + } + } +} diff --git a/magicblock-config/src/config/chain.rs b/magicblock-config/src/config/chain.rs index ff9aa912e..cd173ddc6 100644 --- a/magicblock-config/src/config/chain.rs +++ b/magicblock-config/src/config/chain.rs @@ -65,9 +65,6 @@ pub struct ChainLinkConfig { /// overwhelming the RPC provider. Default is 50ms. #[serde(with = "humantime")] pub resubscription_delay: Duration, - - /// AML/Risk checks for post-delegation actions via Range API. - pub risk: RiskConfig, } impl Default for ChainLinkConfig { @@ -79,42 +76,6 @@ impl Default for ChainLinkConfig { resubscription_delay: Duration::from_millis( consts::DEFAULT_RESUBSCRIPTION_DELAY_MS, ), - risk: RiskConfig::default(), - } - } -} - -/// Configuration for checking account risk with the Range API and a local sqlite cache. -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] -pub struct RiskConfig { - /// Enables post-delegation address risk checks. - pub enabled: bool, - /// Base URL for the Range API. - pub base_url: String, - /// API key used for Range API authorization. - pub api_key: Option, - /// TTL for cache entries. - #[serde(with = "humantime")] - pub cache_ttl: Duration, - /// Request timeout for Range API calls. - #[serde(with = "humantime")] - pub request_timeout: Duration, - /// Threshold on a scale of 0-10 for the risk score. - pub risk_score_threshold: u64, -} - -impl Default for RiskConfig { - fn default() -> Self { - Self { - enabled: false, - base_url: consts::DEFAULT_RISK_BASE_URL.to_string(), - api_key: None, - cache_ttl: Duration::from_secs(consts::DEFAULT_RISK_CACHE_TTL_SEC), - request_timeout: Duration::from_secs( - consts::DEFAULT_RISK_REQUEST_TIMEOUT_SEC, - ), - risk_score_threshold: consts::DEFAULT_RISK_SCORE_THRESHOLD, } } } diff --git a/magicblock-config/src/config/mod.rs b/magicblock-config/src/config/mod.rs index 5d9377d10..2496ba64d 100644 --- a/magicblock-config/src/config/mod.rs +++ b/magicblock-config/src/config/mod.rs @@ -1,4 +1,5 @@ pub mod accounts; +pub mod aml_risk; pub mod aperture; pub mod chain; pub mod cli; @@ -7,19 +8,21 @@ pub mod ledger; pub mod lifecycle; pub mod metrics; pub mod program; +pub mod query_filtering; pub mod scheduler; pub mod validator; // Re-export types for backward compatibility and easier access pub use accounts::{AccountsDbConfig, BlockSize}; +pub use aml_risk::AmlRiskConfig; pub use aperture::ApertureConfig; pub use chain::{ AllowedProgram, ChainLinkConfig, ChainOperationConfig, CommittorConfig, - RiskConfig, }; pub use grpc::GrpcConfig; pub use ledger::LedgerConfig; pub use lifecycle::LifecycleMode; pub use program::LoadableProgram; +pub use query_filtering::QueryFilteringConfig; pub use scheduler::TaskSchedulerConfig; pub use validator::ValidatorConfig; diff --git a/magicblock-config/src/config/query_filtering.rs b/magicblock-config/src/config/query_filtering.rs new file mode 100644 index 000000000..36a3c871c --- /dev/null +++ b/magicblock-config/src/config/query_filtering.rs @@ -0,0 +1,28 @@ +use serde::{Deserialize, Serialize}; + +use crate::consts; + +/// Configuration for local query filtering in Aperture. +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "kebab-case", deny_unknown_fields, default)] +pub struct QueryFilteringConfig { + /// Whether Aperture should apply local permission-aware response filtering. + pub enabled: bool, + /// JWT secret used by the local query filtering login route. + pub jwt_secret: String, + /// Token expiration time in days. + pub token_expiry_days: i64, + /// Challenge expiration time in seconds. + pub challenge_ttl_seconds: i64, +} + +impl Default for QueryFilteringConfig { + fn default() -> Self { + Self { + enabled: false, + jwt_secret: consts::DEFAULT_JWT_SECRET.to_string(), + token_expiry_days: consts::DEFAULT_TOKEN_EXPIRY_DAYS, + challenge_ttl_seconds: consts::DEFAULT_CHALLENGE_TTL_SECONDS, + } + } +} diff --git a/magicblock-config/src/consts.rs b/magicblock-config/src/consts.rs index e1452b7b4..d9d27fa1b 100644 --- a/magicblock-config/src/consts.rs +++ b/magicblock-config/src/consts.rs @@ -78,6 +78,7 @@ pub const DEFAULT_RESUBSCRIPTION_DELAY_MS: u64 = 50; /// Default capacity for the LRU cache of subscribed accounts pub const DEFAULT_MAX_MONITORED_ACCOUNTS: usize = 5_000; +// AML Risk Defaults /// Default base URL for Range risk service pub const DEFAULT_RISK_BASE_URL: &str = "https://api.range.org/v1"; @@ -89,3 +90,13 @@ pub const DEFAULT_RISK_REQUEST_TIMEOUT_SEC: u64 = 5; /// Default risk score threshold for Range risk service pub const DEFAULT_RISK_SCORE_THRESHOLD: u64 = 5; + +// Query Filtering Defaults +/// Default JWT secret for query filtering +pub const DEFAULT_JWT_SECRET: &str = "your_jwt_secret_key"; + +/// Default token expiry days for query filtering +pub const DEFAULT_TOKEN_EXPIRY_DAYS: i64 = 30; + +/// Default challenge TTL seconds for query filtering +pub const DEFAULT_CHALLENGE_TTL_SECONDS: i64 = 5 * 60; diff --git a/magicblock-config/src/lib.rs b/magicblock-config/src/lib.rs index d69b549ae..3f9932c23 100644 --- a/magicblock-config/src/lib.rs +++ b/magicblock-config/src/lib.rs @@ -21,9 +21,9 @@ pub mod types; use crate::{ config::{ - AccountsDbConfig, ChainLinkConfig, ChainOperationConfig, - CommittorConfig, LedgerConfig, LoadableProgram, TaskSchedulerConfig, - ValidatorConfig, + AccountsDbConfig, AmlRiskConfig, ChainLinkConfig, ChainOperationConfig, + CommittorConfig, LedgerConfig, LoadableProgram, QueryFilteringConfig, + TaskSchedulerConfig, ValidatorConfig, }, types::Remote, }; @@ -60,6 +60,12 @@ pub struct ValidatorParams { /// Aperture-specific configuration. pub aperture: ApertureConfig, + /// AML risk configuration. + pub aml_risk: AmlRiskConfig, + + /// Query filtering configuration. + pub query_filtering: QueryFilteringConfig, + // --- File-Only Configuration --- pub commit: CommittorConfig, pub accountsdb: AccountsDbConfig, diff --git a/magicblock-config/src/tests.rs b/magicblock-config/src/tests.rs index 50632ad6e..2ecad3653 100644 --- a/magicblock-config/src/tests.rs +++ b/magicblock-config/src/tests.rs @@ -66,6 +66,7 @@ fn test_defaults_are_sane() { // Remotes default to [devnet HTTP] + [devnet WS] (added by ensure_websocket) assert_eq!(config.remotes.len(), 2); assert_eq!(config.aperture.listen.0.port(), 8899); + assert_eq!(config.lifecycle, LifecycleMode::Ephemeral); // Verify internal config defaults (not exposed to CLI) @@ -77,6 +78,38 @@ fn test_defaults_are_sane() { assert_eq!(config.grpc.max_old_unoptimized, 5); assert_eq!(config.grpc.max_subs_in_new, 400); assert_eq!(config.grpc.max_time_without_optimization_secs, 60); + + // Query filtering defaults + assert!(!config.query_filtering.enabled); + assert_eq!( + config.query_filtering.jwt_secret, + consts::DEFAULT_JWT_SECRET + ); + assert_eq!( + config.query_filtering.token_expiry_days, + consts::DEFAULT_TOKEN_EXPIRY_DAYS + ); + assert_eq!( + config.query_filtering.challenge_ttl_seconds, + consts::DEFAULT_CHALLENGE_TTL_SECONDS + ); + + // AML risk defaults + assert!(!config.aml_risk.enabled); + assert_eq!(config.aml_risk.base_url, consts::DEFAULT_RISK_BASE_URL); + assert_eq!(config.aml_risk.api_key, None); + assert_eq!( + config.aml_risk.cache_ttl, + Duration::from_secs(consts::DEFAULT_RISK_CACHE_TTL_SEC) + ); + assert_eq!( + config.aml_risk.request_timeout, + Duration::from_secs(consts::DEFAULT_RISK_REQUEST_TIMEOUT_SEC) + ); + assert_eq!( + config.aml_risk.risk_score_threshold, + consts::DEFAULT_RISK_SCORE_THRESHOLD + ); } #[test] @@ -269,7 +302,7 @@ fn test_chainlink_config() { max-monitored-accounts = 5000 resubscription-delay = "50ms" - [chainlink.risk] + [aml-risk] enabled = true api-key = "test-token" cache-ttl = "20m" @@ -285,20 +318,11 @@ fn test_chainlink_config() { config.chainlink.resubscription_delay, std::time::Duration::from_millis(50) ); - assert!(config.chainlink.risk.enabled); - assert_eq!( - config.chainlink.risk.api_key, - Some("test-token".to_string()) - ); - assert_eq!( - config.chainlink.risk.cache_ttl, - std::time::Duration::from_secs(20 * 60) - ); - assert_eq!( - config.chainlink.risk.request_timeout, - std::time::Duration::from_secs(2) - ); - assert_eq!(config.chainlink.risk.risk_score_threshold, 8); + assert!(config.aml_risk.enabled); + assert_eq!(config.aml_risk.api_key, Some("test-token".to_owned())); + assert_eq!(config.aml_risk.cache_ttl, Duration::from_secs(20 * 60)); + assert_eq!(config.aml_risk.request_timeout, Duration::from_secs(2)); + assert_eq!(config.aml_risk.risk_score_threshold, 8); } // ============================================================================ @@ -466,7 +490,6 @@ fn test_example_config_full_coverage() { // 9. Chainlink (Cloning) // ======================================================================== assert_eq!(config.chainlink.max_monitored_accounts, 5000); - assert!(!config.chainlink.risk.enabled); // ======================================================================== // 10. Aperture @@ -481,15 +504,50 @@ fn test_example_config_full_coverage() { assert!(!config.task_scheduler.reset); assert_eq!( config.task_scheduler.min_interval, - Duration::from_millis(10) + Duration::from_millis( + consts::DEFAULT_TASK_SCHEDULER_MIN_INTERVAL_MILLIS + ) ); assert_eq!( config.task_scheduler.failed_task_retention, - Duration::from_secs(14 * 24 * 60 * 60) + Duration::from_secs( + consts::DEFAULT_TASK_SCHEDULER_FAILED_TASK_RETENTION_SECS + ) ); assert_eq!( config.task_scheduler.failed_task_cleanup_interval, - Duration::from_secs(60 * 60) + Duration::from_secs( + consts::DEFAULT_TASK_SCHEDULER_FAILED_TASK_CLEANUP_INTERVAL_SECS + ) + ); + + assert!(!config.query_filtering.enabled); + assert_eq!( + config.query_filtering.jwt_secret, + consts::DEFAULT_JWT_SECRET + ); + assert_eq!( + config.query_filtering.token_expiry_days, + consts::DEFAULT_TOKEN_EXPIRY_DAYS + ); + assert_eq!( + config.query_filtering.challenge_ttl_seconds, + consts::DEFAULT_CHALLENGE_TTL_SECONDS + ); + + assert!(!config.aml_risk.enabled); + assert_eq!(config.aml_risk.api_key, None); + assert_eq!( + config.aml_risk.cache_ttl, + Duration::from_secs(consts::DEFAULT_RISK_CACHE_TTL_SEC) + ); + assert_eq!( + config.aml_risk.request_timeout, + Duration::from_secs(consts::DEFAULT_RISK_REQUEST_TIMEOUT_SEC) + ); + assert_eq!( + config.aml_risk.risk_score_threshold, + consts::DEFAULT_RISK_SCORE_THRESHOLD ); // The example file has the programs section with 2 entries @@ -555,11 +613,6 @@ fn test_env_vars_full_coverage() { // --- Chainlink --- EnvVarGuard::new("MBV_CHAINLINK__MAX_MONITORED_ACCOUNTS", "123"), EnvVarGuard::new("MBV_CHAINLINK__RESUBSCRIPTION_DELAY", "150ms"), - EnvVarGuard::new("MBV_CHAINLINK__RISK__ENABLED", "true"), - EnvVarGuard::new("MBV_CHAINLINK__RISK__API_KEY", "env-range-token"), - EnvVarGuard::new("MBV_CHAINLINK__RISK__CACHE_TTL", "45m"), - EnvVarGuard::new("MBV_CHAINLINK__RISK__REQUEST_TIMEOUT", "3s"), - EnvVarGuard::new("MBV_CHAINLINK__RISK__RISK_SCORE_THRESHOLD", "8"), // --- Task Scheduler --- EnvVarGuard::new("MBV_TASK_SCHEDULER__RESET", "true"), EnvVarGuard::new("MBV_TASK_SCHEDULER__MIN_INTERVAL", "99ms"), @@ -568,6 +621,17 @@ fn test_env_vars_full_coverage() { "MBV_TASK_SCHEDULER__FAILED_TASK_CLEANUP_INTERVAL", "3m", ), + // --- Query Filtering --- + EnvVarGuard::new("MBV_QUERY_FILTERING__ENABLED", "true"), + EnvVarGuard::new("MBV_QUERY_FILTERING__JWT_SECRET", "env-jwt-secret"), + EnvVarGuard::new("MBV_QUERY_FILTERING__TOKEN_EXPIRY_DAYS", "7"), + EnvVarGuard::new("MBV_QUERY_FILTERING__CHALLENGE_TTL_SECONDS", "60"), + // --- AML Risk --- + EnvVarGuard::new("MBV_AML_RISK__ENABLED", "true"), + EnvVarGuard::new("MBV_AML_RISK__API_KEY", "env-range-token"), + EnvVarGuard::new("MBV_AML_RISK__CACHE_TTL", "45m"), + EnvVarGuard::new("MBV_AML_RISK__REQUEST_TIMEOUT", "3s"), + EnvVarGuard::new("MBV_AML_RISK__RISK_SCORE_THRESHOLD", "8"), // --- Chain Operation (Optional Section) --- // Figment can instantiate optional structs if their fields are present EnvVarGuard::new("MBV_CHAIN_OPERATION__COUNTRY_CODE", "DE"), @@ -629,20 +693,6 @@ fn test_env_vars_full_coverage() { config.chainlink.resubscription_delay, Duration::from_millis(150) ); - assert!(config.chainlink.risk.enabled); - assert_eq!( - config.chainlink.risk.api_key, - Some("env-range-token".to_string()) - ); - assert_eq!( - config.chainlink.risk.cache_ttl, - Duration::from_secs(45 * 60) - ); - assert_eq!( - config.chainlink.risk.request_timeout, - Duration::from_secs(3) - ); - assert_eq!(config.chainlink.risk.risk_score_threshold, 8); // Task Scheduler assert!(config.task_scheduler.reset); @@ -659,6 +709,19 @@ fn test_env_vars_full_coverage() { Duration::from_secs(3 * 60) ); + // Query Filtering + assert!(config.query_filtering.enabled); + assert_eq!(config.query_filtering.jwt_secret, "env-jwt-secret"); + assert_eq!(config.query_filtering.token_expiry_days, 7); + assert_eq!(config.query_filtering.challenge_ttl_seconds, 60); + + // AML Risk + assert!(config.aml_risk.enabled); + assert_eq!(config.aml_risk.api_key, Some("env-range-token".to_string())); + assert_eq!(config.aml_risk.cache_ttl, Duration::from_secs(45 * 60)); + assert_eq!(config.aml_risk.request_timeout, Duration::from_secs(3)); + assert_eq!(config.aml_risk.risk_score_threshold, 8); + // Chain Operation // Verify the optional struct was created and populated let chain_op = config diff --git a/magicblock-query-filtering/Cargo.toml b/magicblock-query-filtering/Cargo.toml new file mode 100644 index 000000000..dfaa0f224 --- /dev/null +++ b/magicblock-query-filtering/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "magicblock-query-filtering" +version.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +license.workspace = true +edition.workspace = true + +[features] +default = [] +# Full Trusted Execution Environment integration: query filtering plus the TDX +# attestation quote endpoints. When `tee` is off the quote endpoints return an +# error +tee = [] + +[dependencies] +base64 = { workspace = true } +chrono = { workspace = true } +ed25519-dalek = { workspace = true } +jsonwebtoken = { workspace = true } +magicblock-accounts-db = { workspace = true } +magicblock-config = { workspace = true } +magicblock-core = { workspace = true } +magicblock-aml = { workspace = true } +rand = { workspace = true } +scc = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +sha2 = { workspace = true } +solana-account = { workspace = true } +solana-account-decoder-client-types = { workspace = true } +solana-message = { workspace = true } +solana-program-pack = { workspace = true } +solana-pubkey = { workspace = true, features = ["curve25519"] } +solana-signature = { workspace = true } +solana-transaction = { workspace = true } +solana-transaction-status = { workspace = true, features = [ + "agave-unstable-api", +] } +spl-token = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt", "sync"] } +tracing = { workspace = true } + +[dev-dependencies] +solana-hash = { workspace = true } +solana-keypair = { workspace = true } +solana-signer = { workspace = true } diff --git a/magicblock-query-filtering/src/auth/mod.rs b/magicblock-query-filtering/src/auth/mod.rs new file mode 100644 index 000000000..cf8f67c4c --- /dev/null +++ b/magicblock-query-filtering/src/auth/mod.rs @@ -0,0 +1,6 @@ +mod service; +mod token; + +pub use service::{AuthError, AuthResult, AuthService}; + +pub use crate::service::QueryFilteringService; diff --git a/magicblock-query-filtering/src/auth/service.rs b/magicblock-query-filtering/src/auth/service.rs new file mode 100644 index 000000000..28439b511 --- /dev/null +++ b/magicblock-query-filtering/src/auth/service.rs @@ -0,0 +1,202 @@ +use std::{str::FromStr, sync::Arc}; + +use chrono::{Duration, Utc}; +use magicblock_aml::{RiskError, RiskService}; +use magicblock_config::consts; +use solana_pubkey::Pubkey; +use solana_signature::Signature; +use thiserror::Error; +use tracing::*; + +use crate::{ + auth::token::AuthTokenGenerator, + types::{LoginRequest, LoginResponse}, +}; + +pub type AuthResult = Result; + +#[derive(Debug, Error)] +pub enum AuthError { + #[error(transparent)] + Json(#[from] serde_json::Error), + #[error(transparent)] + ParsePubkey(#[from] solana_pubkey::ParsePubkeyError), + #[error(transparent)] + ParseSignature(#[from] solana_signature::ParseSignatureError), + #[error(transparent)] + Jwt(#[from] jsonwebtoken::errors::Error), + #[error("signature verification failed")] + SignatureVerification, + #[error("challenge expired")] + ChallengeExpired, + #[error("invalid challenge date")] + InvalidChallengeDate, + #[error("invalid challenge format")] + InvalidChallengeFormat, + #[error("invalid challenge user pubkey")] + InvalidChallengeUserPubkey, + #[error(transparent)] + Risk(#[from] RiskError), + #[error("token expired")] + TokenExpired, + #[error("challenge_ttl_seconds is less than 0!")] + InvalidChallengeTtlSeconds, +} + +pub struct AuthService { + risk: Option>, + challenge_ttl_seconds: i64, + token_generator: AuthTokenGenerator, +} + +impl AuthService { + pub fn try_new( + jwt_secret: &str, + token_expiry_days: i64, + challenge_ttl_seconds: i64, + risk: Option>, + ) -> AuthResult { + if jwt_secret == consts::DEFAULT_JWT_SECRET { + // Not failing here so that test setups can use the default secret + error!( + "query_filtering is enabled but default jwt_secret is used!" + ); + } + + if challenge_ttl_seconds < 0 { + error!("query_filtering is enabled but challenge_ttl_seconds is less than 0!"); + return Err(AuthError::InvalidChallengeTtlSeconds); + } + + Ok(Self { + risk, + challenge_ttl_seconds, + token_generator: AuthTokenGenerator::new( + jwt_secret, + token_expiry_days, + ), + }) + } + + pub fn generate_challenge(&self, user_pubkey: &str) -> String { + format!( + "Login to Query Filtering Service\nTimestamp: {}\nUser: {}", + Utc::now().timestamp(), + user_pubkey + ) + } + + pub fn verify_challenge( + &self, + challenge: &str, + user_pubkey: &str, + ) -> AuthResult<()> { + let lines: Vec<&str> = challenge.lines().collect(); + if lines.len() != 3 + || lines[0] != "Login to Query Filtering Service" + || !lines[1].starts_with("Timestamp: ") + || !lines[2].starts_with("User: ") + { + return Err(AuthError::InvalidChallengeFormat); + } + + if &lines[2][6..] != user_pubkey { + return Err(AuthError::InvalidChallengeUserPubkey); + } + + let timestamp = lines[1][11..] + .parse::() + .map_err(|_| AuthError::InvalidChallengeDate)?; + let challenge_time = chrono::DateTime::from_timestamp(timestamp, 0) + .ok_or(AuthError::InvalidChallengeDate)?; + let now = Utc::now(); + + let age = now.signed_duration_since(challenge_time); + if age > Duration::seconds(self.challenge_ttl_seconds) + || age < Duration::zero() + { + return Err(AuthError::ChallengeExpired); + } + + Ok(()) + } + + pub fn verify_signature(&self, request: &LoginRequest) -> AuthResult<()> { + let pubkey = Pubkey::from_str(&request.pubkey)?; + let signature = Signature::from_str(&request.signature)?; + + self.verify_challenge(&request.challenge, &request.pubkey)?; + if !signature.verify(pubkey.as_ref(), request.challenge.as_bytes()) { + return Err(AuthError::SignatureVerification); + } + + Ok(()) + } + + pub async fn login( + &self, + request: LoginRequest, + ) -> AuthResult { + self.verify_signature(&request)?; + if let Some(risk) = &self.risk { + risk.check_addresses(vec![request.pubkey.clone()]).await?; + } + let token = self.token_generator.generate(&request.pubkey)?; + Ok(LoginResponse { token }) + } + + pub fn verify_token(&self, token: &str) -> AuthResult { + let claims = self.token_generator.verify(token)?; + + Ok(claims.pubkey) + } +} + +#[cfg(test)] +mod tests { + use solana_keypair::Keypair; + use solana_signer::Signer; + + use super::*; + + #[tokio::test] + async fn login_issues_verifiable_token() { + let auth = AuthService::try_new("test-secret", 30, 60, None).unwrap(); + let keypair = Keypair::new(); + let pubkey = keypair.pubkey().to_string(); + let challenge = auth.generate_challenge(&pubkey); + let signature = keypair.sign_message(challenge.as_bytes()).to_string(); + + let response = auth + .login(LoginRequest { + pubkey: pubkey.clone(), + signature, + challenge, + }) + .await + .unwrap(); + let claims = auth.token_generator.verify(&response.token).unwrap(); + + assert_eq!(claims.pubkey, pubkey); + } + + #[tokio::test] + async fn login_rejects_challenge_for_other_pubkey() { + let auth = AuthService::try_new("test-secret", 30, 60, None).unwrap(); + let signer = Keypair::new(); + let other = Keypair::new().pubkey().to_string(); + let challenge = auth.generate_challenge(&other); + let signature = signer.sign_message(challenge.as_bytes()).to_string(); + + let err = auth + .login(LoginRequest { + pubkey: signer.pubkey().to_string(), + signature, + challenge, + }) + .await + .unwrap_err(); + + assert!(matches!(err, AuthError::InvalidChallengeUserPubkey)); + } +} diff --git a/magicblock-query-filtering/src/auth/token.rs b/magicblock-query-filtering/src/auth/token.rs new file mode 100644 index 000000000..add67cf7d --- /dev/null +++ b/magicblock-query-filtering/src/auth/token.rs @@ -0,0 +1,125 @@ +use chrono::{Duration, Utc}; +use jsonwebtoken::{ + decode, encode, errors::Error as JwtError, DecodingKey, EncodingKey, + Header, Validation, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Claims { + pub pubkey: String, + pub exp: usize, + pub iat: usize, +} + +#[derive(Clone)] +pub struct AuthTokenGenerator { + encoding_key: EncodingKey, + decoding_key: DecodingKey, + pub token_expiry_days: i64, +} + +impl AuthTokenGenerator { + /// Creates a new AuthTokenGenerator. + /// + /// # Arguments + /// * `jwt_secret` - The JWT secret to use for encoding and decoding tokens + /// * `token_expiry_days` - The number of days the token will be valid for + /// + /// # Returns + /// The new AuthTokenGenerator + pub fn new(jwt_secret: &str, token_expiry_days: i64) -> Self { + Self { + encoding_key: EncodingKey::from_secret(jwt_secret.as_bytes()), + decoding_key: DecodingKey::from_secret(jwt_secret.as_bytes()), + token_expiry_days, + } + } + + /// Generates a JWT token for the given pubkey. + /// + /// # Arguments + /// * `pubkey` - The pubkey to generate a token for + /// + /// # Returns + /// The generated JWT token + pub fn generate(&self, pubkey: &str) -> Result { + let now = Utc::now(); + let exp = now + Duration::days(self.token_expiry_days); + let claims = Claims { + pubkey: pubkey.to_owned(), + exp: exp.timestamp() as usize, + iat: now.timestamp() as usize, + }; + + encode(&Header::default(), &claims, &self.encoding_key) + } + + /// Verifies a JWT token and returns the claims. + /// + /// # Arguments + /// * `token` - The JWT token to verify + /// + /// # Returns + /// The claims if the token is valid + pub fn verify(&self, token: &str) -> Result { + Ok( + decode::( + token, + &self.decoding_key, + &Validation::default(), + )? + .claims, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generate_and_verify() { + let generator = AuthTokenGenerator::new("test-secret", 30); + let token = generator.generate("test-pubkey").unwrap(); + let claims = generator.verify(&token).unwrap(); + assert_eq!(claims.pubkey, "test-pubkey"); + } + + #[test] + fn test_verify_invalid_token() { + let generator = AuthTokenGenerator::new("test-secret", 30); + let token = "invalid-token"; + let result = generator.verify(token); + assert!(result.is_err()); + } + + #[test] + fn test_verify_expired_token() { + let generator = AuthTokenGenerator::new("test-secret", 30); + let past_time = Utc::now() - Duration::days(31); + let claims = Claims { + pubkey: "test-pubkey".to_owned(), + exp: past_time.timestamp() as usize, + iat: (past_time - Duration::days(1)).timestamp() as usize, + }; + let token = + encode(&Header::default(), &claims, &generator.encoding_key) + .unwrap(); + let result = generator.verify(&token); + assert!( + result.is_err(), + "Expected expired token to fail verification" + ); + } + + #[test] + fn test_verify_invalid_secret() { + let generator = AuthTokenGenerator::new("valid-secret", 30); + let wrong_generator = AuthTokenGenerator::new("invalid-secret", 30); + let token = generator.generate("test-pubkey").unwrap(); + let result = wrong_generator.verify(&token); + assert!(result.is_err()); + } +} diff --git a/magicblock-query-filtering/src/lib.rs b/magicblock-query-filtering/src/lib.rs new file mode 100644 index 000000000..f30ceb7c0 --- /dev/null +++ b/magicblock-query-filtering/src/lib.rs @@ -0,0 +1,21 @@ +use solana_pubkey::{pubkey, Pubkey}; + +pub mod auth; +pub mod quote; +pub mod service; +pub mod transaction; +pub mod types; + +pub use service::{ + filter_account, filter_accounts, filter_keyed_accounts, + filter_program_accounts, filter_signatures, permission_for_account, + permissions_for_accounts, visible_transaction_accounts, + QueryFilteringError, QueryFilteringService, +}; +pub use transaction::{ + check_transaction_admission, filter_confirmed_transaction, + WHITELISTED_PROGRAMS, +}; + +pub const PERMISSION_PROGRAM_ID: Pubkey = + pubkey!("ACLseoPoyC3cBqoUtkbjZ4aDrkurZW86v19pXz2XQnp1"); diff --git a/magicblock-query-filtering/src/quote/mod.rs b/magicblock-query-filtering/src/quote/mod.rs new file mode 100644 index 000000000..a5dec92db --- /dev/null +++ b/magicblock-query-filtering/src/quote/mod.rs @@ -0,0 +1,146 @@ +use thiserror::Error; + +use crate::types::{FastQuoteResponse, QuoteResponse}; + +#[cfg(feature = "tee")] +mod tdx_guest_native; + +pub type QuoteResult = Result; + +#[derive(Debug, Error)] +pub enum QuoteError { + #[error("challenge must decode to 64 bytes")] + InvalidChallenge, + #[error(transparent)] + #[cfg(feature = "tee")] + TdxGuest(#[from] tdx_guest_native::TdxGuestError), + #[error(transparent)] + Base64(#[from] base64::DecodeError), + #[error(transparent)] + #[cfg(feature = "tee")] + Join(#[from] tokio::task::JoinError), + #[error("TEE attestation quote feature is not enabled in this build")] + Disabled, +} + +#[cfg(feature = "tee")] +mod tee { + use base64::{prelude::BASE64_STANDARD, Engine}; + use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey}; + use sha2::{Digest, Sha256, Sha512}; + use tokio::{sync::OnceCell, task::spawn_blocking}; + + use super::{ + tdx_guest_native, FastQuoteResponse, QuoteError, QuoteResponse, + QuoteResult, + }; + + pub struct AttestedKeyMaterial { + pub quote: Vec, + pub verifying_key: VerifyingKey, + pub signing_key: SigningKey, + pub report_data_sha256: [u8; 32], + } + + static ATTESTED: OnceCell = OnceCell::const_new(); + + fn decode_challenge(challenge: &str) -> QuoteResult<[u8; 64]> { + let challenge_bytes = BASE64_STANDARD.decode(challenge)?; + challenge_bytes + .try_into() + .map_err(|_| QuoteError::InvalidChallenge) + } + + fn init_attested_key_blocking() -> QuoteResult { + let signing_key = SigningKey::from_bytes(&rand::random::<[u8; 32]>()); + let verifying_key = signing_key.verifying_key(); + + let mut report_data = [0u8; 64]; + let digest = Sha512::digest(verifying_key.as_bytes()); + report_data.copy_from_slice(&digest); + let report_data_sha256: [u8; 32] = Sha256::digest(report_data).into(); + + let quote = tdx_guest_native::get_tdx_quote(report_data)?; + + Ok(AttestedKeyMaterial { + quote, + verifying_key, + signing_key, + report_data_sha256, + }) + } + + pub(crate) async fn init_attested_key( + ) -> QuoteResult<&'static AttestedKeyMaterial> { + ATTESTED + .get_or_try_init(|| async move { + if !tdx_guest_native::tdx_guest_device_present() { + return Err(tdx_guest_native::TdxGuestError::NoDevice( + tdx_guest_native::CONFIGFS_TSM_REPORT_PATH, + ) + .into()); + } + spawn_blocking(init_attested_key_blocking).await? + }) + .await + } + + pub(crate) async fn quote(challenge: &str) -> QuoteResult { + if !tdx_guest_native::tdx_guest_device_present() { + return Err(tdx_guest_native::TdxGuestError::NoDevice( + tdx_guest_native::CONFIGFS_TSM_REPORT_PATH, + ) + .into()); + } + + let report_data = decode_challenge(challenge)?; + let quote = spawn_blocking(move || { + tdx_guest_native::get_tdx_quote(report_data) + }) + .await??; + + Ok(QuoteResponse { + quote: BASE64_STANDARD.encode(quote), + }) + } + + pub(crate) async fn fast_quote( + challenge: &str, + ) -> QuoteResult { + let challenge_bytes = BASE64_STANDARD.decode(challenge)?; + let key_material = init_attested_key().await?; + let sig: Signature = key_material.signing_key.sign(&challenge_bytes); + + Ok(FastQuoteResponse { + quote: BASE64_STANDARD.encode(&key_material.quote), + report_data_sha256: BASE64_STANDARD + .encode(key_material.report_data_sha256), + pubkey: BASE64_STANDARD + .encode(key_material.verifying_key.as_bytes()), + challenge: challenge.to_owned(), + signature: BASE64_STANDARD.encode(sig.to_bytes()), + }) + } +} + +#[cfg(feature = "tee")] +pub async fn quote(challenge: &str) -> QuoteResult { + tee::quote(challenge).await +} + +#[cfg(feature = "tee")] +pub async fn fast_quote(challenge: &str) -> QuoteResult { + tee::fast_quote(challenge).await +} + +/// Stub returned when the crate is built without the `tee` feature. The TDX +/// attestation code is compiled out, so any quote request is rejected. +#[cfg(not(feature = "tee"))] +pub async fn quote(_challenge: &str) -> QuoteResult { + Err(QuoteError::Disabled) +} + +#[cfg(not(feature = "tee"))] +pub async fn fast_quote(_challenge: &str) -> QuoteResult { + Err(QuoteError::Disabled) +} diff --git a/magicblock-query-filtering/src/quote/tdx_guest_native.rs b/magicblock-query-filtering/src/quote/tdx_guest_native.rs new file mode 100644 index 000000000..3cdf172d6 --- /dev/null +++ b/magicblock-query-filtering/src/quote/tdx_guest_native.rs @@ -0,0 +1,104 @@ +use std::{ + fs::{self, File}, + io::{self, ErrorKind, Read, Write}, + path::{Path, PathBuf}, +}; + +use sha2::{Digest, Sha256}; +use thiserror::Error; + +pub const CONFIGFS_TSM_REPORT_PATH: &str = "/sys/kernel/config/tsm/report"; + +#[derive(Error, Debug)] +pub enum TdxGuestError { + #[error( + "TSM report interface missing ({0}); need kernel configfs-tsm and tdx_guest provider" + )] + NoDevice(&'static str), + #[error("TDX quote: {0}")] + Quote(String), + #[error(transparent)] + Io(#[from] io::Error), +} + +pub fn tdx_guest_device_present() -> bool { + Path::new(CONFIGFS_TSM_REPORT_PATH).is_dir() +} + +fn quote_dir_name(input: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(input); + let hash = hasher.finalize(); + hash.iter().map(|b| format!("{:02x}", b)).collect() +} + +fn trim_newline(s: &mut String) { + if s.ends_with('\n') { + s.pop(); + if s.ends_with('\r') { + s.pop(); + } + } +} + +fn read_generation(quote_path: &Path) -> Result { + let mut s = fs::read_to_string(quote_path.join("generation"))?; + trim_newline(&mut s); + s.parse() + .map_err(|_| TdxGuestError::Quote("invalid generation counter".into())) +} + +pub fn get_tdx_quote(report_data: [u8; 64]) -> Result, TdxGuestError> { + let base = Path::new(CONFIGFS_TSM_REPORT_PATH); + if !base.is_dir() { + return Err(TdxGuestError::NoDevice(CONFIGFS_TSM_REPORT_PATH)); + } + + let name = quote_dir_name(&report_data); + let quote_path: PathBuf = base.join(&name); + + fs::create_dir("e_path).or_else(|e| { + if e.kind() == ErrorKind::AlreadyExists { + Ok(()) + } else { + Err(e) + } + })?; + + let mut provider = fs::read_to_string(quote_path.join("provider"))?; + trim_newline(&mut provider); + if provider != "tdx_guest" { + return Err(TdxGuestError::Quote(format!( + "TSM provider is {provider:?}, expected tdx_guest" + ))); + } + + let mut expected_generation = read_generation("e_path)?; + + let mut inblob = File::create(quote_path.join("inblob"))?; + inblob.write_all(&report_data)?; + inblob.flush()?; + drop(inblob); + + expected_generation = expected_generation + .checked_add(1) + .ok_or_else(|| TdxGuestError::Quote("generation overflow".into()))?; + + let mut out = Vec::new(); + File::open(quote_path.join("outblob"))?.read_to_end(&mut out)?; + + let actual = read_generation("e_path)?; + if actual != expected_generation { + return Err(TdxGuestError::Quote(format!( + "quote generation mismatch: expected {expected_generation}, got {actual}" + ))); + } + + if out.is_empty() { + return Err(TdxGuestError::Quote( + "empty quote (check QGS/tdx-qgs and permissions)".into(), + )); + } + + Ok(out) +} diff --git a/magicblock-query-filtering/src/service.rs b/magicblock-query-filtering/src/service.rs new file mode 100644 index 000000000..71c09fc03 --- /dev/null +++ b/magicblock-query-filtering/src/service.rs @@ -0,0 +1,404 @@ +use std::{collections::HashSet, sync::Arc}; + +use magicblock_accounts_db::traits::AccountsBank; +use magicblock_aml::RiskService; +use magicblock_config::config::QueryFilteringConfig; +use solana_account::ReadableAccount; +use solana_pubkey::Pubkey; +use thiserror::Error; + +use crate::{ + auth::{AuthError, AuthService}, + types::{ + ChallengeResponse, LoginRequest, LoginResponse, Permission, + PermissionEntry, PermissionError, + }, +}; + +pub type QueryFilteringResult = Result; + +#[derive(Debug, Error)] +pub enum QueryFilteringError { + #[error(transparent)] + Permission(#[from] PermissionError), + #[error(transparent)] + Auth(#[from] AuthError), + #[error("access denied: transaction touches a restricted account")] + AccessDenied, + #[error("query filtering feature (`query-filtering`) is not enabled in this build")] + Disabled, +} + +pub struct QueryFilteringService { + auth_service: Arc, +} + +impl QueryFilteringService { + pub fn try_new( + config: &QueryFilteringConfig, + risk: Option>, + ) -> QueryFilteringResult { + Ok(Self { + auth_service: Arc::new(AuthService::try_new( + &config.jwt_secret, + config.token_expiry_days, + config.challenge_ttl_seconds, + risk, + )?), + }) + } + + pub fn verify_token(&self, token: &str) -> QueryFilteringResult { + Ok(self.auth_service.verify_token(token)?) + } + + pub fn get_challenge(&self, user_pubkey: &str) -> ChallengeResponse { + ChallengeResponse { + challenge: self.auth_service.generate_challenge(user_pubkey), + } + } + + pub async fn login( + &self, + request: LoginRequest, + ) -> QueryFilteringResult { + Ok(self.auth_service.login(request).await?) + } + + /// Graceful-shutdown hook invoked during validator teardown. The service + /// owns no background tasks today, so there is nothing to release. + pub fn stop(&self) {} +} + +pub fn filter_account( + value: Option, + permission: &PermissionEntry, + user: &Pubkey, +) -> Option { + permission + .access_for(user) + .account + .then_some(value) + .flatten() +} + +pub fn filter_accounts( + values: Vec>, + permissions: &[PermissionEntry], + user: &Pubkey, +) -> Vec> { + values + .into_iter() + .zip(permissions) + .map(|(value, permission)| filter_account(value, permission, user)) + .collect() +} + +pub fn filter_signatures( + values: Vec, + permission: &PermissionEntry, + user: &Pubkey, +) -> Vec { + if permission.access_for(user).signatures { + values + } else { + Vec::new() + } +} + +pub fn filter_program_accounts( + values: Vec<(Pubkey, T)>, + program_permission: &PermissionEntry, + user: &Pubkey, +) -> Vec<(Pubkey, T)> { + if program_permission.access_for(user).account { + values + } else { + Vec::new() + } +} + +pub fn filter_keyed_accounts( + values: Vec<(Pubkey, T)>, + permissions: &[PermissionEntry], + user: &Pubkey, +) -> Vec<(Pubkey, T)> { + values + .into_iter() + .zip(permissions) + .filter_map(|(value, permission)| { + permission.access_for(user).account.then_some(value) + }) + .collect() +} + +/// Loads the on-chain permission for a single account, returning +/// [`PermissionEntry::Unrestricted`] when no permission account exists. +/// +/// Two cases short-circuit to [`PermissionEntry::Unrestricted`]: +/// - The queried account is itself a permission account (owned by the +/// permission program). Permission accounts hold the ACL rules themselves +/// and must remain visible to everyone — they are never subject to +/// permission filtering. +/// - The permission PDA isn't owned by the permission program +pub fn permission_for_account( + accounts_db: &impl AccountsBank, + account: &Pubkey, +) -> QueryFilteringResult { + if let Some(account_data) = accounts_db.get_account(account) { + if account_data.owner() == &crate::PERMISSION_PROGRAM_ID { + return Ok(PermissionEntry::Unrestricted); + } + } + let permission_account = Permission::pda(account); + let Some(account) = accounts_db.get_account(&permission_account) else { + return Ok(PermissionEntry::Unrestricted); + }; + if account.owner() != &crate::PERMISSION_PROGRAM_ID { + return Ok(PermissionEntry::Unrestricted); + } + Ok(PermissionEntry::from_permission(Permission::decode( + account.data(), + )?)) +} + +/// Loads permissions for a batch of accounts, preserving input order so the +/// result can be zipped positionally with the corresponding accounts. +pub fn permissions_for_accounts( + accounts_db: &impl AccountsBank, + accounts: &[Pubkey], +) -> QueryFilteringResult> { + accounts + .iter() + .map(|account| permission_for_account(accounts_db, account)) + .collect() +} + +pub fn visible_transaction_accounts( + accounts_db: &impl AccountsBank, + accounts: &[Pubkey], + user: &Pubkey, +) -> QueryFilteringResult> { + let permissions = permissions_for_accounts(accounts_db, accounts)?; + Ok(accounts + .iter() + .zip(permissions) + .filter_map(|(account, permission)| { + permission.access_for(user).account.then_some(*account) + }) + .collect()) +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use magicblock_accounts_db::AccountsDbResult; + use solana_account::AccountSharedData; + + use super::*; + use crate::{ + types::{Member, RestrictedEntry}, + PERMISSION_PROGRAM_ID, + }; + + #[derive(Default)] + struct MockAccountsBank { + accounts: HashMap, + } + + impl MockAccountsBank { + fn insert_account_with_permission( + &mut self, + pubkey: Pubkey, + account: AccountSharedData, + permission: Permission, + ephemeral: bool, + ) { + self.accounts.insert(pubkey, account); + + let permission_data = permission.encode(ephemeral); + let mut permission_account = AccountSharedData::new( + 1000000, + permission_data.len(), + &PERMISSION_PROGRAM_ID, + ); + permission_account.set_data_from_slice(&permission_data); + self.accounts + .insert(Permission::pda(&pubkey), permission_account); + } + } + + impl AccountsBank for MockAccountsBank { + fn get_account(&self, pubkey: &Pubkey) -> Option { + self.accounts.get(pubkey).cloned() + } + + fn remove_account(&self, _pubkey: &Pubkey) {} + + fn remove_where( + &self, + _predicate: impl FnMut(&Pubkey, &AccountSharedData) -> bool, + ) -> AccountsDbResult { + Ok(0) + } + } + + #[test] + fn account_filter_preserves_shape() { + let allowed = Pubkey::new_unique(); + let denied = Pubkey::new_unique(); + let permission = PermissionEntry::Restricted(RestrictedEntry { + members: vec![Member { + pubkey: allowed, + flags: u8::MAX, + }], + are_program_restricted: true, + }); + + assert_eq!(filter_account(Some(42), &permission, &allowed), Some(42)); + assert_eq!(filter_account(Some(42), &permission, &denied), None); + } + + #[test] + fn visible_transaction_accounts_returns_only_visible_accounts_for_restricted_permission( + ) { + let mut accounts_db = MockAccountsBank::default(); + let accounts = vec![Pubkey::new_unique(), Pubkey::new_unique()]; + let user = Pubkey::new_unique(); + let denied_user = Pubkey::new_unique(); + accounts_db.insert_account_with_permission( + accounts[0], + AccountSharedData::new(1000000, 0, &Pubkey::default()), + Permission::new( + accounts[0], + Some(vec![Member { + pubkey: user, + flags: u8::MAX, + }]), + ), + false, + ); + + let visible_accounts = + visible_transaction_accounts(&accounts_db, &accounts, &user) + .unwrap(); + assert_eq!(visible_accounts, HashSet::from([accounts[0], accounts[1]])); + + let visible_accounts = + visible_transaction_accounts(&accounts_db, &accounts, &denied_user) + .unwrap(); + assert_eq!(visible_accounts, HashSet::from([accounts[1]])); + } + + #[test] + fn permission_for_account_is_unrestricted_when_no_permission_exists() { + let accounts_db = MockAccountsBank::default(); + let account = Pubkey::new_unique(); + + let permission = + permission_for_account(&accounts_db, &account).unwrap(); + + assert_eq!(permission, PermissionEntry::Unrestricted); + } + + #[test] + fn permission_for_account_decodes_restricted_permission() { + let mut accounts_db = MockAccountsBank::default(); + let account = Pubkey::new_unique(); + let member = Pubkey::new_unique(); + accounts_db.insert_account_with_permission( + account, + AccountSharedData::new(1000000, 0, &Pubkey::default()), + Permission::new( + account, + Some(vec![Member { + pubkey: member, + flags: u8::MAX, + }]), + ), + false, + ); + + let permission = + permission_for_account(&accounts_db, &account).unwrap(); + + assert!(permission.access_for(&member).account); + assert!(!permission.access_for(&Pubkey::new_unique()).account); + } + + #[test] + fn permission_for_account_treats_permission_accounts_as_public() { + // A permission account at `Permission::pda(counter)` is itself owned + // by the permission program. When someone queries that permission + // account directly, the filter layer must report it as public — the + // ACL data is never subject to ACL filtering. + let mut accounts_db = MockAccountsBank::default(); + let counter = Pubkey::new_unique(); + let member = Pubkey::new_unique(); + accounts_db.insert_account_with_permission( + counter, + AccountSharedData::new(1_000_000, 0, &Pubkey::default()), + Permission::new( + counter, + Some(vec![Member { + pubkey: member, + flags: u8::MAX, + }]), + ), + false, + ); + + let permission_pda = Permission::pda(&counter); + + // The data account (counter) is private to `member`. + let counter_permission = + permission_for_account(&accounts_db, &counter).unwrap(); + assert!(!counter_permission.access_for(&Pubkey::new_unique()).account); + + // The permission account itself is public — every caller sees it. + let perm_for_perm = + permission_for_account(&accounts_db, &permission_pda).unwrap(); + assert_eq!(perm_for_perm, PermissionEntry::Unrestricted); + assert!(perm_for_perm.access_for(&Pubkey::new_unique()).account); + } + + #[test] + fn permission_for_account_reports_decode_context() { + let mut accounts_db = MockAccountsBank::default(); + let account = Pubkey::new_unique(); + let permission_account = Permission::pda(&account); + let corrupt_data = vec![7, 8, 9]; + let mut permission = AccountSharedData::new( + 1000000, + corrupt_data.len(), + &PERMISSION_PROGRAM_ID, + ); + permission.set_data_from_slice(&corrupt_data); + accounts_db.accounts.insert(permission_account, permission); + + let err = permission_for_account(&accounts_db, &account).unwrap_err(); + + match err { + QueryFilteringError::Permission( + PermissionError::DataTooShort { len, min_len }, + ) => { + assert_eq!(len, 3); + assert_eq!(min_len, 34); + } + err => panic!("unexpected error: {err:?}"), + } + } + + #[test] + fn visible_transaction_accounts_returns_only_visible_accounts_for_unrestricted_permission( + ) { + let accounts_db = MockAccountsBank::default(); + let accounts = vec![Pubkey::new_unique(), Pubkey::new_unique()]; + let user = Pubkey::new_unique(); + let visible_accounts = + visible_transaction_accounts(&accounts_db, &accounts, &user) + .unwrap(); + assert_eq!(visible_accounts, HashSet::from([accounts[0], accounts[1]])); + } +} diff --git a/magicblock-query-filtering/src/transaction.rs b/magicblock-query-filtering/src/transaction.rs new file mode 100644 index 000000000..86e6ea1e0 --- /dev/null +++ b/magicblock-query-filtering/src/transaction.rs @@ -0,0 +1,567 @@ +use magicblock_accounts_db::traits::AccountsBank; +use solana_account_decoder_client_types::token::UiTokenAmount; +use solana_message::{ + compiled_instruction::CompiledInstruction, v0::Message as MessageV0, + Message as LegacyMessage, MessageHeader, VersionedMessage, +}; +use solana_pubkey::{pubkey, Pubkey}; +use solana_transaction::versioned::VersionedTransaction; +use solana_transaction_status::{ + ConfirmedTransactionWithStatusMeta, TransactionStatusMeta, + TransactionTokenBalance, TransactionWithStatusMeta, + VersionedTransactionWithStatusMeta, +}; + +use crate::{ + permissions_for_accounts, + service::{QueryFilteringError, QueryFilteringResult}, + types::Access, +}; + +const SYSTEM_PROGRAM_ID: Pubkey = pubkey!("11111111111111111111111111111111"); + +/// Programs whose instructions are always allowed, even when the accounts +/// they touch are restricted. +pub const WHITELISTED_PROGRAMS: &[Pubkey] = &[ + SYSTEM_PROGRAM_ID, + pubkey!("ComputeBudget111111111111111111111111111111"), + pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"), + pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"), + pubkey!("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"), + pubkey!("Ed25519SigVerify111111111111111111111111111"), + pubkey!("ACLseoPoyC3cBqoUtkbjZ4aDrkurZW86v19pXz2XQnp1"), + pubkey!("SPLxh1LVZzEkX99H6rqYizhytLWPZVV296zyYDPagv2"), + pubkey!("Magic11111111111111111111111111111111111111"), +]; + +/// Rejects a transaction submission when any non-whitelisted instruction +/// reads an account whose permission entry restricts the invoked program. +/// +/// The check intentionally ignores the calling user: program-level access is +/// controlled by listing the program pubkey as a member of the account's +/// permission entry. +pub fn check_transaction_admission( + accounts_db: &impl AccountsBank, + static_account_keys: &[Pubkey], + instructions: &[CompiledInstruction], +) -> QueryFilteringResult<()> { + let permissions = + permissions_for_accounts(accounts_db, static_account_keys)?; + + for ix in instructions { + let Some(program_id) = + static_account_keys.get(ix.program_id_index as usize) + else { + continue; + }; + if WHITELISTED_PROGRAMS.contains(program_id) { + continue; + } + for &account_index in &ix.accounts { + let Some(permission) = permissions.get(account_index as usize) + else { + continue; + }; + if permission.is_program_restricted(program_id) { + return Err(QueryFilteringError::AccessDenied); + } + } + } + + Ok(()) +} + +/// Convenience overload that extracts static keys and instructions from a +/// [`VersionedMessage`] before delegating to [`check_transaction_admission`]. +pub fn check_versioned_message_admission( + accounts_db: &impl AccountsBank, + message: &VersionedMessage, +) -> QueryFilteringResult<()> { + check_transaction_admission( + accounts_db, + message.static_account_keys(), + message.instructions(), + ) +} + +/// Mutates a confirmed transaction in place, redacting fields the caller +/// cannot see according to per-account permission flags. +/// +/// When any account in the transaction is invisible to the user, the +/// transaction body is reduced to its signatures and the meta is reduced to +/// its status. Otherwise, balances/logs/message are individually redacted +/// based on per-member access flags. +pub fn filter_confirmed_transaction( + accounts_db: &impl AccountsBank, + confirmed: &mut ConfirmedTransactionWithStatusMeta, + user: &Pubkey, +) -> QueryFilteringResult<()> { + match &mut confirmed.tx_with_meta { + TransactionWithStatusMeta::Complete(complete) => { + filter_complete_transaction(accounts_db, complete, user) + } + TransactionWithStatusMeta::MissingMetadata(legacy) => { + let permissions = permissions_for_accounts( + accounts_db, + &legacy.message.account_keys, + )?; + let any_invisible = permissions + .iter() + .any(|permission| !permission.access_for(user).account); + if any_invisible { + let blockhash = legacy.message.recent_blockhash; + legacy.message = LegacyMessage { + header: MessageHeader::default(), + account_keys: vec![], + recent_blockhash: blockhash, + instructions: vec![], + }; + } + Ok(()) + } + } +} + +fn filter_complete_transaction( + accounts_db: &impl AccountsBank, + complete: &mut VersionedTransactionWithStatusMeta, + user: &Pubkey, +) -> QueryFilteringResult<()> { + let mut all_keys: Vec = + complete.transaction.message.static_account_keys().to_vec(); + all_keys.extend(complete.meta.loaded_addresses.writable.iter().copied()); + all_keys.extend(complete.meta.loaded_addresses.readonly.iter().copied()); + + let permissions = permissions_for_accounts(accounts_db, &all_keys)?; + let accesses: Vec = permissions + .iter() + .map(|permission| permission.access_for(user)) + .collect(); + + if accesses.iter().any(|access| !access.account) { + redact_full(complete); + return Ok(()); + } + + if !accesses.iter().all(|access| access.balances) { + zero_balances(&mut complete.meta, &accesses); + } + if !accesses.iter().all(|access| access.logs) { + complete.meta.log_messages = Some(vec![]); + } + if !accesses.iter().all(|access| access.message) { + redact_message(&mut complete.transaction); + } + + Ok(()) +} + +fn redact_full(complete: &mut VersionedTransactionWithStatusMeta) { + redact_message(&mut complete.transaction); + complete.meta = TransactionStatusMeta { + status: complete.meta.status.clone(), + ..TransactionStatusMeta::default() + }; +} + +fn redact_message(transaction: &mut VersionedTransaction) { + let blockhash = *transaction.message.recent_blockhash(); + transaction.message = VersionedMessage::V0(MessageV0 { + header: MessageHeader::default(), + account_keys: vec![], + recent_blockhash: blockhash, + instructions: vec![], + address_table_lookups: vec![], + }); +} + +fn zero_balances(meta: &mut TransactionStatusMeta, accesses: &[Access]) { + for (balance, access) in meta.pre_balances.iter_mut().zip(accesses) { + if !access.balances { + *balance = 0; + } + } + for (balance, access) in meta.post_balances.iter_mut().zip(accesses) { + if !access.balances { + *balance = 0; + } + } + if let Some(balances) = meta.pre_token_balances.as_mut() { + zero_token_balances(balances, accesses); + } + if let Some(balances) = meta.post_token_balances.as_mut() { + zero_token_balances(balances, accesses); + } +} + +fn zero_token_balances( + balances: &mut [TransactionTokenBalance], + accesses: &[Access], +) { + for balance in balances.iter_mut() { + let Some(access) = accesses.get(balance.account_index as usize) else { + continue; + }; + if !access.balances { + balance.mint = SYSTEM_PROGRAM_ID.to_string(); + balance.owner = String::new(); + balance.program_id = String::new(); + balance.ui_token_amount = UiTokenAmount { + ui_amount: None, + decimals: 0, + amount: "0".to_string(), + ui_amount_string: "0.0".to_string(), + }; + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use magicblock_accounts_db::AccountsDbResult; + use solana_account::AccountSharedData; + use solana_hash::Hash; + use solana_message::{ + compiled_instruction::CompiledInstruction, MessageHeader, + }; + use solana_signature::Signature; + + use super::*; + use crate::{ + types::{Member, Permission}, + PERMISSION_PROGRAM_ID, + }; + + const FLAG_LOGS: u8 = 1 << 1; + const FLAG_BALANCES: u8 = 1 << 2; + const FLAG_MESSAGE: u8 = 1 << 3; + + #[derive(Default)] + struct MockAccountsBank { + accounts: HashMap, + } + + impl MockAccountsBank { + fn insert_permission(&mut self, account: Pubkey, members: Vec) { + let permission = Permission::new(account, Some(members)); + let encoded = permission.encode(false); + let mut data = AccountSharedData::new( + 1_000_000, + encoded.len(), + &PERMISSION_PROGRAM_ID, + ); + data.set_data_from_slice(&encoded); + self.accounts.insert(Permission::pda(&account), data); + } + } + + impl AccountsBank for MockAccountsBank { + fn get_account(&self, pubkey: &Pubkey) -> Option { + self.accounts.get(pubkey).cloned() + } + + fn remove_account(&self, _pubkey: &Pubkey) {} + + fn remove_where( + &self, + _predicate: impl FnMut(&Pubkey, &AccountSharedData) -> bool, + ) -> AccountsDbResult { + Ok(0) + } + } + + fn versioned_message( + keys: Vec, + instructions: Vec, + ) -> VersionedMessage { + VersionedMessage::V0(MessageV0 { + header: MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + account_keys: keys, + recent_blockhash: Hash::default(), + instructions, + address_table_lookups: vec![], + }) + } + + fn complete_tx( + keys: Vec, + meta: TransactionStatusMeta, + ) -> VersionedTransactionWithStatusMeta { + VersionedTransactionWithStatusMeta { + transaction: VersionedTransaction { + signatures: vec![Signature::default()], + message: versioned_message(keys, vec![]), + }, + meta, + } + } + + #[test] + fn admission_allows_whitelisted_program_on_restricted_account() { + let mut accounts = MockAccountsBank::default(); + let restricted = Pubkey::new_unique(); + accounts.insert_permission(restricted, vec![]); + + let message = versioned_message( + vec![SYSTEM_PROGRAM_ID, restricted], + vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![1], + data: vec![], + }], + ); + + assert!(check_versioned_message_admission(&accounts, &message).is_ok()); + } + + #[test] + fn admission_rejects_non_whitelisted_program_on_restricted_account() { + let mut accounts = MockAccountsBank::default(); + let restricted = Pubkey::new_unique(); + let custom_program = Pubkey::new_unique(); + accounts.insert_permission(restricted, vec![]); + + let message = versioned_message( + vec![custom_program, restricted], + vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![1], + data: vec![], + }], + ); + + assert!(matches!( + check_versioned_message_admission(&accounts, &message), + Err(QueryFilteringError::AccessDenied) + )); + } + + #[test] + fn admission_allows_program_listed_as_member() { + let mut accounts = MockAccountsBank::default(); + let restricted = Pubkey::new_unique(); + let custom_program = Pubkey::new_unique(); + accounts.insert_permission( + restricted, + vec![Member { + pubkey: custom_program, + flags: 0, + }], + ); + + let message = versioned_message( + vec![custom_program, restricted], + vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![1], + data: vec![], + }], + ); + + assert!(check_versioned_message_admission(&accounts, &message).is_ok()); + } + + #[test] + fn admission_allows_unrestricted_accounts() { + let accounts = MockAccountsBank::default(); + let custom_program = Pubkey::new_unique(); + let unrestricted = Pubkey::new_unique(); + + let message = versioned_message( + vec![custom_program, unrestricted], + vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![1], + data: vec![], + }], + ); + + assert!(check_versioned_message_admission(&accounts, &message).is_ok()); + } + + #[test] + fn filter_redacts_full_transaction_when_account_is_invisible() { + let mut accounts = MockAccountsBank::default(); + let restricted = Pubkey::new_unique(); + let denied = Pubkey::new_unique(); + accounts.insert_permission(restricted, vec![]); + + let original_blockhash = Hash::new_unique(); + let meta = TransactionStatusMeta { + fee: 5_000, + pre_balances: vec![1_000], + post_balances: vec![995], + log_messages: Some(vec!["secret".to_string()]), + ..TransactionStatusMeta::default() + }; + + let mut tx = complete_tx(vec![restricted], meta); + tx.transaction + .message + .set_recent_blockhash(original_blockhash); + + let mut confirmed = ConfirmedTransactionWithStatusMeta { + slot: 0, + block_time: None, + tx_with_meta: TransactionWithStatusMeta::Complete(tx), + }; + filter_confirmed_transaction(&accounts, &mut confirmed, &denied) + .unwrap(); + + let TransactionWithStatusMeta::Complete(filtered) = + confirmed.tx_with_meta + else { + panic!("expected complete tx"); + }; + assert!(filtered + .transaction + .message + .static_account_keys() + .is_empty()); + assert_eq!( + filtered.transaction.message.recent_blockhash(), + &original_blockhash + ); + assert_eq!(filtered.meta.fee, 0); + assert!(filtered.meta.pre_balances.is_empty()); + assert!(filtered.meta.log_messages.is_none()); + } + + #[test] + fn filter_preserves_full_transaction_when_user_has_all_access() { + let mut accounts = MockAccountsBank::default(); + let restricted = Pubkey::new_unique(); + let allowed = Pubkey::new_unique(); + accounts.insert_permission( + restricted, + vec![Member { + pubkey: allowed, + flags: u8::MAX, + }], + ); + + let meta = TransactionStatusMeta { + pre_balances: vec![1_000], + post_balances: vec![995], + log_messages: Some(vec!["allowed".to_string()]), + ..TransactionStatusMeta::default() + }; + + let mut confirmed = ConfirmedTransactionWithStatusMeta { + slot: 0, + block_time: None, + tx_with_meta: TransactionWithStatusMeta::Complete(complete_tx( + vec![restricted], + meta, + )), + }; + filter_confirmed_transaction(&accounts, &mut confirmed, &allowed) + .unwrap(); + + let TransactionWithStatusMeta::Complete(filtered) = + confirmed.tx_with_meta + else { + panic!("expected complete tx"); + }; + assert_eq!( + filtered.transaction.message.static_account_keys(), + &[restricted] + ); + assert_eq!(filtered.meta.pre_balances, vec![1_000]); + assert_eq!( + filtered.meta.log_messages, + Some(vec!["allowed".to_string()]) + ); + } + + #[test] + fn filter_redacts_only_logs_when_only_logs_flag_missing() { + let mut accounts = MockAccountsBank::default(); + let restricted = Pubkey::new_unique(); + let viewer = Pubkey::new_unique(); + accounts.insert_permission( + restricted, + vec![Member { + pubkey: viewer, + flags: FLAG_BALANCES | FLAG_MESSAGE, + }], + ); + + let meta = TransactionStatusMeta { + pre_balances: vec![1_000], + post_balances: vec![1_000], + log_messages: Some(vec!["secret".to_string()]), + ..TransactionStatusMeta::default() + }; + + let mut confirmed = ConfirmedTransactionWithStatusMeta { + slot: 0, + block_time: None, + tx_with_meta: TransactionWithStatusMeta::Complete(complete_tx( + vec![restricted], + meta, + )), + }; + filter_confirmed_transaction(&accounts, &mut confirmed, &viewer) + .unwrap(); + + let TransactionWithStatusMeta::Complete(filtered) = + confirmed.tx_with_meta + else { + panic!("expected complete tx"); + }; + assert_eq!(filtered.meta.pre_balances, vec![1_000]); + assert_eq!(filtered.meta.log_messages, Some(vec![])); + assert_eq!( + filtered.transaction.message.static_account_keys(), + &[restricted] + ); + } + + #[test] + fn filter_zeros_balances_when_balance_flag_missing() { + let mut accounts = MockAccountsBank::default(); + let restricted = Pubkey::new_unique(); + let viewer = Pubkey::new_unique(); + accounts.insert_permission( + restricted, + vec![Member { + pubkey: viewer, + flags: FLAG_LOGS | FLAG_MESSAGE, + }], + ); + + let meta = TransactionStatusMeta { + pre_balances: vec![1_000], + post_balances: vec![995], + log_messages: Some(vec!["ok".to_string()]), + ..TransactionStatusMeta::default() + }; + + let mut confirmed = ConfirmedTransactionWithStatusMeta { + slot: 0, + block_time: None, + tx_with_meta: TransactionWithStatusMeta::Complete(complete_tx( + vec![restricted], + meta, + )), + }; + filter_confirmed_transaction(&accounts, &mut confirmed, &viewer) + .unwrap(); + + let TransactionWithStatusMeta::Complete(filtered) = + confirmed.tx_with_meta + else { + panic!("expected complete tx"); + }; + assert_eq!(filtered.meta.pre_balances, vec![0]); + assert_eq!(filtered.meta.post_balances, vec![0]); + assert_eq!(filtered.meta.log_messages, Some(vec!["ok".to_string()])); + } +} diff --git a/magicblock-query-filtering/src/types.rs b/magicblock-query-filtering/src/types.rs new file mode 100644 index 000000000..a3a59c396 --- /dev/null +++ b/magicblock-query-filtering/src/types.rs @@ -0,0 +1,505 @@ +use serde::{Deserialize, Serialize}; +use solana_pubkey::Pubkey; +use thiserror::Error; + +use crate::PERMISSION_PROGRAM_ID; + +const PERMISSION_PREFIX: &[u8] = b"permission:"; +const PERMISSION_HEADER_LEN: usize = 34; +const MEMBER_COUNT_LEN: usize = 4; + +const MEMBER_FLAG_TX_LOGS: u8 = 1 << 1; +const MEMBER_FLAG_TX_BALANCES: u8 = 1 << 2; +const MEMBER_FLAG_TX_MESSAGE: u8 = 1 << 3; +const MEMBER_FLAG_ACCOUNT_SIGNATURE: u8 = 1 << 4; + +pub type PermissionResult = Result; + +#[derive(Debug, Error)] +pub enum PermissionError { + #[error( + "permission data too short: len={len}, expected at least {min_len}" + )] + DataTooShort { len: usize, min_len: usize }, + #[error("invalid permission option tag: {tag}")] + InvalidOptionTag { tag: u8 }, + #[error("invalid permission member bytes: len={len}")] + InvalidMemberBytes { len: usize }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Member { + pub flags: u8, + pub pubkey: Pubkey, +} + +impl Member { + pub const SIZE: usize = 33; + + pub fn can_see_tx_logs(&self) -> bool { + self.flags & MEMBER_FLAG_TX_LOGS != 0 + } + + pub fn can_see_tx_balances(&self) -> bool { + self.flags & MEMBER_FLAG_TX_BALANCES != 0 + } + + pub fn can_see_tx_message(&self) -> bool { + self.flags & MEMBER_FLAG_TX_MESSAGE != 0 + } + + pub fn can_see_account_signature(&self) -> bool { + self.flags & MEMBER_FLAG_ACCOUNT_SIGNATURE != 0 + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Permission { + discriminator: u8, + bump: u8, + permissioned_account: Pubkey, + members: Option>, +} + +impl Permission { + pub fn pda(account: &Pubkey) -> Pubkey { + Pubkey::find_program_address( + &[PERMISSION_PREFIX, account.as_ref()], + &PERMISSION_PROGRAM_ID, + ) + .0 + } + + pub fn decode(data: &[u8]) -> PermissionResult { + if data.len() < PERMISSION_HEADER_LEN { + return Err(PermissionError::DataTooShort { + len: data.len(), + min_len: PERMISSION_HEADER_LEN, + }); + } + + let discriminator = data[0]; + let bump = data[1]; + let permissioned_account = Pubkey::new_from_array( + data[2..PERMISSION_HEADER_LEN].try_into().map_err(|_| { + PermissionError::DataTooShort { + len: data.len(), + min_len: PERMISSION_HEADER_LEN, + } + })?, + ); + let members = + Self::decode_members_payload(&data[PERMISSION_HEADER_LEN..])?; + + Ok(Permission { + discriminator, + bump, + permissioned_account, + members, + }) + } + + fn decode_members_payload( + payload: &[u8], + ) -> PermissionResult>> { + if let Some(members) = Self::decode_counted_members(payload) { + let members = members?; + return Ok((!members.is_empty()).then_some(members)); + } + + let option_tag = + *payload.first().ok_or(PermissionError::DataTooShort { + len: PERMISSION_HEADER_LEN, + min_len: PERMISSION_HEADER_LEN + 1, + })?; + match option_tag { + 0 => Ok(None), + 1 => Ok(Some(Self::decode_private_members(&payload[1..])?)), + _ => Err(PermissionError::InvalidOptionTag { tag: option_tag }), + } + } + + fn decode_private_members(payload: &[u8]) -> PermissionResult> { + if let Some(members) = Self::decode_counted_members(payload) { + return members; + } + + // Ephemeral permission accounts omit the member count and may include + // account padding after the serialized members. + let members_len = payload.len() - (payload.len() % Member::SIZE); + Self::decode_members(&payload[..members_len]) + } + + fn decode_counted_members( + payload: &[u8], + ) -> Option>> { + let len_bytes: [u8; MEMBER_COUNT_LEN] = + payload.get(..MEMBER_COUNT_LEN)?.try_into().ok()?; + let count = u32::from_le_bytes(len_bytes) as usize; + let members_len = count.checked_mul(Member::SIZE)?; + let members_end = MEMBER_COUNT_LEN.checked_add(members_len)?; + let members_data = payload.get(MEMBER_COUNT_LEN..members_end)?; + let trailing_padding = payload + .get(members_end..) + .map(|tail| tail.iter().all(|byte| *byte == 0)) + .unwrap_or(true); + + trailing_padding.then(|| Self::decode_members(members_data)) + } + + fn decode_members(members_data: &[u8]) -> PermissionResult> { + if !members_data.len().is_multiple_of(Member::SIZE) { + return Err(PermissionError::InvalidMemberBytes { + len: members_data.len(), + }); + } + + members_data + .chunks_exact(Member::SIZE) + .map(|chunk| { + let pubkey = Pubkey::new_from_array( + chunk[1..Member::SIZE].try_into().map_err(|_| { + PermissionError::InvalidMemberBytes { len: chunk.len() } + })?, + ); + Ok(Member { + flags: chunk[0], + pubkey, + }) + }) + .collect() + } + + #[cfg(test)] + pub fn new( + permissioned_account: Pubkey, + members: Option>, + ) -> Self { + Self { + discriminator: 0, + bump: 255, + permissioned_account, + members, + } + } + + #[cfg(test)] + pub fn encode(&self, ephemeral: bool) -> Vec { + let mut data = Vec::new(); + data.push(0); // discriminator + data.push(255); // bump + data.extend_from_slice(self.permissioned_account.as_ref()); // permissioned account + match &self.members { + Some(members) => { + data.push(1); + if !ephemeral { + data.extend_from_slice( + &(members.len() as u32).to_le_bytes(), + ); + } + for member in members { + data.push(member.flags); + data.extend_from_slice(member.pubkey.as_ref()); + } + } + None => data.push(0), + } + data + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RestrictedEntry { + pub members: Vec, + pub are_program_restricted: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Access { + pub account: bool, + pub logs: bool, + pub message: bool, + pub balances: bool, + pub signatures: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PermissionEntry { + Unrestricted, + Restricted(RestrictedEntry), +} + +impl PermissionEntry { + pub fn from_permission(permission: Permission) -> Self { + permission + .members + .map(|members| { + Self::Restricted(RestrictedEntry { + members, + are_program_restricted: true, + }) + }) + .unwrap_or(Self::Unrestricted) + } + + pub fn access_for(&self, user: &Pubkey) -> Access { + match self { + Self::Unrestricted => Access { + account: true, + logs: true, + message: true, + balances: true, + signatures: true, + }, + Self::Restricted(entry) => entry + .members + .iter() + .find(|member| member.pubkey == *user) + .map(|member| Access { + account: true, + logs: member.can_see_tx_logs(), + message: member.can_see_tx_message(), + balances: member.can_see_tx_balances(), + signatures: member.can_see_account_signature(), + }) + .unwrap_or(Access { + account: false, + logs: false, + message: false, + balances: false, + signatures: false, + }), + } + } + + pub fn is_program_restricted(&self, program: &Pubkey) -> bool { + match self { + Self::Unrestricted => false, + Self::Restricted(entry) => { + entry.are_program_restricted + && !entry + .members + .iter() + .any(|member| member.pubkey == *program) + } + } + } +} + +// ------------------------------ +// Responses +// ------------------------------ + +#[derive(Debug, Serialize, Deserialize)] +pub struct ChallengeResponse { + pub challenge: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LoginRequest { + pub pubkey: String, + pub signature: String, + pub challenge: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct LoginResponse { + pub token: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct QuoteResponse { + pub quote: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FastQuoteResponse { + pub quote: String, + pub pubkey: String, + pub challenge: String, + pub signature: String, + pub report_data_sha256: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn permission_is_encoded_correctly() { + let original_permissions = vec![ + Permission::new(Pubkey::new_unique(), None), + Permission::new( + Pubkey::new_unique(), + Some(vec![Member { + pubkey: Pubkey::new_unique(), + flags: u8::MAX, + }]), + ), + Permission::new( + Pubkey::new_unique(), + Some(vec![ + Member { + pubkey: Pubkey::new_unique(), + flags: u8::MAX, + }; + 10 + ]), + ), + Permission::new( + Pubkey::new_unique(), + Some(vec![Member { + pubkey: Pubkey::new_unique(), + flags: u8::MAX, + }]), + ), + ]; + + for ephemeral in [true, false] { + for original_permission in &original_permissions { + let permission_data = original_permission.encode(ephemeral); + let permission = Permission::decode(&permission_data).unwrap(); + assert_eq!(permission, *original_permission); + } + } + } + + #[test] + fn permission_entry_is_encoded_correctly() { + let original_permission_entries = vec![ + ( + Permission::new(Pubkey::new_unique(), None), + PermissionEntry::Unrestricted, + ), + { + let permissioned_account = Pubkey::new_unique(); + let member_pk = Pubkey::new_unique(); + ( + Permission::new( + permissioned_account, + Some(vec![Member { + pubkey: member_pk, + flags: u8::MAX, + }]), + ), + PermissionEntry::Restricted(RestrictedEntry { + members: vec![Member { + pubkey: member_pk, + flags: u8::MAX, + }], + are_program_restricted: true, + }), + ) + }, + ]; + + for (original_permission, original_permission_entry) in + original_permission_entries + { + let permission_entry = + PermissionEntry::from_permission(original_permission); + assert_eq!(permission_entry, original_permission_entry); + } + } + + #[test] + fn missing_permission_is_unrestricted() { + let account = Pubkey::new_unique(); + + let permission = PermissionEntry::from_permission( + Permission::decode(&Permission::new(account, None).encode(false)) + .unwrap(), + ); + + assert_eq!(permission, PermissionEntry::Unrestricted); + } + + #[test] + fn restricted_permission_controls_access() { + let account = Pubkey::new_unique(); + let allowed = Pubkey::new_unique(); + let denied = Pubkey::new_unique(); + + let permission_data = Permission { + discriminator: 0, + bump: 255, + permissioned_account: account, + members: Some(vec![Member { + pubkey: allowed, + flags: u8::MAX, + }]), + } + .encode(false); + + let permission = PermissionEntry::from_permission( + Permission::decode(&permission_data).unwrap(), + ); + + assert!(permission.access_for(&allowed).account); + assert!(!permission.access_for(&denied).account); + } + + #[test] + fn padded_ephemeral_permission_controls_access() { + let account = Pubkey::new_unique(); + let default_member = Pubkey::new_unique(); + let allowed = Pubkey::new_unique(); + let denied = Pubkey::new_unique(); + + let mut permission_data = Permission { + discriminator: 0, + bump: 255, + permissioned_account: account, + members: Some(vec![ + Member { + pubkey: default_member, + flags: 0, + }, + Member { + pubkey: allowed, + flags: u8::MAX, + }, + ]), + } + .encode(true); + permission_data.extend_from_slice(&[0; 7]); + + let permission = PermissionEntry::from_permission( + Permission::decode(&permission_data).unwrap(), + ); + + assert!(permission.access_for(&allowed).account); + assert!(!permission.access_for(&denied).account); + } + + #[test] + fn count_prefixed_permission_controls_access() { + let account = Pubkey::new_unique(); + let allowed = Pubkey::new_unique(); + let denied = Pubkey::new_unique(); + let members = [Member { + pubkey: allowed, + flags: u8::MAX, + }]; + + let mut permission_data = Vec::new(); + permission_data.push(0); + permission_data.push(255); + permission_data.extend_from_slice(account.as_ref()); + permission_data + .extend_from_slice(&(members.len() as u32).to_le_bytes()); + for member in members { + permission_data.push(member.flags); + permission_data.extend_from_slice(member.pubkey.as_ref()); + } + + let permission = PermissionEntry::from_permission( + Permission::decode(&permission_data).unwrap(), + ); + + assert!(permission.access_for(&allowed).account); + assert!(!permission.access_for(&denied).account); + } +} diff --git a/magicblock-table-mania/src/derive_keypair.rs b/magicblock-table-mania/src/derive_keypair.rs index 1b2ae83a1..9ac8967cf 100644 --- a/magicblock-table-mania/src/derive_keypair.rs +++ b/magicblock-table-mania/src/derive_keypair.rs @@ -29,9 +29,9 @@ fn derive_insecure(message: &[u8]) -> Keypair { let hash = ::digest(message); let seed = &hash.as_slice()[0..32]; - let secret = SecretKey::from_bytes(seed).unwrap(); + let secret: SecretKey = seed.try_into().unwrap(); let mut secret_bytes = [0u8; Keypair::SECRET_KEY_LENGTH]; - secret_bytes.copy_from_slice(secret.as_bytes()); + secret_bytes.copy_from_slice(&secret); Keypair::new_from_array(secret_bytes) } diff --git a/magicblock-validator/Cargo.toml b/magicblock-validator/Cargo.toml index 12ecc4692..0dc14cd56 100644 --- a/magicblock-validator/Cargo.toml +++ b/magicblock-validator/Cargo.toml @@ -22,5 +22,13 @@ tokio = { workspace = true, features = ["rt-multi-thread"] } [features] default = [] -tokio-console = ["console-subscriber", "tokio/tracing", "magicblock-core/tokio-console"] +# Permission-aware query filtering +query-filtering = ["magicblock-api/query-filtering"] +# Full TEE integration: query filtering + TDX attestation quote. +tee = ["magicblock-api/tee", "query-filtering"] +tokio-console = [ + "console-subscriber", + "tokio/tracing", + "magicblock-core/tokio-console", +] tui = ["magicblock-tui-client"] diff --git a/test-integration/Cargo.lock b/test-integration/Cargo.lock index 9530e9f06..684452651 100644 --- a/test-integration/Cargo.lock +++ b/test-integration/Cargo.lock @@ -76,7 +76,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6200f3b8cfbe5992fde00d443f60e62a79d2d8f6a658af1ffb7c4f0baa3c7028" dependencies = [ "ahash 0.8.12", - "solana-epoch-schedule", + "solana-epoch-schedule 3.1.0", "solana-hash 3.1.0", "solana-pubkey 3.0.0", "solana-sha256-hasher 3.1.0", @@ -104,7 +104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d93caf9e6dd35ba4193fe778c1e52ee69433ba53b9eaeebc00c7bcd4d699081" dependencies = [ "log", - "solana-clock", + "solana-clock 3.0.1", "solana-hash 3.1.0", "solana-signature", "solana-transaction", @@ -151,10 +151,10 @@ dependencies = [ "openssl", "sha3", "solana-ed25519-program", - "solana-message", + "solana-message 3.1.0", "solana-precompile-error", "solana-pubkey 3.0.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-secp256k1-program", "solana-secp256r1-program", ] @@ -167,7 +167,7 @@ checksum = "2c3998a6ec388df954d8f78eeaf73ff487b50a8bdaebd48c8573af22c7b6db72" dependencies = [ "agave-feature-set", "solana-pubkey 3.0.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", ] [[package]] @@ -186,7 +186,7 @@ dependencies = [ "regex", "semver", "solana-accounts-db", - "solana-clock", + "solana-clock 3.0.1", "solana-genesis-config", "solana-hash 3.1.0", "solana-lattice-hash", @@ -209,36 +209,36 @@ dependencies = [ "bincode", "libsecp256k1", "num-traits", - "solana-account", - "solana-account-info", - "solana-big-mod-exp", - "solana-blake3-hasher", + "solana-account 3.4.0", + "solana-account-info 3.1.1", + "solana-big-mod-exp 3.0.0", + "solana-blake3-hasher 3.1.0", "solana-bn254", - "solana-clock", - "solana-cpi", + "solana-clock 3.0.1", + "solana-cpi 3.1.0", "solana-curve25519", "solana-hash 3.1.0", - "solana-instruction", - "solana-keccak-hasher", - "solana-loader-v3-interface", + "solana-instruction 3.4.0", + "solana-keccak-hasher 3.1.0", + "solana-loader-v3-interface 6.1.1", "solana-poseidon", - "solana-program-entrypoint", + "solana-program-entrypoint 3.1.1", "solana-program-runtime", "solana-pubkey 3.0.0", "solana-sbpf", - "solana-sdk-ids", - "solana-secp256k1-recover", + "solana-sdk-ids 3.1.0", + "solana-secp256k1-recover 3.1.1", "solana-sha256-hasher 3.1.0", - "solana-stable-layout", - "solana-stake-interface", + "solana-stable-layout 3.0.1", + "solana-stake-interface 2.0.2", "solana-svm-callback", "solana-svm-feature-set", "solana-svm-log-collector", "solana-svm-measure", "solana-svm-timings", "solana-svm-type-overrides", - "solana-sysvar", - "solana-sysvar-id", + "solana-sysvar 3.1.1", + "solana-sysvar-id 3.1.0", "solana-transaction-context", "thiserror 2.0.18", ] @@ -250,11 +250,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9b36ce7792c4e48140ee2f9ef4eaa289fab1c383a0670588a6c7c755947608c" dependencies = [ "solana-hash 3.1.0", - "solana-message", + "solana-message 3.1.0", "solana-packet", "solana-pubkey 3.0.0", - "solana-sdk-ids", - "solana-short-vec", + "solana-sdk-ids 3.1.0", + "solana-short-vec 3.2.0", "solana-signature", "solana-svm-transaction", "solana-transaction-context", @@ -269,7 +269,7 @@ dependencies = [ "agave-logger", "serde", "solana-bls-signatures", - "solana-clock", + "solana-clock 3.0.1", "solana-hash 3.1.0", ] @@ -396,7 +396,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -407,7 +407,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1200,16 +1200,39 @@ dependencies = [ "subtle", ] +[[package]] +name = "borsh" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115e54d64eb62cdebad391c19efc9dce4981c690c85a33a12199d99bb9546fee" +dependencies = [ + "borsh-derive 0.10.4", + "hashbrown 0.12.3", +] + [[package]] name = "borsh" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" dependencies = [ - "borsh-derive", + "borsh-derive 1.6.0", "cfg_aliases", ] +[[package]] +name = "borsh-derive" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831213f80d9423998dd696e2c5345aba6be7a0bd8cd19e31c5243e13df1cef89" +dependencies = [ + "borsh-derive-internal", + "borsh-schema-derive-internal", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn 1.0.109", +] + [[package]] name = "borsh-derive" version = "1.6.0" @@ -1217,12 +1240,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" dependencies = [ "once_cell", - "proc-macro-crate", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", "syn 2.0.117", ] +[[package]] +name = "borsh-derive-internal" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65d6ba50644c98714aa2a70d13d7df3cd75cd2b523a2b452bf010443800976b3" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276691d96f063427be83e6692b86148e488ebba9f48f77788724ca027ba3b6d4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "brotli" version = "8.0.2" @@ -1579,6 +1624,26 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "console_log" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89f72f65e8501878b8a004d5a1afb780987e2ce2b4532c562e367a72c57499f" +dependencies = [ + "log", + "web-sys", +] + [[package]] name = "const-crypto" version = "0.3.0" @@ -2289,35 +2354,44 @@ dependencies = [ [[package]] name = "ephemeral-rollups-sdk" -version = "0.11.0" -source = "git+https://github.com/magicblock-labs/ephemeral-rollups-sdk.git?rev=ccfc9f924dc40#ccfc9f924dc403e6ec93780e575fa655af1bebb5" +version = "0.14.3" +source = "git+https://github.com/magicblock-labs/ephemeral-rollups-sdk.git?tag=v0.14.3#68c2d142673959912506da54387b28fccfb38657" dependencies = [ "base64ct", "bincode", - "borsh", + "bytemuck", "ephemeral-rollups-sdk-attribute-action", "ephemeral-rollups-sdk-attribute-commit", "ephemeral-rollups-sdk-attribute-delegate", "ephemeral-rollups-sdk-attribute-ephemeral", "ephemeral-rollups-sdk-attribute-ephemeral-accounts", + "five8 0.2.1", "getrandom 0.2.16", - "magicblock-delegation-program-api 0.3.0 (git+https://github.com/magicblock-labs/delegation-program.git?branch=snawaz%2Fupgrade)", - "magicblock-magic-program-api 0.9.0", - "solana-account", - "solana-account-info", - "solana-cpi", - "solana-instruction", - "solana-program-error", - "solana-program-memory", + "magicblock-delegation-program-api 3.0.0", + "magicblock-magic-program-api 0.10.1", + "num-derive", + "num-traits", + "solana-account 3.4.0", + "solana-account-info 2.3.0", + "solana-account-info 3.1.1", + "solana-address 2.6.0", + "solana-cpi 3.1.0", + "solana-instruction 3.4.0", + "solana-program 2.3.0", + "solana-program 3.0.0", + "solana-program-error 2.2.2", + "solana-program-error 3.0.0", + "solana-program-memory 3.1.0", "solana-pubkey 3.0.0", - "solana-system-interface 3.2.0", - "solana-sysvar", + "solana-system-interface 2.0.0", + "solana-sysvar 3.1.1", + "thiserror 1.0.69", ] [[package]] name = "ephemeral-rollups-sdk-attribute-action" -version = "0.11.0" -source = "git+https://github.com/magicblock-labs/ephemeral-rollups-sdk.git?rev=ccfc9f924dc40#ccfc9f924dc403e6ec93780e575fa655af1bebb5" +version = "0.14.3" +source = "git+https://github.com/magicblock-labs/ephemeral-rollups-sdk.git?tag=v0.14.3#68c2d142673959912506da54387b28fccfb38657" dependencies = [ "quote", "syn 1.0.109", @@ -2325,17 +2399,18 @@ dependencies = [ [[package]] name = "ephemeral-rollups-sdk-attribute-commit" -version = "0.11.0" -source = "git+https://github.com/magicblock-labs/ephemeral-rollups-sdk.git?rev=ccfc9f924dc40#ccfc9f924dc403e6ec93780e575fa655af1bebb5" +version = "0.14.3" +source = "git+https://github.com/magicblock-labs/ephemeral-rollups-sdk.git?tag=v0.14.3#68c2d142673959912506da54387b28fccfb38657" dependencies = [ + "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "ephemeral-rollups-sdk-attribute-delegate" -version = "0.11.0" -source = "git+https://github.com/magicblock-labs/ephemeral-rollups-sdk.git?rev=ccfc9f924dc40#ccfc9f924dc403e6ec93780e575fa655af1bebb5" +version = "0.14.3" +source = "git+https://github.com/magicblock-labs/ephemeral-rollups-sdk.git?tag=v0.14.3#68c2d142673959912506da54387b28fccfb38657" dependencies = [ "proc-macro2", "quote", @@ -2344,8 +2419,8 @@ dependencies = [ [[package]] name = "ephemeral-rollups-sdk-attribute-ephemeral" -version = "0.11.0" -source = "git+https://github.com/magicblock-labs/ephemeral-rollups-sdk.git?rev=ccfc9f924dc40#ccfc9f924dc403e6ec93780e575fa655af1bebb5" +version = "0.14.3" +source = "git+https://github.com/magicblock-labs/ephemeral-rollups-sdk.git?tag=v0.14.3#68c2d142673959912506da54387b28fccfb38657" dependencies = [ "proc-macro2", "quote", @@ -2354,8 +2429,8 @@ dependencies = [ [[package]] name = "ephemeral-rollups-sdk-attribute-ephemeral-accounts" -version = "0.11.0" -source = "git+https://github.com/magicblock-labs/ephemeral-rollups-sdk.git?rev=ccfc9f924dc40#ccfc9f924dc403e6ec93780e575fa655af1bebb5" +version = "0.14.3" +source = "git+https://github.com/magicblock-labs/ephemeral-rollups-sdk.git?tag=v0.14.3#68c2d142673959912506da54387b28fccfb38657" dependencies = [ "proc-macro2", "quote", @@ -2504,7 +2579,7 @@ dependencies = [ "atomic", "pear", "serde", - "toml", + "toml 0.8.23", "uncased", "version_check", ] @@ -2918,7 +2993,7 @@ dependencies = [ "bincode", "magicblock-magic-program-api 0.11.3", "serde", - "solana-program", + "solana-program 3.0.0", ] [[package]] @@ -3584,10 +3659,10 @@ name = "integration-test-tools" version = "0.0.0" dependencies = [ "anyhow", - "borsh", + "borsh 1.6.0", "color-backtrace", "magicblock-config", - "magicblock-delegation-program-api 0.3.0 (git+https://github.com/magicblock-labs/delegation-program.git?rev=25386a7c1d406d06b8d07a4d5b0fd37d5e74213b)", + "magicblock-delegation-program-api 0.3.0", "rand 0.8.5", "rayon", "serde", @@ -3599,7 +3674,7 @@ dependencies = [ "solana-system-interface 3.2.0", "solana-transaction-status", "tempfile", - "toml", + "toml 0.8.23", "tracing", "ureq 2.12.1", "url", @@ -3762,6 +3837,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem 3.0.6", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "k256" version = "0.13.4" @@ -3836,16 +3926,16 @@ dependencies = [ "prost 0.14.3", "prost-types 0.14.3", "protobuf-src", - "solana-account", + "solana-account 3.4.0", "solana-account-decoder", - "solana-clock", + "solana-clock 3.0.1", "solana-hash 3.1.0", - "solana-message", + "solana-message 3.1.0", "solana-pubkey 3.0.0", "solana-signature", "solana-transaction", "solana-transaction-context", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "solana-transaction-status", "tonic 0.14.5", "tonic-build 0.14.5", @@ -4169,9 +4259,9 @@ name = "magic-domain-program" version = "0.3.0" source = "git+https://github.com/magicblock-labs/magic-domain-program.git?rev=335a22#335a22ba5aa7b8c4bc84d5053444c74c3b05cdac" dependencies = [ - "borsh", + "borsh 1.6.0", "bytemuck_derive", - "solana-program", + "solana-program 3.0.0", "solana-system-interface 3.2.0", ] @@ -4191,16 +4281,16 @@ dependencies = [ "magicblock-program", "magicblock-rpc-client", "rand 0.9.2", - "solana-account", + "solana-account 3.4.0", "solana-hash 3.1.0", - "solana-instruction", - "solana-loader-v3-interface", - "solana-loader-v4-interface", + "solana-instruction 3.4.0", + "solana-loader-v3-interface 6.1.1", + "solana-loader-v4-interface 3.1.0", "solana-pubkey 3.0.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-signature", "solana-signer", - "solana-sysvar", + "solana-sysvar 3.1.1", "solana-transaction", "thiserror 2.0.18", "tokio", @@ -4222,7 +4312,7 @@ dependencies = [ "solana-hash 3.1.0", "solana-pubkey 3.0.0", "solana-transaction", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "thiserror 2.0.18", "tokio", "tokio-util 0.7.17", @@ -4240,7 +4330,7 @@ dependencies = [ "memmap2 0.9.9", "parking_lot", "reflink-copy", - "solana-account", + "solana-account 3.4.0", "solana-pubkey 3.0.0", "tar", "thiserror 2.0.18", @@ -4291,23 +4381,25 @@ dependencies = [ "parking_lot", "scc", "serde", - "solana-account", + "serde_json", + "solana-account 3.4.0", "solana-account-decoder", "solana-fee-structure", "solana-keypair", - "solana-message", + "solana-message 3.1.0", "solana-pubkey 3.0.0", "solana-rpc-client-api", "solana-signature", "solana-system-transaction", "solana-transaction", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "solana-transaction-status", "sonic-rs", "thiserror 2.0.18", "tokio", "tokio-util 0.7.17", "tracing", + "url", ] [[package]] @@ -4316,18 +4408,19 @@ version = "0.11.3" dependencies = [ "agave-feature-set", "anyhow", - "borsh", + "borsh 1.6.0", "fd-lock", "magic-domain-program", "magicblock-account-cloner", "magicblock-accounts", "magicblock-accounts-db", + "magicblock-aml", "magicblock-aperture", "magicblock-chainlink", "magicblock-committor-service", "magicblock-config", "magicblock-core", - "magicblock-delegation-program-api 0.3.0 (git+https://github.com/magicblock-labs/delegation-program.git?rev=25386a7c1d406d06b8d07a4d5b0fd37d5e74213b)", + "magicblock-delegation-program-api 0.3.0", "magicblock-ledger", "magicblock-magic-program-api 0.11.3", "magicblock-metrics", @@ -4339,32 +4432,32 @@ dependencies = [ "magicblock-validator-admin", "num_cpus", "paste", - "solana-account", - "solana-clock", + "solana-account 3.4.0", + "solana-clock 3.0.1", "solana-cluster-type 3.1.0", "solana-commitment-config", - "solana-feature-gate-interface", - "solana-fee-calculator", + "solana-feature-gate-interface 3.1.0", + "solana-fee-calculator 3.2.0", "solana-genesis-config", "solana-hash 3.1.0", - "solana-instruction", + "solana-instruction 3.4.0", "solana-keypair", - "solana-message", - "solana-native-token", - "solana-program", - "solana-program-option", - "solana-program-pack", + "solana-message 3.1.0", + "solana-native-token 3.0.0", + "solana-program 3.0.0", + "solana-program-option 3.1.0", + "solana-program-pack 3.1.0", "solana-pubkey 3.0.0", "solana-rent 3.1.0", "solana-rpc-client", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-sha256-hasher 3.1.0", "solana-signature", "solana-signer", "solana-system-program", - "solana-sysvar", + "solana-sysvar 3.1.1", "solana-transaction", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "spl-token-interface", "thiserror 2.0.18", "tokio", @@ -4380,7 +4473,7 @@ dependencies = [ "arc-swap", "async-trait", "bincode", - "borsh", + "borsh 1.6.0", "futures-util", "helius-laserstream", "lru 0.16.2", @@ -4388,35 +4481,35 @@ dependencies = [ "magicblock-aml", "magicblock-config", "magicblock-core", - "magicblock-delegation-program-api 0.3.0 (git+https://github.com/magicblock-labs/delegation-program.git?rev=25386a7c1d406d06b8d07a4d5b0fd37d5e74213b)", + "magicblock-delegation-program-api 0.3.0", "magicblock-magic-program-api 0.11.3", "magicblock-metrics", "parking_lot", "scc", - "solana-account", + "solana-account 3.4.0", "solana-account-decoder", "solana-account-decoder-client-types", - "solana-address-lookup-table-interface", - "solana-clock", + "solana-address-lookup-table-interface 3.1.0", + "solana-clock 3.0.1", "solana-commitment-config", "solana-hash 3.1.0", - "solana-instruction", + "solana-instruction 3.4.0", "solana-keypair", - "solana-loader-v3-interface", - "solana-loader-v4-interface", - "solana-message", - "solana-program", + "solana-loader-v3-interface 6.1.1", + "solana-loader-v4-interface 3.1.0", + "solana-message 3.1.0", + "solana-program 3.0.0", "solana-pubkey 3.0.0", "solana-pubsub-client", "solana-rent 3.1.0", "solana-rpc-client", "solana-rpc-client-api", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-signature", "solana-signer", "solana-system-interface 3.2.0", "solana-transaction", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "spl-token-2022-interface", "spl-token-interface", "thiserror 2.0.18", @@ -4432,13 +4525,13 @@ dependencies = [ name = "magicblock-committor-program" version = "0.11.3" dependencies = [ - "borsh", + "borsh 1.6.0", "paste", - "solana-account", - "solana-account-info", - "solana-program", + "solana-account 3.4.0", + "solana-account-info 3.1.1", + "solana-program 3.0.0", "solana-pubkey 3.0.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-system-interface 3.2.0", "thiserror 2.0.18", ] @@ -4450,27 +4543,27 @@ dependencies = [ "async-trait", "base64 0.21.7", "bincode", - "borsh", + "borsh 1.6.0", "futures-util", "lru 0.16.2", "magicblock-committor-program", "magicblock-core", - "magicblock-delegation-program-api 0.3.0 (git+https://github.com/magicblock-labs/delegation-program.git?rev=25386a7c1d406d06b8d07a4d5b0fd37d5e74213b)", + "magicblock-delegation-program-api 0.3.0", "magicblock-metrics", "magicblock-program", "magicblock-rpc-client", "magicblock-table-mania", "rusqlite", - "solana-account", + "solana-account 3.4.0", "solana-account-decoder", - "solana-address-lookup-table-interface", + "solana-address-lookup-table-interface 3.1.0", "solana-commitment-config", "solana-compute-budget-interface", "solana-hash 3.1.0", - "solana-instruction", + "solana-instruction 3.4.0", "solana-keypair", - "solana-message", - "solana-program", + "solana-message 3.1.0", + "solana-program 3.0.0", "solana-pubkey 3.0.0", "solana-rpc-client", "solana-rpc-client-api", @@ -4478,7 +4571,7 @@ dependencies = [ "solana-signer", "solana-system-program", "solana-transaction", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "solana-transaction-status-client-types", "static_assertions", "thiserror 2.0.18", @@ -4501,7 +4594,7 @@ dependencies = [ "solana-keypair", "solana-pubkey 3.0.0", "solana-signer", - "toml", + "toml 0.8.23", "url", ] @@ -4514,17 +4607,17 @@ dependencies = [ "flume", "magicblock-magic-program-api 0.11.3", "serde", - "solana-account", + "solana-account 3.4.0", "solana-account-decoder", - "solana-clock", + "solana-clock 3.0.1", "solana-hash 3.1.0", - "solana-message", - "solana-program", + "solana-message 3.1.0", + "solana-program 3.0.0", "solana-pubkey 3.0.0", "solana-signature", "solana-transaction", "solana-transaction-context", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "solana-transaction-status-client-types", "spl-token-2022-interface", "spl-token-interface", @@ -4537,12 +4630,13 @@ dependencies = [ [[package]] name = "magicblock-delegation-program-api" version = "0.3.0" -source = "git+https://github.com/magicblock-labs/delegation-program.git?branch=snawaz%2Fupgrade#25386a7c1d406d06b8d07a4d5b0fd37d5e74213b" +source = "git+https://github.com/magicblock-labs/delegation-program.git?rev=25386a7c1d406d06b8d07a4d5b0fd37d5e74213b#25386a7c1d406d06b8d07a4d5b0fd37d5e74213b" dependencies = [ "bincode", - "borsh", + "borsh 1.6.0", "bytemuck", "const-crypto", + "libsodium-rs", "num_enum", "pinocchio 0.10.2", "pinocchio-log", @@ -4551,10 +4645,11 @@ dependencies = [ "rkyv 0.7.45", "serde", "solana-address 2.6.0", - "solana-instruction", - "solana-loader-v3-interface", - "solana-program", - "solana-sdk-ids", + "solana-instruction 3.4.0", + "solana-loader-v3-interface 6.1.1", + "solana-program 3.0.0", + "solana-sdk", + "solana-sdk-ids 3.1.0", "solana-sha256-hasher 3.1.0", "solana-system-interface 2.0.0", "static_assertions", @@ -4564,14 +4659,15 @@ dependencies = [ [[package]] name = "magicblock-delegation-program-api" -version = "0.3.0" -source = "git+https://github.com/magicblock-labs/delegation-program.git?rev=25386a7c1d406d06b8d07a4d5b0fd37d5e74213b#25386a7c1d406d06b8d07a4d5b0fd37d5e74213b" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "288904a9950bd20f27f0ef934f320ab1410bd35a6d5c9cf138eca276442b6b2e" dependencies = [ "bincode", - "borsh", + "borsh 0.10.4", + "borsh 1.6.0", "bytemuck", "const-crypto", - "libsodium-rs", "num_enum", "pinocchio 0.10.2", "pinocchio-log", @@ -4580,16 +4676,16 @@ dependencies = [ "rkyv 0.7.45", "serde", "solana-address 2.6.0", - "solana-instruction", - "solana-loader-v3-interface", - "solana-program", - "solana-sdk", - "solana-sdk-ids", + "solana-instruction 3.4.0", + "solana-loader-v3-interface 6.1.1", + "solana-program 3.0.0", + "solana-pubkey 2.4.0", + "solana-sdk-ids 3.1.0", "solana-sha256-hasher 3.1.0", "solana-system-interface 2.0.0", "static_assertions", "strum 0.27.2", - "thiserror 2.0.18", + "thiserror 1.0.69", ] [[package]] @@ -4610,10 +4706,10 @@ dependencies = [ "rocksdb", "serde", "solana-account-decoder", - "solana-clock", + "solana-clock 3.0.1", "solana-hash 3.1.0", - "solana-instruction", - "solana-message", + "solana-instruction 3.4.0", + "solana-message 3.1.0", "solana-metrics 2.3.13", "solana-pubkey 3.0.0", "solana-signature", @@ -4622,7 +4718,7 @@ dependencies = [ "solana-svm-measure", "solana-transaction", "solana-transaction-context", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "solana-transaction-status", "thiserror 2.0.18", "tokio", @@ -4632,12 +4728,14 @@ dependencies = [ [[package]] name = "magicblock-magic-program-api" -version = "0.9.0" -source = "git+https://github.com/magicblock-labs/magicblock-validator.git?branch=bmuddha%2Fepic%2Fmigration-solana-v3#ef8ebb18879f0a3c41c11159eb5a5358dd3c89c0" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dc8fba0307c90b91b70c9ed06d4242d6c4159f331b2f05bf8f875c2a94e0e98" dependencies = [ "bincode", + "const-crypto", "serde", - "solana-program", + "solana-program 3.0.0", "solana-signature", ] @@ -4648,7 +4746,7 @@ dependencies = [ "bincode", "const-crypto", "serde", - "solana-program", + "solana-program 3.0.0", "solana-signature", ] @@ -4684,27 +4782,27 @@ dependencies = [ "parking_lot", "rustc-hash 2.1.1", "serde", - "solana-account", + "solana-account 3.4.0", "solana-bpf-loader-program", "solana-compute-budget", "solana-compute-budget-program", - "solana-feature-gate-interface", + "solana-feature-gate-interface 3.1.0", "solana-fee-structure", - "solana-instruction", - "solana-loader-v3-interface", + "solana-instruction 3.4.0", + "solana-loader-v3-interface 6.1.1", "solana-loader-v4-program", "solana-precompile-error", - "solana-program", + "solana-program 3.0.0", "solana-program-runtime", "solana-pubkey 3.0.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-svm", "solana-svm-callback", "solana-svm-transaction", "solana-system-program", "solana-transaction", "solana-transaction-context", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "solana-transaction-status", "solana-zk-elgamal-proof-program", "tokio", @@ -4719,35 +4817,66 @@ dependencies = [ "bincode", "lazy_static", "magicblock-core", - "magicblock-delegation-program-api 0.3.0 (git+https://github.com/magicblock-labs/delegation-program.git?rev=25386a7c1d406d06b8d07a4d5b0fd37d5e74213b)", + "magicblock-delegation-program-api 0.3.0", "magicblock-magic-program-api 0.11.3", "num-derive", "num-traits", "parking_lot", "serde", - "solana-account", - "solana-account-info", - "solana-clock", - "solana-fee-calculator", + "solana-account 3.4.0", + "solana-account-info 3.1.1", + "solana-clock 3.0.1", + "solana-fee-calculator 3.2.0", "solana-hash 3.1.0", - "solana-instruction", - "solana-instructions-sysvar", + "solana-instruction 3.4.0", + "solana-instructions-sysvar 3.0.0", "solana-keypair", - "solana-loader-v3-interface", - "solana-loader-v4-interface", + "solana-loader-v3-interface 6.1.1", + "solana-loader-v4-interface 3.1.0", "solana-program-runtime", "solana-pubkey 3.0.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-seed-derivable", "solana-signature", "solana-signer", "solana-svm-log-collector", - "solana-sysvar", + "solana-sysvar 3.1.1", "solana-transaction", "solana-transaction-context", "thiserror 2.0.18", ] +[[package]] +name = "magicblock-query-filtering" +version = "0.11.3" +dependencies = [ + "base64 0.21.7", + "chrono", + "ed25519-dalek 2.2.0", + "jsonwebtoken", + "magicblock-accounts-db", + "magicblock-aml", + "magicblock-config", + "magicblock-core", + "rand 0.9.2", + "scc", + "serde", + "serde_json", + "sha2 0.10.9", + "solana-account 3.4.0", + "solana-account-decoder-client-types", + "solana-message 3.1.0", + "solana-program-pack 3.1.0", + "solana-pubkey 3.0.0", + "solana-signature", + "solana-transaction", + "solana-transaction-status", + "spl-token-interface", + "thiserror 2.0.18", + "tokio", + "tracing", +] + [[package]] name = "magicblock-replicator" version = "0.11.3" @@ -4765,7 +4894,7 @@ dependencies = [ "solana-hash 3.1.0", "solana-pubkey 3.0.0", "solana-transaction", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "thiserror 2.0.18", "tokio", "tokio-util 0.7.17", @@ -4780,19 +4909,19 @@ dependencies = [ "futures-util", "magicblock-metrics", "serde_json", - "solana-account", + "solana-account 3.4.0", "solana-account-decoder-client-types", - "solana-address-lookup-table-interface", - "solana-clock", + "solana-address-lookup-table-interface 3.1.0", + "solana-clock 3.0.1", "solana-commitment-config", "solana-hash 3.1.0", - "solana-instruction", + "solana-instruction 3.4.0", "solana-pubkey 3.0.0", "solana-pubsub-client", "solana-rpc-client", "solana-rpc-client-api", "solana-signature", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "solana-transaction-status-client-types", "thiserror 2.0.18", "tokio", @@ -4807,9 +4936,9 @@ dependencies = [ "futures-util", "magicblock-core", "magicblock-magic-program-api 0.11.3", - "solana-instruction", + "solana-instruction 3.4.0", "solana-keypair", - "solana-message", + "solana-message 3.1.0", "solana-pubkey 3.0.0", "solana-rpc-client", "solana-signature", @@ -4824,22 +4953,22 @@ dependencies = [ name = "magicblock-table-mania" version = "0.11.3" dependencies = [ - "ed25519-dalek 1.0.1", + "ed25519-dalek 2.2.0", "magicblock-metrics", "magicblock-rpc-client", "rand 0.9.2", "sha3", - "solana-address-lookup-table-interface", - "solana-clock", + "solana-address-lookup-table-interface 3.1.0", + "solana-clock 3.0.1", "solana-commitment-config", "solana-compute-budget-interface", - "solana-instruction", + "solana-instruction 3.4.0", "solana-keypair", - "solana-message", + "solana-message 3.1.0", "solana-pubkey 3.0.0", "solana-signature", "solana-signer", - "solana-slot-hashes", + "solana-slot-hashes 3.0.1", "solana-transaction", "thiserror 2.0.18", "tokio", @@ -4858,14 +4987,14 @@ dependencies = [ "magicblock-ledger", "magicblock-program", "rusqlite", - "solana-instruction", - "solana-message", + "solana-instruction 3.4.0", + "solana-message 3.1.0", "solana-pubkey 3.0.0", "solana-rpc-client", "solana-rpc-client-api", "solana-signature", "solana-transaction", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "thiserror 2.0.18", "tokio", "tokio-util 0.7.17", @@ -4876,7 +5005,7 @@ dependencies = [ name = "magicblock-validator-admin" version = "0.11.3" dependencies = [ - "magicblock-delegation-program-api 0.3.0 (git+https://github.com/magicblock-labs/delegation-program.git?rev=25386a7c1d406d06b8d07a4d5b0fd37d5e74213b)", + "magicblock-delegation-program-api 0.3.0", "magicblock-program", "magicblock-rpc-client", "solana-commitment-config", @@ -5257,9 +5386,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-derive" @@ -5349,7 +5478,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", "syn 2.0.117", @@ -5570,6 +5699,16 @@ dependencies = [ "base64 0.13.1", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -5663,7 +5802,7 @@ dependencies = [ "solana-address 2.6.0", "solana-define-syscall 4.0.1", "solana-instruction-view", - "solana-program-error", + "solana-program-error 3.0.0", ] [[package]] @@ -5814,6 +5953,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml 0.5.11", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -5871,11 +6019,11 @@ name = "program-flexi-counter" version = "0.0.0" dependencies = [ "bincode", - "borsh", + "borsh 1.6.0", "ephemeral-rollups-sdk", "magicblock-magic-program-api 0.11.3", "serde", - "solana-program", + "solana-program 3.0.0", "solana-system-interface 3.2.0", ] @@ -5883,10 +6031,10 @@ dependencies = [ name = "program-mini" version = "0.0.0" dependencies = [ - "solana-program", + "solana-program 3.0.0", "solana-program-test", "solana-sdk", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-system-interface 3.2.0", "tokio", ] @@ -5895,12 +6043,12 @@ dependencies = [ name = "program-schedulecommit" version = "0.0.0" dependencies = [ - "borsh", + "borsh 1.6.0", "ephemeral-rollups-sdk", - "magicblock-delegation-program-api 0.3.0 (git+https://github.com/magicblock-labs/delegation-program.git?rev=25386a7c1d406d06b8d07a4d5b0fd37d5e74213b)", + "magicblock-delegation-program-api 0.3.0", "magicblock-magic-program-api 0.11.3", "rkyv 0.7.45", - "solana-program", + "solana-program 3.0.0", "solana-system-interface 3.2.0", "static_assertions", ] @@ -5909,10 +6057,10 @@ dependencies = [ name = "program-schedulecommit-security" version = "0.0.0" dependencies = [ - "borsh", + "borsh 1.6.0", "ephemeral-rollups-sdk", "program-schedulecommit", - "solana-program", + "solana-program 3.0.0", ] [[package]] @@ -6930,13 +7078,13 @@ name = "schedulecommit-client" version = "0.0.0" dependencies = [ "anyhow", - "borsh", + "borsh 1.6.0", "integration-test-tools", - "magicblock-delegation-program-api 0.3.0 (git+https://github.com/magicblock-labs/delegation-program.git?rev=25386a7c1d406d06b8d07a4d5b0fd37d5e74213b)", + "magicblock-delegation-program-api 0.3.0", "program-schedulecommit", "solana-commitment-config", "solana-compute-budget-interface", - "solana-program", + "solana-program 3.0.0", "solana-rpc-client", "solana-rpc-client-api", "solana-sdk", @@ -6949,25 +7097,25 @@ name = "schedulecommit-committor-service" version = "0.0.0" dependencies = [ "async-trait", - "borsh", + "borsh 1.6.0", "futures", "magicblock-committor-program", "magicblock-committor-service", "magicblock-core", - "magicblock-delegation-program-api 0.3.0 (git+https://github.com/magicblock-labs/delegation-program.git?rev=25386a7c1d406d06b8d07a4d5b0fd37d5e74213b)", + "magicblock-delegation-program-api 0.3.0", "magicblock-program", "magicblock-rpc-client", "magicblock-table-mania", "program-flexi-counter", "program-schedulecommit", "rand 0.8.5", - "solana-account", + "solana-account 3.4.0", "solana-commitment-config", "solana-pubkey 3.0.0", "solana-rpc-client", "solana-rpc-client-api", "solana-sdk", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-system-interface 3.2.0", "test-kit", "tokio", @@ -6978,11 +7126,11 @@ dependencies = [ name = "schedulecommit-test-scenarios" version = "0.0.0" dependencies = [ - "borsh", + "borsh 1.6.0", "ephemeral-rollups-sdk", "integration-test-tools", "magicblock-core", - "magicblock-delegation-program-api 0.3.0 (git+https://github.com/magicblock-labs/delegation-program.git?rev=25386a7c1d406d06b8d07a4d5b0fd37d5e74213b)", + "magicblock-delegation-program-api 0.3.0", "magicblock-magic-program-api 0.11.3", "magicblock-program", "program-schedulecommit", @@ -6990,7 +7138,7 @@ dependencies = [ "schedulecommit-client", "serial_test", "solana-commitment-config", - "solana-program", + "solana-program 3.0.0", "solana-rpc-client", "solana-rpc-client-api", "solana-sdk", @@ -7003,7 +7151,7 @@ name = "schedulecommit-test-security" version = "0.0.0" dependencies = [ "integration-test-tools", - "magicblock-delegation-program-api 0.3.0 (git+https://github.com/magicblock-labs/delegation-program.git?rev=25386a7c1d406d06b8d07a4d5b0fd37d5e74213b)", + "magicblock-delegation-program-api 0.3.0", "magicblock-magic-program-api 0.11.3", "program-schedulecommit", "program-schedulecommit-security", @@ -7415,6 +7563,18 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint 0.4.6", + "num-traits", + "thiserror 2.0.18", + "time", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -7469,6 +7629,19 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "solana-account" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f949fe4edaeaea78c844023bfc1c898e0b1f5a100f8a8d2d0f85d0a7b090258" +dependencies = [ + "solana-account-info 2.3.0", + "solana-clock 2.2.3", + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", + "solana-sdk-ids 2.2.1", +] + [[package]] name = "solana-account" version = "3.4.0" @@ -7478,12 +7651,12 @@ dependencies = [ "qualifier_attr", "serde", "serde_bytes", - "solana-account-info", - "solana-clock", - "solana-instruction", + "solana-account-info 3.1.1", + "solana-clock 3.0.1", + "solana-instruction 3.4.0", "solana-pubkey 3.0.0", - "solana-sdk-ids", - "solana-sysvar", + "solana-sdk-ids 3.1.0", + "solana-sysvar 3.1.1", ] [[package]] @@ -7499,26 +7672,26 @@ dependencies = [ "bv", "serde", "serde_json", - "solana-account", + "solana-account 3.4.0", "solana-account-decoder-client-types", - "solana-address-lookup-table-interface", - "solana-clock", + "solana-address-lookup-table-interface 3.1.0", + "solana-clock 3.0.1", "solana-config-interface", - "solana-epoch-schedule", - "solana-fee-calculator", - "solana-instruction", - "solana-loader-v3-interface", - "solana-nonce", - "solana-program-option", - "solana-program-pack", + "solana-epoch-schedule 3.1.0", + "solana-fee-calculator 3.2.0", + "solana-instruction 3.4.0", + "solana-loader-v3-interface 6.1.1", + "solana-nonce 3.1.0", + "solana-program-option 3.1.0", + "solana-program-pack 3.1.0", "solana-pubkey 3.0.0", "solana-rent 3.1.0", - "solana-sdk-ids", - "solana-slot-hashes", - "solana-slot-history", - "solana-stake-interface", - "solana-sysvar", - "solana-vote-interface", + "solana-sdk-ids 3.1.0", + "solana-slot-hashes 3.0.1", + "solana-slot-history 3.0.0", + "solana-stake-interface 2.0.2", + "solana-sysvar 3.1.1", + "solana-vote-interface 4.0.4", "spl-generic-token", "spl-token-2022-interface", "spl-token-group-interface", @@ -7538,11 +7711,24 @@ dependencies = [ "bs58", "serde", "serde_json", - "solana-account", + "solana-account 3.4.0", "solana-pubkey 3.0.0", "zstd", ] +[[package]] +name = "solana-account-info" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8f5152a288ef1912300fc6efa6c2d1f9bb55d9398eb6c72326360b8063987da" +dependencies = [ + "bincode", + "serde", + "solana-program-error 2.2.2", + "solana-program-memory 2.3.1", + "solana-pubkey 2.4.0", +] + [[package]] name = "solana-account-info" version = "3.1.1" @@ -7552,8 +7738,8 @@ dependencies = [ "bincode", "serde_core", "solana-address 2.6.0", - "solana-program-error", - "solana-program-memory", + "solana-program-error 3.0.0", + "solana-program-memory 3.1.0", ] [[package]] @@ -7563,7 +7749,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37ca34c37f92ee341b73d5ce7c8ef5bb38e9a87955b4bd343c63fa18b149215" dependencies = [ "solana-address 2.6.0", - "solana-program-error", + "solana-program-error 3.0.0", ] [[package]] @@ -7594,31 +7780,31 @@ dependencies = [ "seqlock", "serde", "smallvec", - "solana-account", - "solana-address-lookup-table-interface", + "solana-account 3.4.0", + "solana-address-lookup-table-interface 3.1.0", "solana-bucket-map", - "solana-clock", - "solana-epoch-schedule", - "solana-fee-calculator", + "solana-clock 3.0.1", + "solana-epoch-schedule 3.1.0", + "solana-fee-calculator 3.2.0", "solana-genesis-config", "solana-hash 3.1.0", "solana-lattice-hash", "solana-measure", - "solana-message", + "solana-message 3.1.0", "solana-metrics 3.1.12", "solana-nohash-hasher", "solana-pubkey 3.0.0", "solana-rayon-threadlimit", "solana-reward-info", "solana-sha256-hasher 3.1.0", - "solana-slot-hashes", + "solana-slot-hashes 3.0.1", "solana-svm-transaction", "solana-system-interface 2.0.0", - "solana-sysvar", + "solana-sysvar 3.1.1", "solana-time-utils 3.0.0", "solana-transaction", "solana-transaction-context", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "spl-generic-token", "static_assertions", "tempfile", @@ -7640,7 +7826,7 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1384b52c435a750cc9c538760fc7bb472fd78e65a9900a2d07312c5bb335b72" dependencies = [ - "borsh", + "borsh 1.6.0", "bytemuck", "bytemuck_derive", "curve25519-dalek 4.1.3", @@ -7653,12 +7839,29 @@ dependencies = [ "solana-atomic-u64 3.0.1", "solana-define-syscall 5.0.0", "solana-nullable", - "solana-program-error", + "solana-program-error 3.0.0", "solana-sanitize 3.0.1", "solana-sha256-hasher 3.1.0", "wincode 0.5.1", ] +[[package]] +name = "solana-address-lookup-table-interface" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1673f67efe870b64a65cb39e6194be5b26527691ce5922909939961a6e6b395" +dependencies = [ + "bincode", + "bytemuck", + "serde", + "serde_derive", + "solana-clock 2.2.3", + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", + "solana-sdk-ids 2.2.1", + "solana-slot-hashes 2.2.1", +] + [[package]] name = "solana-address-lookup-table-interface" version = "3.1.0" @@ -7669,12 +7872,12 @@ dependencies = [ "bytemuck", "serde", "serde_derive", - "solana-clock", - "solana-instruction", + "solana-clock 3.0.1", + "solana-instruction 3.4.0", "solana-instruction-error", "solana-pubkey 4.2.0", - "solana-sdk-ids", - "solana-slot-hashes", + "solana-sdk-ids 3.1.0", + "solana-slot-hashes 3.0.1", ] [[package]] @@ -7701,22 +7904,22 @@ version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16034da9477c2ceb6bf03a04389d68f463fb9e09a7641b4517af4177976c5f59" dependencies = [ - "borsh", + "borsh 1.6.0", "futures", - "solana-account", + "solana-account 3.4.0", "solana-banks-interface", - "solana-clock", + "solana-clock 3.0.1", "solana-commitment-config", "solana-hash 3.1.0", - "solana-message", - "solana-program-pack", + "solana-message 3.1.0", + "solana-program-pack 3.1.0", "solana-pubkey 3.0.0", "solana-rent 3.1.0", "solana-signature", - "solana-sysvar", + "solana-sysvar 3.1.1", "solana-transaction", "solana-transaction-context", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "tarpc", "thiserror 2.0.18", "tokio", @@ -7730,16 +7933,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aeed8b00c1b30e10cada0de485c4c7aa2805bc54587d7fbfd4c89b9a6a2b7bb5" dependencies = [ "serde", - "solana-account", - "solana-clock", + "solana-account 3.4.0", + "solana-clock 3.0.1", "solana-commitment-config", "solana-hash 3.1.0", - "solana-message", + "solana-message 3.1.0", "solana-pubkey 3.0.0", "solana-signature", "solana-transaction", "solana-transaction-context", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "tarpc", ] @@ -7753,13 +7956,13 @@ dependencies = [ "bincode", "crossbeam-channel", "futures", - "solana-account", + "solana-account 3.4.0", "solana-banks-interface", "solana-client", - "solana-clock", + "solana-clock 3.0.1", "solana-commitment-config", "solana-hash 3.1.0", - "solana-message", + "solana-message 3.1.0", "solana-pubkey 3.0.0", "solana-runtime", "solana-runtime-transaction", @@ -7767,12 +7970,23 @@ dependencies = [ "solana-signature", "solana-svm", "solana-transaction", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "tarpc", "tokio", "tokio-serde", ] +[[package]] +name = "solana-big-mod-exp" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75db7f2bbac3e62cfd139065d15bcda9e2428883ba61fc8d27ccb251081e7567" +dependencies = [ + "num-bigint 0.4.6", + "num-traits", + "solana-define-syscall 2.3.0", +] + [[package]] name = "solana-big-mod-exp" version = "3.0.0" @@ -7784,6 +7998,17 @@ dependencies = [ "solana-define-syscall 3.0.0", ] +[[package]] +name = "solana-bincode" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a3787b8cf9c9fe3dd360800e8b70982b9e5a8af9e11c354b6665dd4a003adc" +dependencies = [ + "bincode", + "serde", + "solana-instruction 2.3.3", +] + [[package]] name = "solana-bincode" version = "3.1.0" @@ -7795,6 +8020,18 @@ dependencies = [ "solana-instruction-error", ] +[[package]] +name = "solana-blake3-hasher" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0801e25a1b31a14494fc80882a036be0ffd290efc4c2d640bfcca120a4672" +dependencies = [ + "blake3", + "solana-define-syscall 2.3.0", + "solana-hash 2.3.0", + "solana-sanitize 2.2.1", +] + [[package]] name = "solana-blake3-hasher" version = "3.1.0" @@ -7851,7 +8088,7 @@ version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c04abbae16f57178a163125805637b8a076175bb5c0002fb04f4792bea901cf7" dependencies = [ - "borsh", + "borsh 1.6.0", ] [[package]] @@ -7863,18 +8100,18 @@ dependencies = [ "agave-syscalls", "bincode", "qualifier_attr", - "solana-account", - "solana-bincode", - "solana-clock", - "solana-instruction", - "solana-loader-v3-interface", - "solana-loader-v4-interface", + "solana-account 3.4.0", + "solana-bincode 3.1.0", + "solana-clock 3.0.1", + "solana-instruction 3.4.0", + "solana-loader-v3-interface 6.1.1", + "solana-loader-v4-interface 3.1.0", "solana-packet", - "solana-program-entrypoint", + "solana-program-entrypoint 3.1.1", "solana-program-runtime", "solana-pubkey 3.0.0", "solana-sbpf", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-svm-feature-set", "solana-svm-log-collector", "solana-svm-measure", @@ -7896,7 +8133,7 @@ dependencies = [ "modular-bitfield", "num_enum", "rand 0.8.5", - "solana-clock", + "solana-clock 3.0.1", "solana-measure", "solana-pubkey 3.0.0", "tempfile", @@ -7915,7 +8152,7 @@ dependencies = [ "solana-loader-v4-program", "solana-program-runtime", "solana-pubkey 3.0.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-system-program", "solana-vote-program", "solana-zk-elgamal-proof-program", @@ -7935,7 +8172,7 @@ dependencies = [ "solana-compute-budget-program", "solana-loader-v4-program", "solana-pubkey 3.0.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-system-program", "solana-vote-program", ] @@ -7956,16 +8193,16 @@ dependencies = [ "indicatif", "log", "rayon", - "solana-account", + "solana-account 3.4.0", "solana-client-traits", "solana-commitment-config", "solana-connection-cache", "solana-epoch-info", "solana-hash 3.1.0", - "solana-instruction", + "solana-instruction 3.4.0", "solana-keypair", "solana-measure", - "solana-message", + "solana-message 3.1.0", "solana-net-utils", "solana-pubkey 3.0.0", "solana-pubsub-client", @@ -7980,7 +8217,7 @@ dependencies = [ "solana-time-utils 3.0.0", "solana-tpu-client", "solana-transaction", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "solana-transaction-status-client-types", "solana-udp-client", "thiserror 2.0.18", @@ -7994,19 +8231,32 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08618ed587e128105510c54ae3e456b9a06d674d8640db75afe66dad65cb4e02" dependencies = [ - "solana-account", + "solana-account 3.4.0", "solana-commitment-config", "solana-epoch-info", "solana-hash 3.1.0", - "solana-instruction", + "solana-instruction 3.4.0", "solana-keypair", - "solana-message", + "solana-message 3.1.0", "solana-pubkey 3.0.0", "solana-signature", "solana-signer", "solana-system-interface 2.0.0", "solana-transaction", - "solana-transaction-error", + "solana-transaction-error 3.1.0", +] + +[[package]] +name = "solana-clock" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8584296123df8fe229b95e2ebfd37ae637fe9db9b7d4dd677ac5a78e80dbfce" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids 2.2.1", + "solana-sdk-macro 2.2.1", + "solana-sysvar-id 2.2.1", ] [[package]] @@ -8017,9 +8267,9 @@ checksum = "95cf11109c3b6115cc510f1e31f06fdd52f504271bc24ef5f1249fbbcae5f9f3" dependencies = [ "serde", "serde_derive", - "solana-sdk-ids", - "solana-sdk-macro", - "solana-sysvar-id", + "solana-sdk-ids 3.1.0", + "solana-sdk-macro 3.0.1", + "solana-sysvar-id 3.1.0", ] [[package]] @@ -8074,12 +8324,12 @@ dependencies = [ "solana-builtins-default-costs", "solana-compute-budget", "solana-compute-budget-interface", - "solana-instruction", + "solana-instruction 3.4.0", "solana-packet", "solana-pubkey 3.0.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-svm-transaction", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "thiserror 2.0.18", ] @@ -8089,9 +8339,9 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8292c436b269ad23cecc8b24f7da3ab07ca111661e25e00ce0e1d22771951ab9" dependencies = [ - "borsh", - "solana-instruction", - "solana-sdk-ids", + "borsh 1.6.0", + "solana-instruction 3.4.0", + "solana-sdk-ids 3.1.0", ] [[package]] @@ -8112,11 +8362,11 @@ dependencies = [ "bincode", "serde", "serde_derive", - "solana-account", - "solana-instruction", + "solana-account 3.4.0", + "solana-instruction 3.4.0", "solana-pubkey 3.0.0", - "solana-sdk-ids", - "solana-short-vec", + "solana-sdk-ids 3.1.0", + "solana-short-vec 3.2.0", "solana-system-interface 2.0.0", ] @@ -8138,7 +8388,7 @@ dependencies = [ "solana-measure", "solana-metrics 3.1.12", "solana-time-utils 3.0.0", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "thiserror 2.0.18", "tokio", ] @@ -8152,10 +8402,10 @@ dependencies = [ "agave-feature-set", "ahash 0.8.12", "log", - "solana-bincode", + "solana-bincode 3.1.0", "solana-borsh", "solana-builtins-default-costs", - "solana-clock", + "solana-clock 3.0.1", "solana-compute-budget", "solana-compute-budget-instruction", "solana-compute-budget-interface", @@ -8164,25 +8414,39 @@ dependencies = [ "solana-packet", "solana-pubkey 3.0.0", "solana-runtime-transaction", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-svm-transaction", "solana-system-interface 2.0.0", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "solana-vote-program", ] +[[package]] +name = "solana-cpi" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc71126edddc2ba014622fc32d0f5e2e78ec6c5a1e0eb511b85618c09e9ea11" +dependencies = [ + "solana-account-info 2.3.0", + "solana-define-syscall 2.3.0", + "solana-instruction 2.3.3", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", + "solana-stable-layout 2.2.1", +] + [[package]] name = "solana-cpi" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dea26709d867aada85d0d3617db0944215c8bb28d3745b912de7db13a23280c" dependencies = [ - "solana-account-info", + "solana-account-info 3.1.1", "solana-define-syscall 4.0.1", - "solana-instruction", - "solana-program-error", + "solana-instruction 3.4.0", + "solana-program-error 3.0.0", "solana-pubkey 4.2.0", - "solana-stable-layout", + "solana-stable-layout 3.0.1", ] [[package]] @@ -8199,6 +8463,15 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "solana-decode-error" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c781686a18db2f942e70913f7ca15dc120ec38dcab42ff7557db2c70c625a35" +dependencies = [ + "num-traits", +] + [[package]] name = "solana-define-syscall" version = "2.3.0" @@ -8242,8 +8515,8 @@ checksum = "e1419197f1c06abf760043f6d64ba9d79a03ad5a43f18c7586471937122094da" dependencies = [ "bytemuck", "bytemuck_derive", - "solana-instruction", - "solana-sdk-ids", + "solana-instruction 3.4.0", + "solana-sdk-ids 3.1.0", ] [[package]] @@ -8256,6 +8529,20 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "solana-epoch-rewards" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b575d3dd323b9ea10bb6fe89bf6bf93e249b215ba8ed7f68f1a3633f384db7" +dependencies = [ + "serde", + "serde_derive", + "solana-hash 2.3.0", + "solana-sdk-ids 2.2.1", + "solana-sdk-macro 2.2.1", + "solana-sysvar-id 2.2.1", +] + [[package]] name = "solana-epoch-rewards" version = "3.0.1" @@ -8265,9 +8552,9 @@ dependencies = [ "serde", "serde_derive", "solana-hash 4.2.0", - "solana-sdk-ids", - "solana-sdk-macro", - "solana-sysvar-id", + "solana-sdk-ids 3.1.0", + "solana-sdk-macro 3.0.1", + "solana-sysvar-id 3.1.0", ] [[package]] @@ -8283,20 +8570,33 @@ dependencies = [ [[package]] name = "solana-epoch-schedule" -version = "3.1.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce264b7b42322325947c4136a09460bf5c73d9aa8262c9b0a2064be63ba8639" +checksum = "3fce071fbddecc55d727b1d7ed16a629afe4f6e4c217bc8d00af3b785f6f67ed" dependencies = [ "serde", "serde_derive", - "solana-sdk-ids", - "solana-sdk-macro", - "solana-sysvar-id", + "solana-sdk-ids 2.2.1", + "solana-sdk-macro 2.2.1", + "solana-sysvar-id 2.2.1", ] [[package]] -name = "solana-epoch-stake" -version = "3.0.1" +name = "solana-epoch-schedule" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce264b7b42322325947c4136a09460bf5c73d9aa8262c9b0a2064be63ba8639" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids 3.1.0", + "solana-sdk-macro 3.0.1", + "solana-sysvar-id 3.1.0", +] + +[[package]] +name = "solana-epoch-stake" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "027e6d0b9e7daac5b2ac7c3f9ca1b727861121d9ef05084cf435ff736051e7c2" dependencies = [ @@ -8304,6 +8604,27 @@ dependencies = [ "solana-pubkey 4.2.0", ] +[[package]] +name = "solana-example-mocks" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84461d56cbb8bb8d539347151e0525b53910102e4bced875d49d5139708e39d3" +dependencies = [ + "serde", + "serde_derive", + "solana-address-lookup-table-interface 2.2.2", + "solana-clock 2.2.3", + "solana-hash 2.3.0", + "solana-instruction 2.3.3", + "solana-keccak-hasher 2.2.1", + "solana-message 2.4.0", + "solana-nonce 2.2.1", + "solana-pubkey 2.4.0", + "solana-sdk-ids 2.2.1", + "solana-system-interface 1.0.0", + "thiserror 2.0.18", +] + [[package]] name = "solana-example-mocks" version = "3.0.0" @@ -8312,19 +8633,38 @@ checksum = "978855d164845c1b0235d4b4d101cadc55373fffaf0b5b6cfa2194d25b2ed658" dependencies = [ "serde", "serde_derive", - "solana-address-lookup-table-interface", - "solana-clock", + "solana-address-lookup-table-interface 3.1.0", + "solana-clock 3.0.1", "solana-hash 3.1.0", - "solana-instruction", - "solana-keccak-hasher", - "solana-message", - "solana-nonce", + "solana-instruction 3.4.0", + "solana-keccak-hasher 3.1.0", + "solana-message 3.1.0", + "solana-nonce 3.1.0", "solana-pubkey 3.0.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-system-interface 2.0.0", "thiserror 2.0.18", ] +[[package]] +name = "solana-feature-gate-interface" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f5c5382b449e8e4e3016fb05e418c53d57782d8b5c30aa372fc265654b956d" +dependencies = [ + "bincode", + "serde", + "serde_derive", + "solana-account 2.2.1", + "solana-account-info 2.3.0", + "solana-instruction 2.3.3", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", + "solana-rent 2.2.1", + "solana-sdk-ids 2.2.1", + "solana-system-interface 1.0.0", +] + [[package]] name = "solana-feature-gate-interface" version = "3.1.0" @@ -8334,13 +8674,13 @@ dependencies = [ "bincode", "serde", "serde_derive", - "solana-account", - "solana-account-info", - "solana-instruction", - "solana-program-error", + "solana-account 3.4.0", + "solana-account-info 3.1.1", + "solana-instruction 3.4.0", + "solana-program-error 3.0.0", "solana-pubkey 4.2.0", "solana-rent 4.2.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-system-interface 3.2.0", ] @@ -8355,6 +8695,17 @@ dependencies = [ "solana-svm-transaction", ] +[[package]] +name = "solana-fee-calculator" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89bc408da0fb3812bc3008189d148b4d3e08252c79ad810b245482a3f70cd8d" +dependencies = [ + "log", + "serde", + "serde_derive", +] + [[package]] name = "solana-fee-calculator" version = "3.2.0" @@ -8398,18 +8749,18 @@ dependencies = [ "memmap2 0.5.10", "serde", "serde_derive", - "solana-account", - "solana-clock", + "solana-account 3.4.0", + "solana-clock 3.0.1", "solana-cluster-type 3.1.0", - "solana-epoch-schedule", - "solana-fee-calculator", + "solana-epoch-schedule 3.1.0", + "solana-fee-calculator 3.2.0", "solana-hash 3.1.0", "solana-inflation", "solana-keypair", "solana-poh-config", "solana-pubkey 3.0.0", "solana-rent 3.1.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-sha256-hasher 3.1.0", "solana-shred-version", "solana-signer", @@ -8432,8 +8783,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b96e9f0300fa287b545613f007dfe20043d7812bee255f418c1eb649c93b63" dependencies = [ + "bytemuck", + "bytemuck_derive", "five8 0.2.1", "js-sys", + "serde", + "serde_derive", "solana-atomic-u64 2.2.1", "solana-sanitize 2.2.1", "wasm-bindgen", @@ -8454,7 +8809,7 @@ version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8064ea1d591ec791be95245058ca40f4f5345d390c200069d0f79bbf55bfae55" dependencies = [ - "borsh", + "borsh 1.6.0", "bytemuck", "bytemuck_derive", "five8 1.0.0", @@ -8475,6 +8830,24 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "solana-instruction" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab5682934bd1f65f8d2c16f21cb532526fcc1a09f796e2cacdb091eee5774ad" +dependencies = [ + "bincode", + "getrandom 0.2.16", + "js-sys", + "num-traits", + "serde", + "serde_derive", + "serde_json", + "solana-define-syscall 2.3.0", + "solana-pubkey 2.4.0", + "wasm-bindgen", +] + [[package]] name = "solana-instruction" version = "3.4.0" @@ -8482,7 +8855,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37ebb0ffd19263051bc3f683fcc086134b8ff23af894dcb63f7563c7137b42f1" dependencies = [ "bincode", - "borsh", + "borsh 1.6.0", "serde", "serde_derive", "solana-define-syscall 5.0.0", @@ -8499,7 +8872,7 @@ dependencies = [ "num-traits", "serde", "serde_derive", - "solana-program-error", + "solana-program-error 3.0.0", ] [[package]] @@ -8511,7 +8884,24 @@ dependencies = [ "solana-account-view", "solana-address 2.6.0", "solana-define-syscall 4.0.1", - "solana-program-error", + "solana-program-error 3.0.0", +] + +[[package]] +name = "solana-instructions-sysvar" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0e85a6fad5c2d0c4f5b91d34b8ca47118fc593af706e523cdbedf846a954f57" +dependencies = [ + "bitflags 2.10.0", + "solana-account-info 2.3.0", + "solana-instruction 2.3.3", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", + "solana-sanitize 2.2.1", + "solana-sdk-ids 2.2.1", + "solana-serialize-utils 2.2.1", + "solana-sysvar-id 2.2.1", ] [[package]] @@ -8521,15 +8911,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ddf67876c541aa1e21ee1acae35c95c6fbc61119814bfef70579317a5e26955" dependencies = [ "bitflags 2.10.0", - "solana-account-info", - "solana-instruction", + "solana-account-info 3.1.1", + "solana-instruction 3.4.0", "solana-instruction-error", - "solana-program-error", + "solana-program-error 3.0.0", "solana-pubkey 3.0.0", "solana-sanitize 3.0.1", - "solana-sdk-ids", - "solana-serialize-utils", - "solana-sysvar-id", + "solana-sdk-ids 3.1.0", + "solana-serialize-utils 3.1.1", + "solana-sysvar-id 3.1.0", +] + +[[package]] +name = "solana-keccak-hasher" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7aeb957fbd42a451b99235df4942d96db7ef678e8d5061ef34c9b34cae12f79" +dependencies = [ + "sha3", + "solana-define-syscall 2.3.0", + "solana-hash 2.3.0", + "solana-sanitize 2.2.1", ] [[package]] @@ -8561,6 +8963,19 @@ dependencies = [ "solana-signer", ] +[[package]] +name = "solana-last-restart-slot" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6360ac2fdc72e7463565cd256eedcf10d7ef0c28a1249d261ec168c1b55cdd" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids 2.2.1", + "solana-sdk-macro 2.2.1", + "solana-sysvar-id 2.2.1", +] + [[package]] name = "solana-last-restart-slot" version = "3.0.0" @@ -8569,9 +8984,9 @@ checksum = "dcda154ec827f5fc1e4da0af3417951b7e9b8157540f81f936c4a8b1156134d0" dependencies = [ "serde", "serde_derive", - "solana-sdk-ids", - "solana-sdk-macro", - "solana-sysvar-id", + "solana-sdk-ids 3.1.0", + "solana-sdk-macro 3.0.1", + "solana-sysvar-id 3.1.0", ] [[package]] @@ -8586,6 +9001,20 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "solana-loader-v2-interface" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8ab08006dad78ae7cd30df8eea0539e207d08d91eaefb3e1d49a446e1c49654" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", + "solana-sdk-ids 2.2.1", +] + [[package]] name = "solana-loader-v2-interface" version = "3.0.0" @@ -8595,9 +9024,24 @@ dependencies = [ "serde", "serde_bytes", "serde_derive", - "solana-instruction", + "solana-instruction 3.4.0", "solana-pubkey 3.0.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", +] + +[[package]] +name = "solana-loader-v3-interface" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f7162a05b8b0773156b443bccd674ea78bb9aa406325b467ea78c06c99a63a2" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", + "solana-sdk-ids 2.2.1", + "solana-system-interface 1.0.0", ] [[package]] @@ -8609,12 +9053,27 @@ dependencies = [ "serde", "serde_bytes", "serde_derive", - "solana-instruction", + "solana-instruction 3.4.0", "solana-pubkey 4.2.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-system-interface 3.2.0", ] +[[package]] +name = "solana-loader-v4-interface" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "706a777242f1f39a83e2a96a2a6cb034cb41169c6ecbee2cf09cb873d9659e7e" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", + "solana-sdk-ids 2.2.1", + "solana-system-interface 1.0.0", +] + [[package]] name = "solana-loader-v4-interface" version = "3.1.0" @@ -8624,9 +9083,9 @@ dependencies = [ "serde", "serde_bytes", "serde_derive", - "solana-instruction", + "solana-instruction 3.4.0", "solana-pubkey 3.0.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-system-interface 2.0.0", ] @@ -8637,17 +9096,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e88b98ba6b408fe6fcf180784fc378d07ae63100c7dd413ff97474b6de30c5d" dependencies = [ "log", - "solana-account", - "solana-bincode", + "solana-account 3.4.0", + "solana-bincode 3.1.0", "solana-bpf-loader-program", - "solana-instruction", - "solana-loader-v3-interface", - "solana-loader-v4-interface", + "solana-instruction 3.4.0", + "solana-loader-v3-interface 6.1.1", + "solana-loader-v4-interface 3.1.0", "solana-packet", "solana-program-runtime", "solana-pubkey 3.0.0", "solana-sbpf", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-svm-log-collector", "solana-svm-measure", "solana-svm-type-overrides", @@ -8660,6 +9119,29 @@ version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de496c3f0e0cee33abd4ae958a8703e04ae1011f3c3e73b641bf319a18301c01" +[[package]] +name = "solana-message" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1796aabce376ff74bf89b78d268fa5e683d7d7a96a0a4e4813ec34de49d5314b" +dependencies = [ + "bincode", + "blake3", + "lazy_static", + "serde", + "serde_derive", + "solana-bincode 2.2.1", + "solana-hash 2.3.0", + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", + "solana-sanitize 2.2.1", + "solana-sdk-ids 2.2.1", + "solana-short-vec 2.2.1", + "solana-system-interface 1.0.0", + "solana-transaction-error 2.2.1", + "wasm-bindgen", +] + [[package]] name = "solana-message" version = "3.1.0" @@ -8673,11 +9155,11 @@ dependencies = [ "serde_derive", "solana-address 2.6.0", "solana-hash 4.2.0", - "solana-instruction", + "solana-instruction 3.4.0", "solana-sanitize 3.0.1", - "solana-sdk-ids", - "solana-short-vec", - "solana-transaction-error", + "solana-sdk-ids 3.1.0", + "solana-short-vec 3.2.0", + "solana-transaction-error 3.1.0", ] [[package]] @@ -8712,6 +9194,15 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "solana-msg" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36a1a14399afaabc2781a1db09cb14ee4cc4ee5c7a5a3cfcc601811379a8092" +dependencies = [ + "solana-define-syscall 2.3.0", +] + [[package]] name = "solana-msg" version = "3.1.0" @@ -8721,6 +9212,12 @@ dependencies = [ "solana-define-syscall 5.0.0", ] +[[package]] +name = "solana-native-token" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61515b880c36974053dd499c0510066783f0cc6ac17def0c7ef2a244874cf4a9" + [[package]] name = "solana-native-token" version = "3.0.0" @@ -8756,6 +9253,20 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b8a731ed60e89177c8a7ab05fe0f1511cedd3e70e773f288f9de33a9cfdc21e" +[[package]] +name = "solana-nonce" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703e22eb185537e06204a5bd9d509b948f0066f2d1d814a6f475dafb3ddf1325" +dependencies = [ + "serde", + "serde_derive", + "solana-fee-calculator 2.2.1", + "solana-hash 2.3.0", + "solana-pubkey 2.4.0", + "solana-sha256-hasher 2.2.1", +] + [[package]] name = "solana-nonce" version = "3.1.0" @@ -8764,7 +9275,7 @@ checksum = "cbc469152a63284ef959b80c59cda015262a021da55d3b8fe42171d89c4b64f8" dependencies = [ "serde", "serde_derive", - "solana-fee-calculator", + "solana-fee-calculator 3.2.0", "solana-hash 4.2.0", "solana-pubkey 4.2.0", "solana-sha256-hasher 3.1.0", @@ -8776,10 +9287,10 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "805fd25b29e5a1a0e6c3dd6320c9da80f275fbe4ff6e392617c303a2085c435e" dependencies = [ - "solana-account", + "solana-account 3.4.0", "solana-hash 3.1.0", - "solana-nonce", - "solana-sdk-ids", + "solana-nonce 3.1.0", + "solana-sdk-ids 3.1.0", ] [[package]] @@ -8842,13 +9353,13 @@ dependencies = [ "rayon", "serde", "solana-hash 3.1.0", - "solana-message", + "solana-message 3.1.0", "solana-metrics 3.1.12", "solana-packet", "solana-pubkey 3.0.0", "solana-rayon-threadlimit", - "solana-sdk-ids", - "solana-short-vec", + "solana-sdk-ids 3.1.0", + "solana-short-vec 3.2.0", "solana-signature", "solana-time-utils 3.0.0", "solana-transaction-context", @@ -8898,6 +9409,83 @@ dependencies = [ "solana-signer", ] +[[package]] +name = "solana-program" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98eca145bd3545e2fbb07166e895370576e47a00a7d824e325390d33bf467210" +dependencies = [ + "bincode", + "blake3", + "bs58", + "bytemuck", + "console_error_panic_hook", + "console_log", + "getrandom 0.2.16", + "lazy_static", + "log", + "memoffset", + "num-bigint 0.4.6", + "num-derive", + "num-traits", + "rand 0.8.5", + "serde", + "serde_bytes", + "serde_derive", + "solana-account-info 2.3.0", + "solana-address-lookup-table-interface 2.2.2", + "solana-atomic-u64 2.2.1", + "solana-big-mod-exp 2.2.1", + "solana-bincode 2.2.1", + "solana-blake3-hasher 2.2.1", + "solana-clock 2.2.3", + "solana-cpi 2.2.1", + "solana-decode-error", + "solana-define-syscall 2.3.0", + "solana-epoch-rewards 2.2.1", + "solana-epoch-schedule 2.2.1", + "solana-example-mocks 2.2.1", + "solana-feature-gate-interface 2.2.2", + "solana-fee-calculator 2.2.1", + "solana-hash 2.3.0", + "solana-instruction 2.3.3", + "solana-instructions-sysvar 2.2.2", + "solana-keccak-hasher 2.2.1", + "solana-last-restart-slot 2.2.1", + "solana-loader-v2-interface 2.2.1", + "solana-loader-v3-interface 5.0.0", + "solana-loader-v4-interface 2.2.1", + "solana-message 2.4.0", + "solana-msg 2.2.1", + "solana-native-token 2.3.0", + "solana-nonce 2.2.1", + "solana-program-entrypoint 2.3.0", + "solana-program-error 2.2.2", + "solana-program-memory 2.3.1", + "solana-program-option 2.2.1", + "solana-program-pack 2.2.1", + "solana-pubkey 2.4.0", + "solana-rent 2.2.1", + "solana-sanitize 2.2.1", + "solana-sdk-ids 2.2.1", + "solana-sdk-macro 2.2.1", + "solana-secp256k1-recover 2.2.1", + "solana-serde-varint 2.2.2", + "solana-serialize-utils 2.2.1", + "solana-sha256-hasher 2.2.1", + "solana-short-vec 2.2.1", + "solana-slot-hashes 2.2.1", + "solana-slot-history 2.2.1", + "solana-stable-layout 2.2.1", + "solana-stake-interface 1.2.1", + "solana-system-interface 1.0.0", + "solana-sysvar 2.3.0", + "solana-sysvar-id 2.2.1", + "solana-vote-interface 2.2.6", + "thiserror 2.0.18", + "wasm-bindgen", +] + [[package]] name = "solana-program" version = "3.0.0" @@ -8905,44 +9493,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91b12305dd81045d705f427acd0435a2e46444b65367d7179d7bdcfc3bc5f5eb" dependencies = [ "memoffset", - "solana-account-info", - "solana-big-mod-exp", - "solana-blake3-hasher", + "solana-account-info 3.1.1", + "solana-big-mod-exp 3.0.0", + "solana-blake3-hasher 3.1.0", "solana-borsh", - "solana-clock", - "solana-cpi", + "solana-clock 3.0.1", + "solana-cpi 3.1.0", "solana-define-syscall 3.0.0", - "solana-epoch-rewards", - "solana-epoch-schedule", + "solana-epoch-rewards 3.0.1", + "solana-epoch-schedule 3.1.0", "solana-epoch-stake", - "solana-example-mocks", - "solana-fee-calculator", + "solana-example-mocks 3.0.0", + "solana-fee-calculator 3.2.0", "solana-hash 3.1.0", - "solana-instruction", + "solana-instruction 3.4.0", "solana-instruction-error", - "solana-instructions-sysvar", - "solana-keccak-hasher", - "solana-last-restart-slot", - "solana-msg", - "solana-native-token", - "solana-program-entrypoint", - "solana-program-error", - "solana-program-memory", - "solana-program-option", - "solana-program-pack", + "solana-instructions-sysvar 3.0.0", + "solana-keccak-hasher 3.1.0", + "solana-last-restart-slot 3.0.0", + "solana-msg 3.1.0", + "solana-native-token 3.0.0", + "solana-program-entrypoint 3.1.1", + "solana-program-error 3.0.0", + "solana-program-memory 3.1.0", + "solana-program-option 3.1.0", + "solana-program-pack 3.1.0", "solana-pubkey 3.0.0", "solana-rent 3.1.0", - "solana-sdk-ids", - "solana-secp256k1-recover", - "solana-serde-varint", - "solana-serialize-utils", + "solana-sdk-ids 3.1.0", + "solana-secp256k1-recover 3.1.1", + "solana-serde-varint 3.0.1", + "solana-serialize-utils 3.1.1", "solana-sha256-hasher 3.1.0", - "solana-short-vec", - "solana-slot-hashes", - "solana-slot-history", - "solana-stable-layout", - "solana-sysvar", - "solana-sysvar-id", + "solana-short-vec 3.2.0", + "solana-slot-hashes 3.0.1", + "solana-slot-history 3.0.0", + "solana-stable-layout 3.0.1", + "solana-sysvar 3.1.1", + "solana-sysvar-id 3.1.0", ] [[package]] @@ -8953,37 +9541,73 @@ checksum = "68d372660fc5c61b691279bcecb4bf92fa1a7cba3b357ef209404f425dafd394" dependencies = [ "bincode", "serde", - "solana-account", - "solana-loader-v3-interface", + "solana-account 3.4.0", + "solana-loader-v3-interface 6.1.1", "solana-pubkey 3.0.0", "solana-rent 3.1.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "spl-generic-token", ] +[[package]] +name = "solana-program-entrypoint" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ce041b1a0ed275290a5008ee1a4a6c48f5054c8a3d78d313c08958a06aedbd" +dependencies = [ + "solana-account-info 2.3.0", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", +] + [[package]] name = "solana-program-entrypoint" version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84c9b0a1ff494e05f503a08b3d51150b73aa639544631e510279d6375f290997" dependencies = [ - "solana-account-info", + "solana-account-info 3.1.1", "solana-define-syscall 4.0.1", - "solana-program-error", + "solana-program-error 3.0.0", "solana-pubkey 4.2.0", ] +[[package]] +name = "solana-program-error" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee2e0217d642e2ea4bee237f37bd61bb02aec60da3647c48ff88f6556ade775" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-decode-error", + "solana-instruction 2.3.3", + "solana-msg 2.2.1", + "solana-pubkey 2.4.0", +] + [[package]] name = "solana-program-error" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1af32c995a7b692a915bb7414d5f8e838450cf7c70414e763d8abcae7b51f28" dependencies = [ - "borsh", + "borsh 1.6.0", "serde", "serde_derive", ] +[[package]] +name = "solana-program-memory" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a5426090c6f3fd6cfdc10685322fede9ca8e5af43cd6a59e98bfe4e91671712" +dependencies = [ + "solana-define-syscall 2.3.0", +] + [[package]] name = "solana-program-memory" version = "3.1.0" @@ -8993,19 +9617,34 @@ dependencies = [ "solana-define-syscall 4.0.1", ] +[[package]] +name = "solana-program-option" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc677a2e9bc616eda6dbdab834d463372b92848b2bfe4a1ed4e4b4adba3397d0" + [[package]] name = "solana-program-option" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a88006a9b8594088cec9027ab77caaaa258a2aaa2083d3f086c44b42e50aeab" +[[package]] +name = "solana-program-pack" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "319f0ef15e6e12dc37c597faccb7d62525a509fec5f6975ecb9419efddeb277b" +dependencies = [ + "solana-program-error 2.2.2", +] + [[package]] name = "solana-program-pack" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7701cb15b90667ae1c89ef4ac35a59c61e66ce58ddee13d729472af7f41d59" dependencies = [ - "solana-program-error", + "solana-program-error 3.0.0", ] [[package]] @@ -9020,24 +9659,24 @@ dependencies = [ "percentage", "rand 0.8.5", "serde", - "solana-account", - "solana-account-info", - "solana-clock", - "solana-epoch-rewards", - "solana-epoch-schedule", + "solana-account 3.4.0", + "solana-account-info 3.1.1", + "solana-clock 3.0.1", + "solana-epoch-rewards 3.0.1", + "solana-epoch-schedule 3.1.0", "solana-fee-structure", "solana-hash 3.1.0", - "solana-instruction", - "solana-last-restart-slot", - "solana-loader-v3-interface", - "solana-program-entrypoint", + "solana-instruction 3.4.0", + "solana-last-restart-slot 3.0.0", + "solana-loader-v3-interface 6.1.1", + "solana-program-entrypoint 3.1.1", "solana-pubkey 3.0.0", "solana-rent 3.1.0", "solana-sbpf", - "solana-sdk-ids", - "solana-slot-hashes", - "solana-stable-layout", - "solana-stake-interface", + "solana-sdk-ids 3.1.0", + "solana-slot-hashes 3.0.1", + "solana-stable-layout 3.0.1", + "solana-stake-interface 2.0.2", "solana-svm-callback", "solana-svm-feature-set", "solana-svm-log-collector", @@ -9046,8 +9685,8 @@ dependencies = [ "solana-svm-transaction", "solana-svm-type-overrides", "solana-system-interface 2.0.0", - "solana-sysvar", - "solana-sysvar-id", + "solana-sysvar 3.1.1", + "solana-sysvar-id 3.1.0", "solana-transaction-context", "thiserror 2.0.18", ] @@ -9068,55 +9707,81 @@ dependencies = [ "crossbeam-channel", "log", "serde", - "solana-account", - "solana-account-info", + "solana-account 3.4.0", + "solana-account-info 3.1.1", "solana-accounts-db", "solana-banks-client", "solana-banks-interface", "solana-banks-server", - "solana-clock", + "solana-clock 3.0.1", "solana-cluster-type 3.1.0", "solana-commitment-config", "solana-compute-budget", - "solana-epoch-rewards", - "solana-epoch-schedule", - "solana-fee-calculator", + "solana-epoch-rewards 3.0.1", + "solana-epoch-schedule 3.1.0", + "solana-fee-calculator 3.2.0", "solana-genesis-config", "solana-hash 3.1.0", - "solana-instruction", + "solana-instruction 3.4.0", "solana-keypair", - "solana-loader-v3-interface", - "solana-message", - "solana-msg", - "solana-native-token", + "solana-loader-v3-interface 6.1.1", + "solana-message 3.1.0", + "solana-msg 3.1.0", + "solana-native-token 3.0.0", "solana-poh-config", "solana-program-binaries", - "solana-program-entrypoint", - "solana-program-error", + "solana-program-entrypoint 3.1.1", + "solana-program-error 3.0.0", "solana-program-runtime", "solana-pubkey 3.0.0", "solana-rent 3.1.0", "solana-runtime", "solana-sbpf", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-signer", - "solana-stable-layout", - "solana-stake-interface", + "solana-stable-layout 3.0.1", + "solana-stake-interface 2.0.2", "solana-svm", "solana-svm-log-collector", "solana-svm-timings", "solana-system-interface 2.0.0", - "solana-sysvar", - "solana-sysvar-id", + "solana-sysvar 3.1.1", + "solana-sysvar-id 3.1.0", "solana-transaction", "solana-transaction-context", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "solana-vote-program", "spl-generic-token", "thiserror 2.0.18", "tokio", ] +[[package]] +name = "solana-pubkey" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b62adb9c3261a052ca1f999398c388f1daf558a1b492f60a6d9e64857db4ff1" +dependencies = [ + "borsh 0.10.4", + "borsh 1.6.0", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek 4.1.3", + "five8 0.2.1", + "five8_const 0.1.4", + "getrandom 0.2.16", + "js-sys", + "num-traits", + "serde", + "serde_derive", + "solana-atomic-u64 2.2.1", + "solana-decode-error", + "solana-define-syscall 2.3.0", + "solana-sanitize 2.2.1", + "solana-sha256-hasher 2.2.1", + "wasm-bindgen", +] + [[package]] name = "solana-pubkey" version = "3.0.0" @@ -9150,7 +9815,7 @@ dependencies = [ "serde", "serde_json", "solana-account-decoder-client-types", - "solana-clock", + "solana-clock 3.0.1", "solana-pubkey 3.0.0", "solana-rpc-client-types", "solana-signature", @@ -9187,7 +9852,7 @@ dependencies = [ "solana-signer", "solana-streamer", "solana-tls-utils", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "thiserror 2.0.18", "tokio", ] @@ -9211,6 +9876,19 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "solana-rent" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1aea8fdea9de98ca6e8c2da5827707fb3842833521b528a713810ca685d2480" +dependencies = [ + "serde", + "serde_derive", + "solana-sdk-ids 2.2.1", + "solana-sdk-macro 2.2.1", + "solana-sysvar-id 2.2.1", +] + [[package]] name = "solana-rent" version = "3.1.0" @@ -9219,9 +9897,9 @@ checksum = "e860d5499a705369778647e97d760f7670adfb6fc8419dd3d568deccd46d5487" dependencies = [ "serde", "serde_derive", - "solana-sdk-ids", - "solana-sdk-macro", - "solana-sysvar-id", + "solana-sdk-ids 3.1.0", + "solana-sdk-macro 3.0.1", + "solana-sysvar-id 3.1.0", ] [[package]] @@ -9230,7 +9908,7 @@ version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9809b081e99bc142ce803bcd7ee18306759ce3b30a96a9da3f6f41c45e50ef0" dependencies = [ - "solana-sdk-macro", + "solana-sdk-macro 3.0.1", ] [[package]] @@ -9261,25 +9939,25 @@ dependencies = [ "semver", "serde", "serde_json", - "solana-account", + "solana-account 3.4.0", "solana-account-decoder", "solana-account-decoder-client-types", - "solana-clock", + "solana-clock 3.0.1", "solana-commitment-config", "solana-epoch-info", - "solana-epoch-schedule", - "solana-feature-gate-interface", + "solana-epoch-schedule 3.1.0", + "solana-feature-gate-interface 3.1.0", "solana-hash 3.1.0", - "solana-instruction", - "solana-message", + "solana-instruction 3.4.0", + "solana-message 3.1.0", "solana-pubkey 3.0.0", "solana-rpc-client-api", "solana-signature", "solana-transaction", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "solana-transaction-status-client-types", "solana-version", - "solana-vote-interface", + "solana-vote-interface 4.0.4", "tokio", ] @@ -9296,10 +9974,10 @@ dependencies = [ "serde", "serde_json", "solana-account-decoder-client-types", - "solana-clock", + "solana-clock 3.0.1", "solana-rpc-client-types", "solana-signer", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "solana-transaction-status-client-types", "thiserror 2.0.18", ] @@ -9310,14 +9988,14 @@ version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1fc7c6ad3eb7df35a1f97c820003439334ee0d5944b155e9cb78bf9e0ecce36" dependencies = [ - "solana-account", + "solana-account 3.4.0", "solana-commitment-config", "solana-hash 3.1.0", - "solana-message", - "solana-nonce", + "solana-message 3.1.0", + "solana-nonce 3.1.0", "solana-pubkey 3.0.0", "solana-rpc-client", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "thiserror 2.0.18", ] @@ -9332,16 +10010,16 @@ dependencies = [ "semver", "serde", "serde_json", - "solana-account", + "solana-account 3.4.0", "solana-account-decoder-client-types", "solana-address 1.1.0", - "solana-clock", + "solana-clock 3.0.1", "solana-commitment-config", - "solana-fee-calculator", + "solana-fee-calculator 3.2.0", "solana-inflation", "solana-reward-info", "solana-transaction", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "solana-transaction-status-client-types", "solana-version", "spl-generic-token", @@ -9396,16 +10074,16 @@ dependencies = [ "serde", "serde_json", "serde_with", - "solana-account", - "solana-account-info", + "solana-account 3.4.0", + "solana-account-info 3.1.1", "solana-accounts-db", - "solana-address-lookup-table-interface", + "solana-address-lookup-table-interface 3.1.0", "solana-bls-signatures", "solana-bpf-loader-program", "solana-bucket-map", "solana-builtins", "solana-client-traits", - "solana-clock", + "solana-clock 3.0.1", "solana-cluster-type 3.1.0", "solana-commitment-config", "solana-compute-budget", @@ -9413,30 +10091,30 @@ dependencies = [ "solana-compute-budget-interface", "solana-config-interface", "solana-cost-model", - "solana-cpi", + "solana-cpi 3.1.0", "solana-ed25519-program", "solana-epoch-info", "solana-epoch-rewards-hasher", - "solana-epoch-schedule", - "solana-feature-gate-interface", + "solana-epoch-schedule 3.1.0", + "solana-feature-gate-interface 3.1.0", "solana-fee", - "solana-fee-calculator", + "solana-fee-calculator 3.2.0", "solana-fee-structure", "solana-genesis-config", "solana-hard-forks", "solana-hash 3.1.0", "solana-inflation", - "solana-instruction", + "solana-instruction 3.4.0", "solana-keypair", "solana-lattice-hash", - "solana-loader-v3-interface", - "solana-loader-v4-interface", + "solana-loader-v3-interface 6.1.1", + "solana-loader-v4-interface 3.1.0", "solana-measure", - "solana-message", + "solana-message 3.1.0", "solana-metrics 3.1.12", - "solana-native-token", + "solana-native-token 3.0.0", "solana-nohash-hasher", - "solana-nonce", + "solana-nonce 3.1.0", "solana-nonce-account", "solana-packet", "solana-perf", @@ -9448,33 +10126,33 @@ dependencies = [ "solana-rent 3.1.0", "solana-reward-info", "solana-runtime-transaction", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-secp256k1-program", "solana-seed-derivable", "solana-serde", "solana-sha256-hasher 3.1.0", "solana-signature", "solana-signer", - "solana-slot-hashes", - "solana-slot-history", - "solana-stake-interface", + "solana-slot-hashes 3.0.1", + "solana-slot-history 3.0.0", + "solana-stake-interface 2.0.2", "solana-svm", "solana-svm-callback", "solana-svm-timings", "solana-svm-transaction", "solana-system-interface 2.0.0", "solana-system-transaction", - "solana-sysvar", - "solana-sysvar-id", + "solana-sysvar 3.1.1", + "solana-sysvar-id 3.1.0", "solana-time-utils 3.0.0", "solana-transaction", "solana-transaction-context", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "solana-transaction-status-client-types", "solana-unified-scheduler-logic", "solana-version", "solana-vote", - "solana-vote-interface", + "solana-vote-interface 4.0.4", "solana-vote-program", "spl-generic-token", "static_assertions", @@ -9496,14 +10174,14 @@ dependencies = [ "solana-compute-budget", "solana-compute-budget-instruction", "solana-hash 3.1.0", - "solana-message", + "solana-message 3.1.0", "solana-pubkey 3.0.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-signature", "solana-svm-transaction", "solana-transaction", "solana-transaction-context", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "thiserror 2.0.18", ] @@ -9545,35 +10223,44 @@ dependencies = [ "bincode", "bs58", "serde", - "solana-account", + "solana-account 3.4.0", "solana-epoch-info", "solana-epoch-rewards-hasher", "solana-fee-structure", "solana-inflation", "solana-keypair", - "solana-message", + "solana-message 3.1.0", "solana-offchain-message", "solana-presigner", - "solana-program", - "solana-program-memory", + "solana-program 3.0.0", + "solana-program-memory 3.1.0", "solana-pubkey 3.0.0", "solana-sanitize 3.0.1", - "solana-sdk-ids", - "solana-sdk-macro", + "solana-sdk-ids 3.1.0", + "solana-sdk-macro 3.0.1", "solana-seed-derivable", "solana-seed-phrase", "solana-serde", - "solana-serde-varint", - "solana-short-vec", + "solana-serde-varint 3.0.1", + "solana-short-vec 3.2.0", "solana-shred-version", "solana-signature", "solana-signer", "solana-time-utils 3.0.0", "solana-transaction", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "thiserror 2.0.18", ] +[[package]] +name = "solana-sdk-ids" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5d8b9cc68d5c88b062a33e23a6466722467dde0035152d8fb1afbcdf350a5f" +dependencies = [ + "solana-pubkey 2.4.0", +] + [[package]] name = "solana-sdk-ids" version = "3.1.0" @@ -9583,6 +10270,18 @@ dependencies = [ "solana-address 2.6.0", ] +[[package]] +name = "solana-sdk-macro" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86280da8b99d03560f6ab5aca9de2e38805681df34e0bb8f238e69b29433b9df" +dependencies = [ + "bs58", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "solana-sdk-macro" version = "3.0.1" @@ -9609,6 +10308,17 @@ dependencies = [ "solana-signature", ] +[[package]] +name = "solana-secp256k1-recover" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa3120b6cdaa270f39444f5093a90a7b03d296d362878f7a6991d6de3bbe496" +dependencies = [ + "libsecp256k1", + "solana-define-syscall 2.3.0", + "thiserror 2.0.18", +] + [[package]] name = "solana-secp256k1-recover" version = "3.1.1" @@ -9628,8 +10338,8 @@ checksum = "445d8e12592631d76fc4dc57858bae66c9fd7cc838c306c62a472547fc9d0ce6" dependencies = [ "bytemuck", "openssl", - "solana-instruction", - "solana-sdk-ids", + "solana-instruction 3.4.0", + "solana-sdk-ids 3.1.0", ] [[package]] @@ -9663,7 +10373,7 @@ dependencies = [ "itertools 0.12.1", "log", "solana-client", - "solana-clock", + "solana-clock 3.0.1", "solana-connection-cache", "solana-hash 3.1.0", "solana-keypair", @@ -9689,6 +10399,15 @@ dependencies = [ "serde", ] +[[package]] +name = "solana-serde-varint" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a7e155eba458ecfb0107b98236088c3764a09ddf0201ec29e52a0be40857113" +dependencies = [ + "serde", +] + [[package]] name = "solana-serde-varint" version = "3.0.1" @@ -9698,6 +10417,17 @@ dependencies = [ "serde", ] +[[package]] +name = "solana-serialize-utils" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "817a284b63197d2b27afdba829c5ab34231da4a9b4e763466a003c40ca4f535e" +dependencies = [ + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", + "solana-sanitize 2.2.1", +] + [[package]] name = "solana-serialize-utils" version = "3.1.1" @@ -9731,6 +10461,15 @@ dependencies = [ "solana-hash 4.2.0", ] +[[package]] +name = "solana-short-vec" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c54c66f19b9766a56fa0057d060de8378676cb64987533fa088861858fc5a69" +dependencies = [ + "serde", +] + [[package]] name = "solana-short-vec" version = "3.2.0" @@ -9775,7 +10514,20 @@ checksum = "5bfea97951fee8bae0d6038f39a5efcb6230ecdfe33425ac75196d1a1e3e3235" dependencies = [ "solana-pubkey 3.0.0", "solana-signature", - "solana-transaction-error", + "solana-transaction-error 3.1.0", +] + +[[package]] +name = "solana-slot-hashes" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8691982114513763e88d04094c9caa0376b867a29577939011331134c301ce" +dependencies = [ + "serde", + "serde_derive", + "solana-hash 2.3.0", + "solana-sdk-ids 2.2.1", + "solana-sysvar-id 2.2.1", ] [[package]] @@ -9787,8 +10539,21 @@ dependencies = [ "serde", "serde_derive", "solana-hash 4.2.0", - "solana-sdk-ids", - "solana-sysvar-id", + "solana-sdk-ids 3.1.0", + "solana-sysvar-id 3.1.0", +] + +[[package]] +name = "solana-slot-history" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ccc1b2067ca22754d5283afb2b0126d61eae734fc616d23871b0943b0d935e" +dependencies = [ + "bv", + "serde", + "serde_derive", + "solana-sdk-ids 2.2.1", + "solana-sysvar-id 2.2.1", ] [[package]] @@ -9800,8 +10565,18 @@ dependencies = [ "bv", "serde", "serde_derive", - "solana-sdk-ids", - "solana-sysvar-id", + "solana-sdk-ids 3.1.0", + "solana-sysvar-id 3.1.0", +] + +[[package]] +name = "solana-stable-layout" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f14f7d02af8f2bc1b5efeeae71bc1c2b7f0f65cd75bcc7d8180f2c762a57f54" +dependencies = [ + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", ] [[package]] @@ -9810,10 +10585,29 @@ version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9f6a291ba063a37780af29e7db14bdd3dc447584d8ba5b3fc4b88e2bbc982fa" dependencies = [ - "solana-instruction", + "solana-instruction 3.4.0", "solana-pubkey 4.2.0", ] +[[package]] +name = "solana-stake-interface" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5269e89fde216b4d7e1d1739cf5303f8398a1ff372a81232abbee80e554a838c" +dependencies = [ + "num-traits", + "serde", + "serde_derive", + "solana-clock 2.2.3", + "solana-cpi 2.2.1", + "solana-decode-error", + "solana-instruction 2.3.3", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", + "solana-system-interface 1.0.0", + "solana-sysvar-id 2.2.1", +] + [[package]] name = "solana-stake-interface" version = "2.0.2" @@ -9823,14 +10617,14 @@ dependencies = [ "num-traits", "serde", "serde_derive", - "solana-clock", - "solana-cpi", - "solana-instruction", - "solana-program-error", + "solana-clock 3.0.1", + "solana-cpi 3.1.0", + "solana-instruction 3.4.0", + "solana-program-error 3.0.0", "solana-pubkey 3.0.0", "solana-system-interface 2.0.0", - "solana-sysvar", - "solana-sysvar-id", + "solana-sysvar 3.1.1", + "solana-sysvar-id 3.1.0", ] [[package]] @@ -9844,12 +10638,12 @@ dependencies = [ "serde", "solana-account-decoder", "solana-hash 3.1.0", - "solana-instruction", - "solana-message", + "solana-instruction 3.4.0", + "solana-message 3.1.0", "solana-pubkey 3.0.0", "solana-signature", "solana-transaction", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "solana-transaction-status", "tonic-prost-build", ] @@ -9876,7 +10670,7 @@ dependencies = [ "log", "nix", "num_cpus", - "pem", + "pem 1.1.1", "percentage", "rand 0.8.5", "rustls 0.23.35", @@ -9894,7 +10688,7 @@ dependencies = [ "solana-signer", "solana-time-utils 3.0.0", "solana-tls-utils", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "solana-transaction-metrics-tracker", "thiserror 2.0.18", "tokio", @@ -9912,24 +10706,24 @@ dependencies = [ "percentage", "qualifier_attr", "serde", - "solana-account", - "solana-clock", + "solana-account 3.4.0", + "solana-clock 3.0.1", "solana-fee-structure", "solana-hash 3.1.0", - "solana-instruction", - "solana-instructions-sysvar", - "solana-loader-v3-interface", - "solana-loader-v4-interface", + "solana-instruction 3.4.0", + "solana-instructions-sysvar 3.0.0", + "solana-loader-v3-interface 6.1.1", + "solana-loader-v4-interface 3.1.0", "solana-loader-v4-program", - "solana-message", - "solana-nonce", + "solana-message 3.1.0", + "solana-nonce 3.1.0", "solana-nonce-account", - "solana-program-entrypoint", - "solana-program-pack", + "solana-program-entrypoint 3.1.1", + "solana-program-pack 3.1.0", "solana-program-runtime", "solana-pubkey 3.0.0", "solana-rent 3.1.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-svm-callback", "solana-svm-feature-set", "solana-svm-log-collector", @@ -9938,9 +10732,9 @@ dependencies = [ "solana-svm-transaction", "solana-svm-type-overrides", "solana-system-interface 2.0.0", - "solana-sysvar-id", + "solana-sysvar-id 3.1.0", "solana-transaction-context", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "spl-generic-token", "thiserror 2.0.18", ] @@ -9951,8 +10745,8 @@ version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb521c7f62db21661267a933f0d311a76b2b744a766b46f5a9a9395ce70f687c" dependencies = [ - "solana-account", - "solana-clock", + "solana-account 3.4.0", + "solana-clock 3.0.1", "solana-precompile-error", "solana-pubkey 3.0.0", ] @@ -9996,9 +10790,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ef6ff55ce4c24e26ad8b0e67bc604cbd54eabfc94540c4c2c93e51fa087ead5" dependencies = [ "solana-hash 3.1.0", - "solana-message", + "solana-message 3.1.0", "solana-pubkey 3.0.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-signature", "solana-transaction", ] @@ -10012,6 +10806,22 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "solana-system-interface" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d7c18cb1a91c6be5f5a8ac9276a1d7c737e39a21beba9ea710ab4b9c63bc90" +dependencies = [ + "js-sys", + "num-traits", + "serde", + "serde_derive", + "solana-decode-error", + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", + "wasm-bindgen", +] + [[package]] name = "solana-system-interface" version = "2.0.0" @@ -10021,9 +10831,9 @@ dependencies = [ "num-traits", "serde", "serde_derive", - "solana-instruction", - "solana-msg", - "solana-program-error", + "solana-instruction 3.4.0", + "solana-msg 3.1.0", + "solana-program-error 3.0.0", "solana-pubkey 3.0.0", ] @@ -10037,9 +10847,9 @@ dependencies = [ "serde", "serde_derive", "solana-address 2.6.0", - "solana-instruction", - "solana-msg", - "solana-program-error", + "solana-instruction 3.4.0", + "solana-msg 3.1.0", + "solana-program-error 3.0.0", ] [[package]] @@ -10051,20 +10861,20 @@ dependencies = [ "bincode", "log", "serde", - "solana-account", - "solana-bincode", - "solana-fee-calculator", - "solana-instruction", - "solana-nonce", + "solana-account 3.4.0", + "solana-bincode 3.1.0", + "solana-fee-calculator 3.2.0", + "solana-instruction 3.4.0", + "solana-nonce 3.1.0", "solana-nonce-account", "solana-packet", "solana-program-runtime", "solana-pubkey 3.0.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-svm-log-collector", "solana-svm-type-overrides", "solana-system-interface 2.0.0", - "solana-sysvar", + "solana-sysvar 3.1.1", "solana-transaction-context", ] @@ -10076,13 +10886,50 @@ checksum = "a31b5699ec533621515e714f1533ee6b3b0e71c463301d919eb59b8c1e249d30" dependencies = [ "solana-hash 3.1.0", "solana-keypair", - "solana-message", + "solana-message 3.1.0", "solana-pubkey 3.0.0", "solana-signer", "solana-system-interface 2.0.0", "solana-transaction", ] +[[package]] +name = "solana-sysvar" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c3595f95069f3d90f275bb9bd235a1973c4d059028b0a7f81baca2703815db" +dependencies = [ + "base64 0.22.1", + "bincode", + "bytemuck", + "bytemuck_derive", + "lazy_static", + "serde", + "serde_derive", + "solana-account-info 2.3.0", + "solana-clock 2.2.3", + "solana-define-syscall 2.3.0", + "solana-epoch-rewards 2.2.1", + "solana-epoch-schedule 2.2.1", + "solana-fee-calculator 2.2.1", + "solana-hash 2.3.0", + "solana-instruction 2.3.3", + "solana-instructions-sysvar 2.2.2", + "solana-last-restart-slot 2.2.1", + "solana-program-entrypoint 2.3.0", + "solana-program-error 2.2.2", + "solana-program-memory 2.3.1", + "solana-pubkey 2.4.0", + "solana-rent 2.2.1", + "solana-sanitize 2.2.1", + "solana-sdk-ids 2.2.1", + "solana-sdk-macro 2.2.1", + "solana-slot-hashes 2.2.1", + "solana-slot-history 2.2.1", + "solana-stake-interface 1.2.1", + "solana-sysvar-id 2.2.1", +] + [[package]] name = "solana-sysvar" version = "3.1.1" @@ -10096,25 +10943,35 @@ dependencies = [ "lazy_static", "serde", "serde_derive", - "solana-account-info", - "solana-clock", + "solana-account-info 3.1.1", + "solana-clock 3.0.1", "solana-define-syscall 4.0.1", - "solana-epoch-rewards", - "solana-epoch-schedule", - "solana-fee-calculator", + "solana-epoch-rewards 3.0.1", + "solana-epoch-schedule 3.1.0", + "solana-fee-calculator 3.2.0", "solana-hash 4.2.0", - "solana-instruction", - "solana-last-restart-slot", - "solana-program-entrypoint", - "solana-program-error", - "solana-program-memory", + "solana-instruction 3.4.0", + "solana-last-restart-slot 3.0.0", + "solana-program-entrypoint 3.1.1", + "solana-program-error 3.0.0", + "solana-program-memory 3.1.0", "solana-pubkey 4.2.0", "solana-rent 3.1.0", - "solana-sdk-ids", - "solana-sdk-macro", - "solana-slot-hashes", - "solana-slot-history", - "solana-sysvar-id", + "solana-sdk-ids 3.1.0", + "solana-sdk-macro 3.0.1", + "solana-slot-hashes 3.0.1", + "solana-slot-history 3.0.0", + "solana-sysvar-id 3.1.0", +] + +[[package]] +name = "solana-sysvar-id" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5762b273d3325b047cfda250787f8d796d781746860d5d0a746ee29f3e8812c1" +dependencies = [ + "solana-pubkey 2.4.0", + "solana-sdk-ids 2.2.1", ] [[package]] @@ -10124,7 +10981,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17358d1e9a13e5b9c2264d301102126cf11a47fd394cdf3dec174fe7bc96e1de" dependencies = [ "solana-address 2.6.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", ] [[package]] @@ -10166,12 +11023,12 @@ dependencies = [ "log", "rayon", "solana-client-traits", - "solana-clock", + "solana-clock 3.0.1", "solana-commitment-config", "solana-connection-cache", - "solana-epoch-schedule", + "solana-epoch-schedule 3.1.0", "solana-measure", - "solana-message", + "solana-message 3.1.0", "solana-net-utils", "solana-pubkey 3.0.0", "solana-pubsub-client", @@ -10181,7 +11038,7 @@ dependencies = [ "solana-signature", "solana-signer", "solana-transaction", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "thiserror 2.0.18", "tokio", ] @@ -10197,7 +11054,7 @@ dependencies = [ "log", "lru 0.7.8", "rustls 0.23.35", - "solana-clock", + "solana-clock 3.0.1", "solana-connection-cache", "solana-keypair", "solana-measure", @@ -10224,15 +11081,15 @@ dependencies = [ "serde_derive", "solana-address 2.6.0", "solana-hash 4.2.0", - "solana-instruction", + "solana-instruction 3.4.0", "solana-instruction-error", - "solana-message", + "solana-message 3.1.0", "solana-sanitize 3.0.1", - "solana-sdk-ids", - "solana-short-vec", + "solana-sdk-ids 3.1.0", + "solana-short-vec 3.2.0", "solana-signature", "solana-signer", - "solana-transaction-error", + "solana-transaction-error 3.1.0", ] [[package]] @@ -10243,13 +11100,23 @@ dependencies = [ "bincode", "qualifier_attr", "serde", - "solana-account", - "solana-instruction", - "solana-instructions-sysvar", + "solana-account 3.4.0", + "solana-instruction 3.4.0", + "solana-instructions-sysvar 3.0.0", "solana-pubkey 3.0.0", "solana-rent 3.1.0", "solana-sbpf", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", +] + +[[package]] +name = "solana-transaction-error" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a9dc8fdb61c6088baab34fc3a8b8473a03a7a5fd404ed8dd502fa79b67cb1" +dependencies = [ + "solana-instruction 2.3.3", + "solana-sanitize 2.2.1", ] [[package]] @@ -10276,7 +11143,7 @@ dependencies = [ "rand 0.8.5", "solana-packet", "solana-perf", - "solana-short-vec", + "solana-short-vec 3.2.0", "solana-signature", ] @@ -10290,30 +11157,30 @@ dependencies = [ "agave-reserved-account-keys", "base64 0.22.1", "bincode", - "borsh", + "borsh 1.6.0", "bs58", "log", "serde", "serde_json", "solana-account-decoder", - "solana-address-lookup-table-interface", - "solana-clock", + "solana-address-lookup-table-interface 3.1.0", + "solana-clock 3.0.1", "solana-hash 3.1.0", - "solana-instruction", - "solana-loader-v2-interface", - "solana-loader-v3-interface", - "solana-message", - "solana-program-option", + "solana-instruction 3.4.0", + "solana-loader-v2-interface 3.0.0", + "solana-loader-v3-interface 6.1.1", + "solana-message 3.1.0", + "solana-program-option 3.1.0", "solana-pubkey 3.0.0", "solana-reward-info", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-signature", - "solana-stake-interface", + "solana-stake-interface 2.0.2", "solana-system-interface 2.0.0", "solana-transaction", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "solana-transaction-status-client-types", - "solana-vote-interface", + "solana-vote-interface 4.0.4", "spl-associated-token-account-interface", "spl-memo-interface", "spl-token-2022-interface", @@ -10336,14 +11203,14 @@ dependencies = [ "serde_json", "solana-account-decoder-client-types", "solana-commitment-config", - "solana-instruction", - "solana-message", + "solana-instruction 3.4.0", + "solana-message 3.1.0", "solana-pubkey 3.0.0", "solana-reward-info", "solana-signature", "solana-transaction", "solana-transaction-context", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "thiserror 2.0.18", ] @@ -10358,7 +11225,7 @@ dependencies = [ "solana-keypair", "solana-net-utils", "solana-streamer", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "thiserror 2.0.18", "tokio", ] @@ -10388,7 +11255,7 @@ dependencies = [ "semver", "serde", "solana-sanitize 3.0.1", - "solana-serde-varint", + "solana-serde-varint 3.0.1", ] [[package]] @@ -10400,24 +11267,48 @@ dependencies = [ "itertools 0.12.1", "log", "serde", - "solana-account", - "solana-bincode", - "solana-clock", + "solana-account 3.4.0", + "solana-bincode 3.1.0", + "solana-clock 3.0.1", "solana-hash 3.1.0", - "solana-instruction", + "solana-instruction 3.4.0", "solana-keypair", "solana-packet", "solana-pubkey 3.0.0", - "solana-sdk-ids", - "solana-serialize-utils", + "solana-sdk-ids 3.1.0", + "solana-serialize-utils 3.1.1", "solana-signature", "solana-signer", "solana-svm-transaction", "solana-transaction", - "solana-vote-interface", + "solana-vote-interface 4.0.4", "thiserror 2.0.18", ] +[[package]] +name = "solana-vote-interface" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b80d57478d6599d30acc31cc5ae7f93ec2361a06aefe8ea79bc81739a08af4c3" +dependencies = [ + "bincode", + "num-derive", + "num-traits", + "serde", + "serde_derive", + "solana-clock 2.2.3", + "solana-decode-error", + "solana-hash 2.3.0", + "solana-instruction 2.3.3", + "solana-pubkey 2.4.0", + "solana-rent 2.2.1", + "solana-sdk-ids 2.2.1", + "solana-serde-varint 2.2.2", + "solana-serialize-utils 2.2.1", + "solana-short-vec 2.2.1", + "solana-system-interface 1.0.0", +] + [[package]] name = "solana-vote-interface" version = "4.0.4" @@ -10431,16 +11322,16 @@ dependencies = [ "serde", "serde_derive", "serde_with", - "solana-clock", + "solana-clock 3.0.1", "solana-hash 3.1.0", - "solana-instruction", + "solana-instruction 3.4.0", "solana-instruction-error", "solana-pubkey 3.0.0", "solana-rent 3.1.0", - "solana-sdk-ids", - "solana-serde-varint", - "solana-serialize-utils", - "solana-short-vec", + "solana-sdk-ids 3.1.0", + "solana-serde-varint 3.0.1", + "solana-serialize-utils 3.1.1", + "solana-short-vec 3.2.0", "solana-system-interface 2.0.0", ] @@ -10456,23 +11347,23 @@ dependencies = [ "num-derive", "num-traits", "serde", - "solana-account", - "solana-bincode", - "solana-clock", - "solana-epoch-schedule", + "solana-account 3.4.0", + "solana-bincode 3.1.0", + "solana-clock 3.0.1", + "solana-epoch-schedule 3.1.0", "solana-hash 3.1.0", - "solana-instruction", + "solana-instruction 3.4.0", "solana-keypair", "solana-packet", "solana-program-runtime", "solana-pubkey 3.0.0", "solana-rent 3.1.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-signer", - "solana-slot-hashes", + "solana-slot-hashes 3.0.1", "solana-transaction", "solana-transaction-context", - "solana-vote-interface", + "solana-vote-interface 4.0.4", "thiserror 2.0.18", ] @@ -10482,7 +11373,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5a91404c7de468dd80658cdb5d894ec803d1092ea6e2bfdf84eee6f07559c0d" dependencies = [ - "borsh", + "borsh 1.6.0", "bytemuck", "bytemuck_derive", ] @@ -10497,9 +11388,9 @@ dependencies = [ "bytemuck", "num-derive", "num-traits", - "solana-instruction", + "solana-instruction 3.4.0", "solana-program-runtime", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-svm-log-collector", "solana-zk-sdk", ] @@ -10528,9 +11419,9 @@ dependencies = [ "serde_json", "sha3", "solana-derivation-path", - "solana-instruction", + "solana-instruction 3.4.0", "solana-pubkey 3.0.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-seed-derivable", "solana-seed-phrase", "solana-signature", @@ -10551,9 +11442,9 @@ dependencies = [ "bytemuck", "num-derive", "num-traits", - "solana-instruction", + "solana-instruction 3.4.0", "solana-program-runtime", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-svm-log-collector", "solana-zk-token-sdk", ] @@ -10580,9 +11471,9 @@ dependencies = [ "sha3", "solana-curve25519", "solana-derivation-path", - "solana-instruction", + "solana-instruction 3.4.0", "solana-pubkey 3.0.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-seed-derivable", "solana-seed-phrase", "solana-signature", @@ -10665,8 +11556,8 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6433917b60441d68d99a17e121d9db0ea15a9a69c0e5afa34649cf5ba12612f" dependencies = [ - "borsh", - "solana-instruction", + "borsh 1.6.0", + "solana-instruction 3.4.0", "solana-pubkey 3.0.0", ] @@ -10677,7 +11568,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e597c5ff9ed7c74a54dbc47bae2f06e4db8c98f4356ad280200dc11878266db1" dependencies = [ "bytemuck", - "solana-program-error", + "solana-program-error 3.0.0", "solana-sha256-hasher 3.1.0", "spl-discriminator-derive", ] @@ -10722,7 +11613,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d4e2aedd58f858337fa609af5ad7100d4a243fdaf6a40d6eb4c28c5f19505d3" dependencies = [ - "solana-instruction", + "solana-instruction 3.4.0", "solana-pubkey 3.0.0", ] @@ -10732,14 +11623,14 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f9c6e142cdf1e7e77f480053ec9f0ce989890768ddf91f619b50f39d1b456f5" dependencies = [ - "borsh", + "borsh 1.6.0", "bytemuck", "bytemuck_derive", "num-derive", "num-traits", "num_enum", - "solana-program-error", - "solana-program-option", + "solana-program-error 3.0.0", + "solana-program-option 3.1.0", "solana-pubkey 3.0.0", "solana-zero-copy", "solana-zk-sdk", @@ -10757,13 +11648,13 @@ dependencies = [ "num-derive", "num-traits", "num_enum", - "solana-account-info", - "solana-instruction", - "solana-program-error", - "solana-program-option", - "solana-program-pack", + "solana-account-info 3.1.1", + "solana-instruction 3.4.0", + "solana-program-error 3.0.0", + "solana-program-option 3.1.0", + "solana-program-pack 3.1.0", "solana-pubkey 3.0.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-zk-sdk", "spl-pod", "spl-token-confidential-transfer-proof-extraction", @@ -10781,14 +11672,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879a9ebad0d77383d3ea71e7de50503554961ff0f4ef6cbca39ad126e6f6da3a" dependencies = [ "bytemuck", - "solana-account-info", + "solana-account-info 3.1.1", "solana-curve25519", - "solana-instruction", - "solana-instructions-sysvar", - "solana-msg", - "solana-program-error", + "solana-instruction 3.4.0", + "solana-instructions-sysvar 3.0.0", + "solana-msg 3.1.0", + "solana-program-error 3.0.0", "solana-pubkey 3.0.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-zk-sdk", "spl-pod", "thiserror 2.0.18", @@ -10816,9 +11707,9 @@ dependencies = [ "num-traits", "num_enum", "solana-address 2.6.0", - "solana-instruction", + "solana-instruction 3.4.0", "solana-nullable", - "solana-program-error", + "solana-program-error 3.0.0", "solana-zero-copy", "spl-discriminator", "thiserror 2.0.18", @@ -10835,12 +11726,12 @@ dependencies = [ "num-derive", "num-traits", "num_enum", - "solana-instruction", - "solana-program-error", - "solana-program-option", - "solana-program-pack", + "solana-instruction 3.4.0", + "solana-program-error 3.0.0", + "solana-program-option 3.1.0", + "solana-program-pack 3.1.0", "solana-pubkey 3.0.0", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "thiserror 2.0.18", ] @@ -10850,12 +11741,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c467c7c3bd056f8fe60119e7ec34ddd6f23052c2fa8f1f51999098063b72676" dependencies = [ - "borsh", + "borsh 1.6.0", "num-derive", "num-traits", "solana-borsh", - "solana-instruction", - "solana-program-error", + "solana-instruction 3.4.0", + "solana-program-error 3.0.0", "solana-pubkey 3.0.0", "spl-discriminator", "spl-pod", @@ -10873,8 +11764,8 @@ dependencies = [ "num-derive", "num-traits", "num_enum", - "solana-account-info", - "solana-program-error", + "solana-account-info 3.1.1", + "solana-program-error 3.0.0", "solana-zero-copy", "spl-discriminator", "thiserror 2.0.18", @@ -11038,7 +11929,7 @@ dependencies = [ name = "sysvars" version = "0.0.0" dependencies = [ - "solana-program", + "solana-program 3.0.0", ] [[package]] @@ -11136,19 +12027,19 @@ dependencies = [ "integration-test-tools", "magicblock-chainlink", "magicblock-config", - "magicblock-delegation-program-api 0.3.0 (git+https://github.com/magicblock-labs/delegation-program.git?rev=25386a7c1d406d06b8d07a4d5b0fd37d5e74213b)", + "magicblock-delegation-program-api 0.3.0", "program-flexi-counter", "program-mini", - "solana-account", + "solana-account 3.4.0", "solana-commitment-config", - "solana-loader-v2-interface", - "solana-loader-v3-interface", - "solana-loader-v4-interface", + "solana-loader-v2-interface 3.0.0", + "solana-loader-v3-interface 6.1.1", + "solana-loader-v4-interface 3.1.0", "solana-pubkey 3.0.0", "solana-rpc-client", "solana-rpc-client-api", "solana-sdk", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-system-interface 3.2.0", "spl-token-interface", "tokio", @@ -11161,10 +12052,10 @@ version = "0.0.0" dependencies = [ "integration-test-tools", "magicblock-core", - "magicblock-delegation-program-api 0.3.0 (git+https://github.com/magicblock-labs/delegation-program.git?rev=25386a7c1d406d06b8d07a4d5b0fd37d5e74213b)", + "magicblock-delegation-program-api 0.3.0", "program-flexi-counter", "program-mini", - "solana-loader-v4-interface", + "solana-loader-v4-interface 3.1.0", "solana-sdk", "solana-system-interface 3.2.0", "spl-associated-token-account-interface", @@ -11188,7 +12079,7 @@ dependencies = [ "serial_test", "solana-rpc-client", "solana-sdk", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-system-interface 3.2.0", "tempfile", "test-kit", @@ -11204,15 +12095,15 @@ dependencies = [ "magicblock-core", "magicblock-ledger", "magicblock-processor", - "solana-account", - "solana-instruction", + "solana-account 3.4.0", + "solana-instruction 3.4.0", "solana-keypair", - "solana-program", + "solana-program 3.0.0", "solana-rpc-client", "solana-signature", "solana-signer", "solana-transaction", - "solana-transaction-error", + "solana-transaction-error 3.1.0", "solana-transaction-status-client-types", "tempfile", "tokio", @@ -11231,12 +12122,12 @@ dependencies = [ "integration-test-tools", "magicblock-accounts-db", "magicblock-config", - "magicblock-delegation-program-api 0.3.0 (git+https://github.com/magicblock-labs/delegation-program.git?rev=25386a7c1d406d06b8d07a4d5b0fd37d5e74213b)", + "magicblock-delegation-program-api 0.3.0", "program-flexi-counter", "solana-commitment-config", "solana-rpc-client", "solana-sdk", - "solana-sdk-ids", + "solana-sdk-ids 3.1.0", "solana-system-interface 3.2.0", "solana-transaction-status", "tempfile", @@ -11254,7 +12145,7 @@ dependencies = [ "magic-domain-program", "magicblock-api", "magicblock-config", - "magicblock-delegation-program-api 0.3.0 (git+https://github.com/magicblock-labs/delegation-program.git?rev=25386a7c1d406d06b8d07a4d5b0fd37d5e74213b)", + "magicblock-delegation-program-api 0.3.0", "magicblock-program", "magicblock-validator-admin", "solana-commitment-config", @@ -11283,6 +12174,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "test-query-filtering" +version = "0.0.0" +dependencies = [ + "anyhow", + "borsh 1.6.0", + "integration-test-tools", + "magicblock-query-filtering", + "program-flexi-counter", + "serde", + "serde_json", + "solana-commitment-config", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-sdk", + "solana-transaction-status", + "ureq 2.12.1", +] + [[package]] name = "test-runner" version = "0.0.0" @@ -11299,7 +12209,7 @@ dependencies = [ "ephemeral-rollups-sdk", "integration-test-tools", "log", - "magicblock-delegation-program-api 0.3.0 (git+https://github.com/magicblock-labs/delegation-program.git?rev=25386a7c1d406d06b8d07a4d5b0fd37d5e74213b)", + "magicblock-delegation-program-api 0.3.0", "magicblock-magic-program-api 0.11.3", "program-flexi-counter", "solana-rpc-client-api", @@ -11315,7 +12225,7 @@ dependencies = [ "magicblock-rpc-client", "magicblock-table-mania", "paste", - "solana-address-lookup-table-interface", + "solana-address-lookup-table-interface 3.1.0", "solana-commitment-config", "solana-pubkey 3.0.0", "solana-rpc-client", @@ -11405,30 +12315,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -11614,6 +12524,15 @@ dependencies = [ "webpki-roots 0.26.11", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.8.23" @@ -12109,6 +13028,8 @@ dependencies = [ "once_cell", "rustls 0.23.35", "rustls-pki-types", + "serde", + "serde_json", "url", "webpki-roots 0.26.11", ] @@ -12386,7 +13307,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/test-integration/Cargo.toml b/test-integration/Cargo.toml index b97350f47..6defab8a3 100644 --- a/test-integration/Cargo.toml +++ b/test-integration/Cargo.toml @@ -14,12 +14,13 @@ members = [ "test-config", "test-ledger-restore", "test-magicblock-api", + "test-query-filtering", "test-pubsub", "test-runner", "test-schedule-intent", "test-table-mania", "test-task-scheduler", - "test-tools" + "test-tools", ] resolver = "2" @@ -36,20 +37,24 @@ chrono = "0.4" cleanass = "0.0.1" color-backtrace = { version = "0.7" } ctrlc = "3.4.7" -ephemeral-rollups-sdk = { git = "https://github.com/magicblock-labs/ephemeral-rollups-sdk.git", rev = "ccfc9f924dc40", default-features = false, features = [ - "disable-realloc", - "modular-sdk" +ephemeral-rollups-sdk = { git = "https://github.com/magicblock-labs/ephemeral-rollups-sdk.git", tag = "v0.14.3", default-features = false, features = [ + "access-control", + "modular-sdk", ] } futures = "0.3.31" integration-test-tools = { path = "test-tools" } isocountry = "0.3.2" lazy_static = "1.4.0" magic-domain-program = { git = "https://github.com/magicblock-labs/magic-domain-program.git", rev = "335a22", default-features = false } -magicblock-accounts-db = { path = "../magicblock-accounts-db", features = ["dev-tools"] } +magicblock-accounts-db = { path = "../magicblock-accounts-db", features = [ + "dev-tools", +] } magicblock-api = { path = "../magicblock-api" } -magicblock-chainlink = { path = "../magicblock-chainlink", features = ["dev-context"] } +magicblock-chainlink = { path = "../magicblock-chainlink", features = [ + "dev-context", +] } magicblock-committor-program = { path = "../magicblock-committor-program", features = [ - "no-entrypoint" + "no-entrypoint", ] } magicblock-committor-service = { path = "../magicblock-committor-service" } magicblock-config = { path = "../magicblock-config" } @@ -57,6 +62,7 @@ magicblock-core = { path = "../magicblock-core" } magicblock-delegation-program-api = { git = "https://github.com/magicblock-labs/delegation-program.git", rev = "25386a7c1d406d06b8d07a4d5b0fd37d5e74213b", default-features = false } magicblock-magic-program-api = { path = "../magicblock-magic-program-api" } magicblock-program = { path = "../programs/magicblock" } +magicblock-query-filtering = { path = "../magicblock-query-filtering" } magicblock-rpc-client = { path = "../magicblock-rpc-client" } magicblock-table-mania = { path = "../magicblock-table-mania" } paste = "1.0" @@ -85,7 +91,7 @@ solana-rpc-client = "3.1.12" solana-rpc-client-api = "3.1.12" solana-sdk = "3.0" solana-sdk-ids = { version = "3.0" } -solana-system-interface = "3.1" +solana-system-interface = { version = "3.1", features = ["bincode"] } solana-transaction-status = "3.1.12" spl-associated-token-account-interface = "2.0" spl-memo-interface = "2.0" diff --git a/test-integration/Makefile b/test-integration/Makefile index 25e1ca4f5..61c539e35 100644 --- a/test-integration/Makefile +++ b/test-integration/Makefile @@ -100,6 +100,25 @@ setup-cloning-both: SETUP_ONLY=both \ $(MAKE) test +test-query-filtering: + RUN_TESTS=query_filtering \ + $(MAKE) test +setup-query-filtering-devnet: + RUST_LOG_STYLE=none \ + RUN_TESTS=query_filtering \ + SETUP_ONLY=devnet \ + $(MAKE) test +setup-query-filtering-ephem: + RUST_LOG_STYLE=none \ + RUN_TESTS=query_filtering \ + SETUP_ONLY=ephem \ + $(MAKE) test +setup-query-filtering-both: + RUST_LOG_STYLE=none \ + RUN_TESTS=query_filtering \ + SETUP_ONLY=both \ + $(MAKE) test + test-restore-ledger: RUN_TESTS=restore_ledger \ $(MAKE) test @@ -286,6 +305,9 @@ ci-lint: lint setup-pubsub-both \ setup-pubsub-devnet \ setup-pubsub-ephem \ + setup-query-filtering-both \ + setup-query-filtering-devnet \ + setup-query-filtering-ephem \ setup-restore-ledger-devnet \ setup-schedule-intents-both \ setup-schedule-intents-devnet \ @@ -312,6 +334,7 @@ ci-lint: lint test-force-mb \ test-magicblock-api \ test-pubsub \ + test-query-filtering \ test-restore-ledger \ test-schedule-intents \ test-schedulecommit \ diff --git a/test-integration/configs/query-filtering-conf.devnet.toml b/test-integration/configs/query-filtering-conf.devnet.toml new file mode 100644 index 000000000..f588b91fc --- /dev/null +++ b/test-integration/configs/query-filtering-conf.devnet.toml @@ -0,0 +1,37 @@ +lifecycle = "offline" +remotes = ["devnet"] + +[aperture] +listen = "0.0.0.0:7799" + +[commit] +compute-unit-price = 1_000_000 + +[accountsdb] +database-size = 1048576000 +block-size = "block256" +index-size = 20485760 +max-snapshots = 7 + +[ledger] +reset = true +block-time = "50ms" + +[[programs]] +id = "ACLseoPoyC3cBqoUtkbjZ4aDrkurZW86v19pXz2XQnp1" +path = "../programs/acl/acl.so" + +[[programs]] +id = "DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh" +path = "../schedulecommit/elfs/dlp.so" + +[[programs]] +id = "DmnRGfyyftzacFb1XadYhWF6vWqXwtQk5tbr6XgR3BA1" +path = "../schedulecommit/elfs/mdp.so" + +[[programs]] +id = "f1exzKGtdeVX3d6UXZ89cY7twiNJe9S5uq84RTA4Rq4" +path = "../target/deploy/program_flexi_counter.so" + +[metrics] +address = "0.0.0.0:9000" diff --git a/test-integration/configs/query-filtering-conf.ephem.toml b/test-integration/configs/query-filtering-conf.ephem.toml new file mode 100644 index 000000000..86a0aaebc --- /dev/null +++ b/test-integration/configs/query-filtering-conf.ephem.toml @@ -0,0 +1,30 @@ +lifecycle = "ephemeral" +remotes = ["http://0.0.0.0:7799", "ws://0.0.0.0:7800"] + +[aperture] +listen = "0.0.0.0:8899" + +[query-filtering] +enabled = true +jwt-secret = "query-filtering-integration-secret" +token-expiry-days = 1 +challenge-ttl-seconds = 300 + +[commit] +compute-unit-price = 1_000_000 + +[chainlink] +max-monitored-accounts = 300 + +[accountsdb] +database-size = 1048576000 +block-size = "block256" +index-size = 2048576 +max-snapshots = 7 +reset = true + +[ledger] +reset = true + +[metrics] +address = "0.0.0.0:9000" diff --git a/test-integration/programs/acl/acl.so b/test-integration/programs/acl/acl.so new file mode 100644 index 000000000..6b2b993a5 Binary files /dev/null and b/test-integration/programs/acl/acl.so differ diff --git a/test-integration/programs/flexi-counter/src/instruction.rs b/test-integration/programs/flexi-counter/src/instruction.rs index 795458892..ea0df3910 100644 --- a/test-integration/programs/flexi-counter/src/instruction.rs +++ b/test-integration/programs/flexi-counter/src/instruction.rs @@ -1,6 +1,9 @@ use borsh::{BorshDeserialize, BorshSerialize}; use ephemeral_rollups_sdk::{ - consts::{MAGIC_CONTEXT_ID, MAGIC_PROGRAM_ID}, + access_control::structs::{ + Member as PermissionMember, Permission as PermissionAccount, + }, + consts::{EPHEMERAL_VAULT_ID, MAGIC_CONTEXT_ID, MAGIC_PROGRAM_ID}, delegate_args::{DelegateAccountMetas, DelegateAccounts}, dlp_api, }; @@ -32,6 +35,12 @@ pub struct CancelArgs { pub task_id: i64, } +#[derive(BorshSerialize, BorshDeserialize, Debug, Clone)] +pub struct PermissionMemberArgs { + pub pubkey: Pubkey, + pub flags: 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 @@ -107,6 +116,32 @@ pub enum FlexiCounterInstruction { /// 7. `[]` The system program Delegate(DelegateArgs), + /// Delegates the FlexiCounter permission account to the ephemeral validator. + /// + /// Accounts: + /// 0. `[signer, write]` The payer that owns the counter. + /// 1. `[]` The counter PDA that signs the permission CPI. + /// 2. `[write]` The permission PDA derived from the counter PDA. + /// 3. `[]` The permission program. + /// 4. `[]` The magic program. + /// 5. `[write]` The ephemeral vault. + CreatePermission { + members: Option>, + }, + + /// Creates or replaces a permission account for the FlexiCounter PDA. + /// + /// Accounts: + /// 0. `[signer, write]` The payer that owns the counter. + /// 1. `[]` The counter PDA that signs the permission CPI. + /// 2. `[write]` The permission PDA derived from the counter PDA. + /// 3. `[]` The permission program. + /// 4. `[]` The magic program. + /// 5. `[write]` The ephemeral vault. + UpdatePermission { + members: Option>, + }, + /// Updates the FlexiCounter by adding the count to it and then /// commits its current state, optionally undelegating the account. /// @@ -390,6 +425,68 @@ pub fn create_delegate_ix_with_commit_frequency_ms( ) } +pub fn create_create_permission_ix( + payer: Pubkey, + members: Option>, +) -> Instruction { + let program_id = &crate::id(); + let (counter, _) = FlexiCounter::pda(&payer); + let (permission, _) = PermissionAccount::find_pda(&counter); + + let accounts = vec![ + AccountMeta::new(payer, true), + AccountMeta::new(counter, false), + AccountMeta::new(permission, false), + AccountMeta::new_readonly( + ephemeral_rollups_sdk::consts::PERMISSION_PROGRAM_ID, + false, + ), + AccountMeta::new_readonly(MAGIC_PROGRAM_ID, false), + AccountMeta::new(EPHEMERAL_VAULT_ID, false), + ]; + + Instruction::new_with_borsh( + *program_id, + &FlexiCounterInstruction::CreatePermission { members }, + accounts, + ) +} + +pub fn create_update_permission_ix( + payer: Pubkey, + members: Option>, +) -> Instruction { + let program_id = &crate::id(); + let (counter, _) = FlexiCounter::pda(&payer); + let (permission, _) = PermissionAccount::find_pda(&counter); + let accounts = vec![ + AccountMeta::new(payer, true), + AccountMeta::new(counter, false), + AccountMeta::new(permission, false), + AccountMeta::new_readonly( + ephemeral_rollups_sdk::consts::PERMISSION_PROGRAM_ID, + false, + ), + AccountMeta::new_readonly(MAGIC_PROGRAM_ID, false), + AccountMeta::new(EPHEMERAL_VAULT_ID, false), + ]; + + Instruction::new_with_borsh( + *program_id, + &FlexiCounterInstruction::UpdatePermission { members }, + accounts, + ) +} + +impl From for PermissionMember { + fn from(value: PermissionMemberArgs) -> Self { + Self { + pubkey: value.pubkey, + flags: value.flags, + } + } +} + pub fn create_add_and_schedule_commit_ix( payer: Pubkey, count: u8, diff --git a/test-integration/programs/flexi-counter/src/processor.rs b/test-integration/programs/flexi-counter/src/processor.rs index 6e4a6b6fb..1fcada741 100644 --- a/test-integration/programs/flexi-counter/src/processor.rs +++ b/test-integration/programs/flexi-counter/src/processor.rs @@ -5,11 +5,18 @@ mod transfer_intent; use borsh::{to_vec, BorshDeserialize}; use ephemeral_rollups_sdk::{ + access_control::{ + instructions::{ + CreateEphemeralPermissionCpi, UpdateEphemeralPermissionCpi, + }, + structs::{EphemeralMembersArgs, EphemeralPermission, Member}, + }, consts::{EXTERNAL_UNDELEGATE_DISCRIMINATOR, MAGIC_PROGRAM_ID}, cpi::{ delegate_account, undelegate_account, DelegateAccounts, DelegateConfig, }, ephem::{commit_accounts, commit_and_undelegate_accounts}, + ephemeral_accounts::rent, }; use magicblock_magic_program_api::{ args::ScheduleTaskArgs, instruction::MagicBlockInstruction, @@ -27,11 +34,14 @@ use solana_program::{ }; use solana_system_interface::instruction as system_instruction; +// The max number of members in the query filtering permission in tests +const QUERY_FILTERING_PERMISSION_MEMBERS: usize = 3; + 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, FlexiCounterInstruction, PermissionMemberArgs, + ScheduleArgs, MAX_ACCOUNT_ALLOC_PER_INSTRUCTION_SIZE, }, processor::{ call_handler::{ @@ -83,6 +93,12 @@ pub fn process( AddError { count } => process_add_error(accounts, count), Mul { multiplier } => process_mul(accounts, multiplier), Delegate(args) => process_delegate(accounts, &args), + CreatePermission { members } => { + process_create_permission(accounts, members) + } + UpdatePermission { members } => { + process_update_permission(accounts, members) + } AddAndScheduleCommit { count, undelegate, @@ -182,7 +198,10 @@ fn process_init( let ix = system_instruction::create_account( payer_info.key, counter_pda_info.key, - Rent::get()?.minimum_balance(size), + Rent::get()?.minimum_balance(size) + + rent(EphemeralPermission::size_of( + QUERY_FILTERING_PERMISSION_MEMBERS, + ) as u32), size as u64, program_id, ); @@ -362,6 +381,100 @@ fn process_delegate( Ok(()) } +fn process_create_permission( + accounts: &[AccountInfo], + members: Option>, +) -> ProgramResult { + msg!("CreatePermission"); + + let account_info_iter = &mut accounts.iter(); + let payer_info = next_account_info(account_info_iter)?; + let counter_pda_info = next_account_info(account_info_iter)?; + let permission_info = next_account_info(account_info_iter)?; + let permission_program_info = next_account_info(account_info_iter)?; + let magic_program_info = next_account_info(account_info_iter)?; + let ephemeral_vault_info = next_account_info(account_info_iter)?; + + let (counter_pda, bump) = FlexiCounter::pda(payer_info.key); + assert_keys_equal(counter_pda_info.key, &counter_pda, || { + format!( + "Invalid Counter PDA {}, should be {}", + counter_pda_info.key, counter_pda + ) + })?; + + let bump_slice = [bump]; + let signer_seeds = + FlexiCounter::seeds_with_bump(payer_info.key, &bump_slice); + + CreateEphemeralPermissionCpi { + permission_program: permission_program_info.clone(), + permissioned_account: counter_pda_info.clone(), + permission: permission_info.clone(), + payer: counter_pda_info.clone(), + magic_program: magic_program_info.clone(), + vault: ephemeral_vault_info.clone(), + args: members + .map(|members| EphemeralMembersArgs { + members: members.into_iter().map(Member::from).collect(), + is_private: true, + }) + .unwrap_or(EphemeralMembersArgs { + members: vec![], + is_private: false, + }), + } + .invoke_signed(&[&signer_seeds]) +} + +fn process_update_permission( + accounts: &[AccountInfo], + members: Option>, +) -> ProgramResult { + msg!("UpdatePermission"); + + let account_info_iter = &mut accounts.iter(); + let payer_info = next_account_info(account_info_iter)?; + let counter_pda_info = next_account_info(account_info_iter)?; + let permission_info = next_account_info(account_info_iter)?; + let permission_program_info = next_account_info(account_info_iter)?; + let magic_program_info = next_account_info(account_info_iter)?; + let ephemeral_vault_info = next_account_info(account_info_iter)?; + + let (counter_pda, bump) = FlexiCounter::pda(payer_info.key); + assert_keys_equal(counter_pda_info.key, &counter_pda, || { + format!( + "Invalid Counter PDA {}, should be {}", + counter_pda_info.key, counter_pda + ) + })?; + + let bump_slice = [bump]; + let signer_seeds = + FlexiCounter::seeds_with_bump(payer_info.key, &bump_slice); + + UpdateEphemeralPermissionCpi { + authority: counter_pda_info.clone(), + authority_is_signer: true, + permission_program: permission_program_info.clone(), + permissioned_account: counter_pda_info.clone(), + permission: permission_info.clone(), + payer: counter_pda_info.clone(), + magic_program: magic_program_info.clone(), + vault: ephemeral_vault_info.clone(), + args: members + .map(|members| EphemeralMembersArgs { + members: members.into_iter().map(Member::from).collect(), + is_private: true, + }) + .unwrap_or(EphemeralMembersArgs { + members: vec![], + is_private: false, + }), + } + .invoke_signed(&[&signer_seeds]) +} + fn process_add_and_schedule_commit( accounts: &[AccountInfo], count: u8, diff --git a/test-integration/test-query-filtering/Cargo.toml b/test-integration/test-query-filtering/Cargo.toml new file mode 100644 index 000000000..02b5ebde1 --- /dev/null +++ b/test-integration/test-query-filtering/Cargo.toml @@ -0,0 +1,19 @@ +[package] +edition.workspace = true +name = "test-query-filtering" +version.workspace = true + +[dependencies] +anyhow = { workspace = true } +borsh = { workspace = true } +integration-test-tools = { workspace = true } +program-flexi-counter = { workspace = true } +magicblock-query-filtering = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = "1.0" +solana-commitment-config = { workspace = true } +solana-rpc-client = { workspace = true } +solana-rpc-client-api = "3.1.12" +solana-sdk = { workspace = true } +solana-transaction-status = { workspace = true } +ureq = { workspace = true, features = ["json"] } diff --git a/test-integration/test-query-filtering/src/lib.rs b/test-integration/test-query-filtering/src/lib.rs new file mode 100644 index 000000000..27d4ab404 --- /dev/null +++ b/test-integration/test-query-filtering/src/lib.rs @@ -0,0 +1,334 @@ +#![allow(dead_code)] + +use std::{thread::sleep, time::Duration}; + +use anyhow::{Context, Result}; +use integration_test_tools::IntegrationTestContext; +use magicblock_query_filtering::types::Permission; +use program_flexi_counter::{ + instruction::{ + create_create_permission_ix, create_delegate_ix, create_init_ix, + PermissionMemberArgs, + }, + state::FlexiCounter, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use solana_commitment_config::CommitmentConfig; +use solana_rpc_client::rpc_client::RpcClient; +use solana_sdk::{ + instruction::Instruction, native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, + signature::Keypair, signer::Signer, transaction::Transaction, +}; + +pub const APERTURE_URL: &str = "http://localhost:8899"; +pub const MEMBER_FLAG_TX_LOGS: u8 = 1 << 1; +pub const MEMBER_FLAG_TX_BALANCES: u8 = 1 << 2; +pub const MEMBER_FLAG_TX_MESSAGE: u8 = 1 << 3; +pub const MEMBER_FLAG_ACCOUNT_SIGNATURE: u8 = 1 << 4; +pub const ALL_VISIBILITY_FLAGS: u8 = MEMBER_FLAG_TX_LOGS + | MEMBER_FLAG_TX_BALANCES + | MEMBER_FLAG_TX_MESSAGE + | MEMBER_FLAG_ACCOUNT_SIGNATURE; + +#[derive(Debug, Deserialize)] +struct ChallengeResponse { + challenge: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct LoginRequest { + pubkey: String, + signature: String, + challenge: String, +} + +#[derive(Debug, Deserialize)] +struct LoginResponse { + token: String, +} + +pub struct QueryFilteringFixture { + pub ctx: IntegrationTestContext, + pub allowed: User, + pub account_only: User, + pub denied: User, + pub restricted_owner: Keypair, + pub restricted_counter: Pubkey, + pub public_counter: Pubkey, +} + +pub struct User { + pub keypair: Keypair, + pub token: String, +} + +pub fn setup_query_filtering_fixture() -> Result { + let mut ctx = IntegrationTestContext::try_new_chain_only()?; + + let allowed = Keypair::new(); + let account_only = Keypair::new(); + let denied = Keypair::new(); + let restricted_owner = Keypair::new(); + let public_owner = Keypair::new(); + for keypair in [ + &allowed, + &account_only, + &denied, + &restricted_owner, + &public_owner, + ] { + ctx.airdrop_chain(&keypair.pubkey(), 5 * LAMPORTS_PER_SOL)?; + } + + let restricted_counter = FlexiCounter::pda(&restricted_owner.pubkey()).0; + let public_counter = FlexiCounter::pda(&public_owner.pubkey()).0; + let members = vec![ + PermissionMemberArgs { + pubkey: allowed.pubkey(), + flags: ALL_VISIBILITY_FLAGS, + }, + PermissionMemberArgs { + pubkey: account_only.pubkey(), + flags: 0, + }, + PermissionMemberArgs { + pubkey: program_flexi_counter::id(), + flags: 0, + }, + ]; + + ctx.send_and_confirm_instructions_with_payer_chain( + &[ + create_init_ix(restricted_owner.pubkey(), "PRIVATE".to_string()), + create_delegate_ix(restricted_owner.pubkey()), + ], + &restricted_owner, + )?; + ctx.send_and_confirm_instructions_with_payer_chain( + &[ + create_init_ix(public_owner.pubkey(), "PUBLIC".to_string()), + create_delegate_ix(public_owner.pubkey()), + ], + &public_owner, + )?; + + let restricted_owner_token = login(&restricted_owner)?; + ctx.ephem_client = Some(authed_rpc(&restricted_owner_token)); + wait_for_account(&restricted_owner_token, &restricted_counter)?; + let restricted_permission_ix = + create_create_permission_ix(restricted_owner.pubkey(), Some(members)); + let (_, confirmed) = ctx.send_and_confirm_instructions_with_payer_ephem( + std::slice::from_ref(&restricted_permission_ix), + &restricted_owner, + )?; + ensure_confirmed_permission_ix( + &ctx, + &restricted_owner, + &restricted_permission_ix, + confirmed, + "restricted", + )?; + + let public_owner_token = login(&public_owner)?; + ctx.ephem_client = Some(authed_rpc(&public_owner_token)); + wait_for_account(&public_owner_token, &public_counter)?; + let public_permission_ix = + create_create_permission_ix(public_owner.pubkey(), None); + let (_, confirmed) = ctx.send_and_confirm_instructions_with_payer_ephem( + std::slice::from_ref(&public_permission_ix), + &public_owner, + )?; + ensure_confirmed_permission_ix( + &ctx, + &public_owner, + &public_permission_ix, + confirmed, + "public", + )?; + + let allowed = User { + token: login(&allowed)?, + keypair: allowed, + }; + let account_only = User { + token: login(&account_only)?, + keypair: account_only, + }; + let denied = User { + token: login(&denied)?, + keypair: denied, + }; + + wait_for_account(&allowed.token, &restricted_counter)?; + wait_for_account(&allowed.token, &public_counter)?; + + Ok(QueryFilteringFixture { + ctx, + allowed, + account_only, + denied, + restricted_owner, + restricted_counter, + public_counter, + }) +} + +fn ensure_confirmed_permission_ix( + ctx: &IntegrationTestContext, + payer: &Keypair, + ix: &Instruction, + confirmed: bool, + label: &str, +) -> Result<()> { + if confirmed { + return Ok(()); + } + + let ephem_client = ctx.try_ephem_client()?; + let blockhash = ephem_client.get_latest_blockhash()?; + let mut tx = Transaction::new_with_payer( + std::slice::from_ref(ix), + Some(&payer.pubkey()), + ); + tx.sign(&[payer], blockhash); + let simulation = ephem_client.simulate_transaction(&tx)?; + anyhow::bail!( + "{label} permission creation should confirm; simulation result: {:?}", + simulation.value, + ); +} + +pub fn authed_rpc(token: &str) -> RpcClient { + RpcClient::new_with_commitment( + format!("{APERTURE_URL}?token={token}"), + CommitmentConfig::confirmed(), + ) +} + +pub fn login(keypair: &Keypair) -> Result { + let challenge_url = + format!("{APERTURE_URL}/auth/challenge?pubkey={}", keypair.pubkey()); + let challenge: ChallengeResponse = + ureq::get(&challenge_url).call()?.into_json()?; + let signature = keypair.sign_message(challenge.challenge.as_bytes()); + let request = LoginRequest { + pubkey: keypair.pubkey().to_string(), + signature: signature.to_string(), + challenge: challenge.challenge, + }; + let response: LoginResponse = + ureq::post(&format!("{APERTURE_URL}/auth/login")) + .send_json(serde_json::to_value(request)?)? + .into_json()?; + Ok(response.token) +} + +/// Raw JSON-RPC call against the aperture server that authenticates via the +/// `?token=` query parameter — the same path the Solana `RpcClient` uses +/// under `authed_rpc`. +/// +/// Bulk data tests should use `authed_rpc(token)` and the typed RpcClient +/// methods. This helper exists only for auth-specific tests that need to +/// inspect raw HTTP status codes or exercise the `?token=` path with an +/// invalid or missing token. +pub fn raw_rpc_with_url_token( + token: Option<&str>, + method: &str, + params: Value, +) -> Result { + let url = match token { + Some(token) => format!("{APERTURE_URL}?token={token}"), + None => APERTURE_URL.to_string(), + }; + raw_post(&url, &[], method, params) +} + +/// Raw JSON-RPC call against the aperture server that authenticates via the +/// `Authorization: Bearer ` header. Used exclusively by the auth test +/// that asserts the production dispatcher still honors the Bearer fallback +/// (Solana's `RpcClient` does not send custom auth headers, so the Bearer +/// path must be exercised manually). +pub fn raw_rpc_with_bearer_header( + token: &str, + method: &str, + params: Value, +) -> Result { + raw_post( + APERTURE_URL, + &[("Authorization", format!("Bearer {token}"))], + method, + params, + ) +} + +fn raw_post( + url: &str, + headers: &[(&str, String)], + method: &str, + params: Value, +) -> Result { + let body = json!({ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + }); + let mut request = ureq::post(url); + for (name, value) in headers { + request = request.set(name, value); + } + Ok(request.send_json(body)?.into_json()?) +} + +pub fn assert_missing_url_token_is_rejected( + method: &str, + params: &[String], +) -> Result<()> { + let err = raw_rpc_with_url_token(None, method, json!(params)).unwrap_err(); + assert_http_status(&err, 401, "missing token should return HTTP 401"); + Ok(()) +} + +pub fn assert_http_status(err: &anyhow::Error, expected: u16, context: &str) { + let status = err.downcast_ref::().and_then(|err| match err { + ureq::Error::Status(status, _) => Some(*status), + ureq::Error::Transport(_) => None, + }); + assert_eq!(status, Some(expected), "{context}"); +} + +fn wait_for_account(token: &str, pubkey: &Pubkey) -> Result<()> { + let client = authed_rpc(token); + let commitment = CommitmentConfig::confirmed(); + let mut last_response = None; + for _ in 0..50 { + match client.get_account_with_commitment(pubkey, commitment) { + Ok(response) if response.value.is_some() => return Ok(()), + Ok(response) => last_response = Some(format!("{response:?}")), + Err(err) => last_response = Some(err.to_string()), + } + sleep(Duration::from_millis(200)); + } + let permission_account = client + .get_account_with_commitment(&Permission::pda(pubkey), commitment) + .map(|response| format!("{response:?}")) + .unwrap_or_else(|err| err.to_string()); + anyhow::bail!( + "account {pubkey} was not visible on ephemeral validator; last response: {}; permission response: {}", + last_response.unwrap_or_else(|| "".to_string()), + permission_account, + ) +} + +impl QueryFilteringFixture { + pub fn chain_account_exists(&self, pubkey: &Pubkey) -> Result<()> { + self.ctx + .chain_client + .as_ref() + .context("missing chain")? + .get_account(pubkey)?; + Ok(()) + } +} diff --git a/test-integration/test-query-filtering/tests/account_visibility.rs b/test-integration/test-query-filtering/tests/account_visibility.rs new file mode 100644 index 000000000..73d3f9399 --- /dev/null +++ b/test-integration/test-query-filtering/tests/account_visibility.rs @@ -0,0 +1,87 @@ +use anyhow::Result; +use solana_commitment_config::CommitmentConfig; +use solana_sdk::{account::Account, pubkey::Pubkey}; +use test_query_filtering::{ + assert_missing_url_token_is_rejected, authed_rpc, + setup_query_filtering_fixture, +}; + +#[test] +fn query_filtering_filters_direct_account_rpc_by_membership() -> Result<()> { + let fixture = setup_query_filtering_fixture()?; + + assert_missing_url_token_is_rejected( + "getAccountInfo", + &[fixture.restricted_counter.to_string()], + )?; + + for (user, can_access_restricted) in [ + (&fixture.allowed, true), + (&fixture.account_only, true), + (&fixture.denied, false), + ] { + let client = authed_rpc(&user.token); + + let restricted = fetch_account(&client, &fixture.restricted_counter)?; + assert_account_visibility( + restricted.as_ref(), + can_access_restricted, + "restricted getAccountInfo", + ); + + let public = fetch_account(&client, &fixture.public_counter)?; + assert_account_visibility( + public.as_ref(), + true, + "public getAccountInfo", + ); + + let balance = client.get_balance(&fixture.restricted_counter)?; + if can_access_restricted { + assert!(balance > 0, "authorized user should see balance"); + } else { + assert_eq!(balance, 0, "unauthorized user should see zero balance"); + } + + let multiple = client + .get_multiple_accounts_with_commitment( + &[fixture.restricted_counter, fixture.public_counter], + CommitmentConfig::confirmed(), + )? + .value; + assert_account_visibility( + multiple[0].as_ref(), + can_access_restricted, + "restricted getMultipleAccounts", + ); + assert_account_visibility( + multiple[1].as_ref(), + true, + "public getMultipleAccounts", + ); + } + + fixture.chain_account_exists(&fixture.restricted_counter)?; + Ok(()) +} + +fn fetch_account( + client: &solana_rpc_client::rpc_client::RpcClient, + pubkey: &Pubkey, +) -> Result> { + Ok(client + .get_account_with_commitment(pubkey, CommitmentConfig::confirmed())? + .value) +} + +fn assert_account_visibility( + account: Option<&Account>, + visible: bool, + context: &str, +) { + if visible { + assert!(account.is_some(), "{context} should be visible"); + } else { + assert!(account.is_none(), "{context} should be filtered"); + } +} diff --git a/test-integration/test-query-filtering/tests/auth.rs b/test-integration/test-query-filtering/tests/auth.rs new file mode 100644 index 000000000..e55b70cd7 --- /dev/null +++ b/test-integration/test-query-filtering/tests/auth.rs @@ -0,0 +1,32 @@ +use anyhow::Result; +use serde_json::json; +use solana_sdk::{signature::Keypair, signer::Signer}; +use test_query_filtering::{ + assert_http_status, assert_missing_url_token_is_rejected, login, + raw_rpc_with_bearer_header, raw_rpc_with_url_token, +}; + +#[test] +fn query_filtering_requires_a_valid_token() -> Result<()> { + assert_missing_url_token_is_rejected("getSlot", &[])?; + + let err = + raw_rpc_with_url_token(Some("not-a-valid-jwt"), "getSlot", json!([])) + .unwrap_err(); + assert_http_status(&err, 401, "invalid token should return HTTP 401"); + Ok(()) +} + +#[test] +fn query_filtering_accepts_bearer_header_tokens() -> Result<()> { + let user = Keypair::new(); + let token = login(&user)?; + let response = raw_rpc_with_bearer_header(&token, "getSlot", json!([]))?; + + assert!( + response.get("result").is_some(), + "header token should authenticate {}", + user.pubkey() + ); + Ok(()) +} diff --git a/test-integration/test-query-filtering/tests/program_accounts.rs b/test-integration/test-query-filtering/tests/program_accounts.rs new file mode 100644 index 000000000..0f5ce7c27 --- /dev/null +++ b/test-integration/test-query-filtering/tests/program_accounts.rs @@ -0,0 +1,35 @@ +use anyhow::Result; +use solana_sdk::signer::Signer; +use test_query_filtering::{authed_rpc, setup_query_filtering_fixture}; + +#[test] +fn query_filtering_filters_get_program_accounts_by_membership() -> Result<()> { + let fixture = setup_query_filtering_fixture()?; + + for (user, can_access_restricted) in [ + (&fixture.allowed, true), + (&fixture.account_only, true), + (&fixture.denied, false), + ] { + let client = authed_rpc(&user.token); + let program_accounts = + client.get_program_accounts(&program_flexi_counter::id())?; + assert_eq!( + program_accounts + .iter() + .any(|(pubkey, _)| pubkey == &fixture.restricted_counter), + can_access_restricted, + "restricted counter program account visibility for {}", + user.keypair.pubkey() + ); + assert!( + program_accounts + .iter() + .any(|(pubkey, _)| pubkey == &fixture.public_counter), + "public counter should stay visible for {}", + user.keypair.pubkey() + ); + } + + Ok(()) +} diff --git a/test-integration/test-query-filtering/tests/signatures.rs b/test-integration/test-query-filtering/tests/signatures.rs new file mode 100644 index 000000000..0463d94b1 --- /dev/null +++ b/test-integration/test-query-filtering/tests/signatures.rs @@ -0,0 +1,41 @@ +use anyhow::Result; +use program_flexi_counter::instruction::create_add_ix; +use solana_sdk::{signer::Signer, transaction::Transaction}; +use test_query_filtering::{authed_rpc, setup_query_filtering_fixture}; + +#[test] +fn query_filtering_filters_signatures_by_account_signature_flag() -> Result<()> +{ + let fixture = setup_query_filtering_fixture()?; + + let ephem_rpc = authed_rpc(&fixture.allowed.token); + let blockhash = ephem_rpc.get_latest_blockhash()?; + let tx = Transaction::new_signed_with_payer( + &[create_add_ix(fixture.restricted_owner.pubkey(), 7)], + Some(&fixture.restricted_owner.pubkey()), + &[&fixture.restricted_owner], + blockhash, + ); + let signature = ephem_rpc.send_and_confirm_transaction(&tx)?; + + for (user, can_see_signatures) in [ + (&fixture.allowed, true), + (&fixture.account_only, false), + (&fixture.denied, false), + ] { + let client = authed_rpc(&user.token); + let signatures = + client.get_signatures_for_address(&fixture.restricted_counter)?; + let signature_str = signature.to_string(); + assert_eq!( + signatures + .iter() + .any(|info| info.signature == signature_str), + can_see_signatures, + "signature visibility for {}", + user.keypair.pubkey() + ); + } + + Ok(()) +} diff --git a/test-integration/test-query-filtering/tests/transactions.rs b/test-integration/test-query-filtering/tests/transactions.rs new file mode 100644 index 000000000..ae3d9de97 --- /dev/null +++ b/test-integration/test-query-filtering/tests/transactions.rs @@ -0,0 +1,307 @@ +use anyhow::{Context, Result}; +use program_flexi_counter::instruction::create_add_ix; +use solana_commitment_config::CommitmentConfig; +use solana_rpc_client::rpc_client::RpcClient; +use solana_rpc_client_api::config::RpcTransactionConfig; +use solana_sdk::{signer::Signer, transaction::Transaction}; +use solana_transaction_status::{ + EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction, UiMessage, + UiTransactionEncoding, UiTransactionStatusMeta, +}; +use test_query_filtering::{authed_rpc, setup_query_filtering_fixture}; + +/// Expected visibility across the four per-account access flags exposed by a +/// permission entry. +#[derive(Clone, Copy, Debug)] +struct ExpectedAccess { + /// Caller can see the underlying account (full body is preserved). + account: bool, + /// Caller can see the transaction message (account keys, instructions). + message: bool, + /// Caller can see balance changes. + balances: bool, + /// Caller can see log messages. + logs: bool, +} + +/// `getTransaction` honors each visibility flag independently — message, +/// balances, and logs are redacted in isolation; the full-redaction path +/// also wipes the fee. +#[test] +fn query_filtering_redacts_get_transaction_per_flag() -> Result<()> { + let fixture = setup_query_filtering_fixture()?; + + // The restricted owner sends a tx through the flexi-counter program so the + // tx touches the restricted counter PDA. flexi-counter is listed as a + // permission member, so the admission check allows the submission. + let ephem_rpc = authed_rpc(&fixture.allowed.token); + let blockhash = ephem_rpc.get_latest_blockhash()?; + let tx = Transaction::new_signed_with_payer( + &[create_add_ix(fixture.restricted_owner.pubkey(), 3)], + Some(&fixture.restricted_owner.pubkey()), + &[&fixture.restricted_owner], + blockhash, + ); + ephem_rpc.send_and_confirm_transaction(&tx)?; + + // Establish baseline shape from the all-access user so each + // user's response can be diffed against ground truth. + let baseline = fetch_transaction(&fixture.allowed.token, &tx)?; + let baseline_balance_count = expect_meta(&baseline).pre_balances.len(); + assert!( + baseline_balance_count > 0, + "baseline tx should report at least one pre-balance entry" + ); + assert!( + !expect_account_keys(&baseline).is_empty(), + "baseline tx should expose account keys" + ); + + // The three redaction paths the validator can take. Per-flag granularity + // (e.g. logs-only, balances-only, message-only members) is exhaustively + // exercised by the unit tests in `magicblock-query-filtering`; here we + // verify each end-to-end path lands the right shape on the wire. + for (user, label, expected) in [ + ( + &fixture.allowed, + "allowed (no redaction)", + ExpectedAccess { + account: true, + message: true, + balances: true, + logs: true, + }, + ), + ( + &fixture.account_only, + "account_only (selective redaction, all sub-flags off)", + ExpectedAccess { + account: true, + message: false, + balances: false, + logs: false, + }, + ), + ( + &fixture.denied, + "denied (full redaction)", + ExpectedAccess { + account: false, + message: false, + balances: false, + logs: false, + }, + ), + ] { + let response = fetch_transaction(&user.token, &tx)?; + assert_expected_access( + &response, + baseline_balance_count, + label, + expected, + ); + } + + Ok(()) +} + +fn fetch_transaction( + token: &str, + tx: &Transaction, +) -> Result { + let client = authed_rpc(token); + poll_for_transaction(&client, &tx.signatures[0].to_string()) +} + +fn poll_for_transaction( + client: &RpcClient, + signature: &str, +) -> Result { + use std::{thread::sleep, time::Duration}; + + let signature = signature + .parse() + .context("failed to parse signature for getTransaction")?; + let config = RpcTransactionConfig { + encoding: Some(UiTransactionEncoding::Json), + commitment: Some(CommitmentConfig::confirmed()), + max_supported_transaction_version: Some(0), + }; + let mut last_err = None; + for _ in 0..50 { + match client.get_transaction_with_config(&signature, config) { + Ok(response) => return Ok(response), + Err(err) => { + last_err = Some(err); + sleep(Duration::from_millis(100)); + } + } + } + anyhow::bail!( + "getTransaction did not return within timeout; last error: {last_err:?}" + ) +} + +fn expect_meta( + response: &EncodedConfirmedTransactionWithStatusMeta, +) -> &UiTransactionStatusMeta { + response + .transaction + .meta + .as_ref() + .expect("transaction meta is missing") +} + +fn expect_account_keys( + response: &EncodedConfirmedTransactionWithStatusMeta, +) -> Vec { + match &response.transaction.transaction { + EncodedTransaction::Json(tx) => match &tx.message { + UiMessage::Raw(raw) => raw.account_keys.clone(), + UiMessage::Parsed(parsed) => parsed + .account_keys + .iter() + .map(|key| key.pubkey.clone()) + .collect(), + }, + other => panic!( + "expected json-encoded transaction, got {other:?} (response is shaped wrong)" + ), + } +} + +fn assert_expected_access( + response: &EncodedConfirmedTransactionWithStatusMeta, + baseline_balance_count: usize, + label: &str, + expected: ExpectedAccess, +) { + let account_keys = expect_account_keys(response); + if expected.message { + assert!( + !account_keys.is_empty(), + "[{label}] expected accountKeys to be preserved" + ); + } else { + assert!( + account_keys.is_empty(), + "[{label}] expected accountKeys to be redacted; got {account_keys:?}" + ); + } + + let meta = expect_meta(response); + let log_messages = Option::>::from(meta.log_messages.clone()); + + if expected.account { + // Selective redaction preserves the shape of meta — balance arrays + // keep their length so positional indices stay valid; per-account + // sub-flags only mask individual entries. + assert_eq!( + meta.pre_balances.len(), + baseline_balance_count, + "[{label}] selective redaction must keep preBalances sized" + ); + assert_eq!( + meta.post_balances.len(), + baseline_balance_count, + "[{label}] selective redaction must keep postBalances sized" + ); + if expected.balances { + // Every account in this tx is visible (unrestricted or + // member-with-balance-flag), so no entry should be zeroed. + assert!( + meta.pre_balances.iter().any(|balance| *balance != 0), + "[{label}] expected at least one nonzero pre-balance" + ); + } else { + // At least one entry must be zeroed (the restricted counter), + // but the others (payer, program) keep their balance because + // they are unrestricted accounts. + assert!( + meta.pre_balances.contains(&0), + "[{label}] expected the restricted account's balance to be zeroed; got {:?}", + meta.pre_balances + ); + } + + if expected.logs { + let logs = log_messages.unwrap_or_else(|| { + panic!("[{label}] expected log messages to be present") + }); + assert!( + !logs.is_empty(), + "[{label}] expected nonempty log messages" + ); + } else { + // logs are wiped if *any* touched account hides them. + let logs = log_messages.unwrap_or_else(|| { + panic!( + "[{label}] expected empty log array (selective redaction)" + ) + }); + assert!( + logs.is_empty(), + "[{label}] expected logMessages to be cleared; got {logs:?}" + ); + } + } else { + // Full redaction: balance arrays and logs are reset to defaults + // (TransactionStatusMeta::default()), distinguishing this path from + // selective redaction which keeps balance arrays sized. + assert!( + meta.pre_balances.is_empty(), + "[{label}] full redaction must empty preBalances" + ); + assert!( + meta.post_balances.is_empty(), + "[{label}] full redaction must empty postBalances" + ); + assert!( + log_messages.is_none(), + "[{label}] full redaction must drop logMessages; got {log_messages:?}" + ); + } +} + +/// `sendTransaction` rejects submissions that touch a restricted account +/// through a non-whitelisted, non-member program — regardless of the signing +/// user's per-account access flags. +#[test] +fn query_filtering_rejects_send_transaction_for_non_member_program( +) -> Result<()> { + use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + }; + + let fixture = setup_query_filtering_fixture()?; + + // A random program id that is not whitelisted and not a permission member. + let custom_program = Pubkey::new_unique(); + let ephem_rpc = authed_rpc(&fixture.allowed.token); + let blockhash = ephem_rpc.get_latest_blockhash()?; + + let ix = Instruction { + program_id: custom_program, + accounts: vec![AccountMeta::new(fixture.restricted_counter, false)], + data: vec![], + }; + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&fixture.allowed.keypair.pubkey()), + &[&fixture.allowed.keypair], + blockhash, + ); + + let result = ephem_rpc.send_transaction(&tx); + let err = result + .err() + .context("expected sendTransaction to be rejected, but it succeeded")?; + let message = err.to_string(); + assert!( + message.contains("access denied"), + "expected access-denied error from sendTransaction; got {message}" + ); + + Ok(()) +} diff --git a/test-integration/test-runner/bin/run_tests.rs b/test-integration/test-runner/bin/run_tests.rs index fadd7f727..6986b56fa 100644 --- a/test-integration/test-runner/bin/run_tests.rs +++ b/test-integration/test-runner/bin/run_tests.rs @@ -38,6 +38,12 @@ pub fn main() { return; }; + let Ok(query_filtering_output) = + run_query_filtering_tests(&manifest_dir, &config) + else { + return; + }; + let Ok(restore_ledger_output) = run_restore_ledger_tests(&manifest_dir, &config) else { @@ -83,6 +89,7 @@ pub fn main() { assert_cargo_tests_passed(scenarios_output, "scenarios"); assert_cargo_tests_passed(chainlink_output, "chainlink"); assert_cargo_tests_passed(cloning_output, "cloning"); + assert_cargo_tests_passed(query_filtering_output, "query_filtering"); assert_cargo_tests_passed(restore_ledger_output, "restore_ledger"); assert_cargo_tests_passed(magicblock_api_output, "magicblock_api"); assert_cargo_tests_passed(table_mania_output, "table_mania"); @@ -785,6 +792,73 @@ fn run_cloning_tests( } } +fn run_query_filtering_tests( + manifest_dir: &str, + config: &TestConfigViaEnvVars, +) -> Result> { + const TEST_NAME: &str = "query_filtering"; + if config.skip_entirely(TEST_NAME) { + return Ok(success_output()); + } + + let loaded_chain_accounts = + LoadedAccounts::with_delegation_program_test_authority(); + let start_devnet_validator = || match start_validator( + "query-filtering-conf.devnet.toml", + ValidatorCluster::Chain(Some(ProgramLoader::UpgradeableProgram)), + &loaded_chain_accounts, + ) { + Some(validator) => validator, + None => { + panic!("Failed to start devnet validator properly"); + } + }; + + let start_ephem_validator = || match start_validator( + "query-filtering-conf.ephem.toml", + ValidatorCluster::Ephem, + &loaded_chain_accounts, + ) { + Some(validator) => validator, + None => { + panic!("Failed to start ephemeral validator properly"); + } + }; + + if config.run_test(TEST_NAME) { + eprintln!("======== RUNNING QUERY FILTERING TESTS ========"); + + let mut devnet_validator = start_devnet_validator(); + let mut ephem_validator = start_ephem_validator(); + + let test_query_filtering_dir = + format!("{}/../{}", manifest_dir, "test-query-filtering"); + eprintln!( + "Running query filtering tests in {}", + test_query_filtering_dir + ); + let output = match run_test( + test_query_filtering_dir, + RunTestConfig::default(), + ) { + Ok(output) => output, + Err(err) => { + eprintln!("Failed to run query filtering tests: {:?}", err); + cleanup_validators(&mut ephem_validator, &mut devnet_validator); + return Err(err.into()); + } + }; + cleanup_validators(&mut ephem_validator, &mut devnet_validator); + Ok(output) + } else { + let devnet_validator = + 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()) + } +} + fn run_magicblock_api_tests( manifest_dir: &str, config: &TestConfigViaEnvVars, diff --git a/test-integration/test-tools/src/validator.rs b/test-integration/test-tools/src/validator.rs index 8581ce146..c9d336d5e 100644 --- a/test-integration/test-tools/src/validator.rs +++ b/test-integration/test-tools/src/validator.rs @@ -69,7 +69,11 @@ pub fn start_magic_block_validator_with_config( } let mut c = process::Command::new("cargo"); - c.arg("run").arg("--").arg(config_path); + c.arg("run") + .arg("--features") + .arg("query-filtering") + .arg("--") + .arg(config_path); c };