From 1211549ca177641bca3874e0b7bca61a9ffd20c3 Mon Sep 17 00:00:00 2001 From: ArthurPaivaT Date: Fri, 3 Jul 2026 12:01:57 +0000 Subject: [PATCH 01/13] Add support to the simulateTransaction method --- Cargo.lock | 1805 ++++++++++++----- Cargo.toml | 24 + crates/api/Cargo.toml | 30 +- crates/api/src/http/mod.rs | 3 + crates/api/src/http/rpc.rs | 16 +- crates/api/src/lib.rs | 4 + crates/api/src/methods/mod.rs | 1 + .../api/src/methods/simulate_transaction.rs | 834 ++++++++ crates/core/src/config.rs | 5 + crates/index/src/db_queries.rs | 50 +- crates/index/src/modules/save_block.rs | 2 + crates/migration/src/lib.rs | 2 + ..._000000_create_recent_blockhashes_table.rs | 35 + 13 files changed, 2294 insertions(+), 517 deletions(-) create mode 100644 crates/api/src/methods/simulate_transaction.rs create mode 100644 crates/migration/src/m20260703_000000_create_recent_blockhashes_table.rs diff --git a/Cargo.lock b/Cargo.lock index 397ba90..955b9ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,25 +54,54 @@ dependencies = [ "zeroize", ] +[[package]] +name = "agave-bls12-381" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "210b1ef312273aa81ccb4c52687d96e3cf07621f3619a7998be20eb9741b08e3" +dependencies = [ + "blst", + "blstrs", + "bytemuck", + "bytemuck_derive", + "group", + "pairing", +] + [[package]] name = "agave-feature-set" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2846bb4fc0831d112255193a54259fabdc82149f0cd0a72db8922837cc62c0cd" +checksum = "6200f3b8cfbe5992fde00d443f60e62a79d2d8f6a658af1ffb7c4f0baa3c7028" dependencies = [ "ahash 0.8.12", "solana-epoch-schedule", "solana-hash 3.1.0", "solana-pubkey 3.0.0", "solana-sha256-hasher", - "solana-svm-feature-set", + "solana-svm-feature-set 3.1.12", +] + +[[package]] +name = "agave-feature-set" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde74a2d1f2f99a3ea59938d1533c7973c344e47d24c1b645ee81e958c54226a" +dependencies = [ + "ahash 0.8.12", + "solana-epoch-schedule", + "solana-hash 4.2.0", + "solana-keypair", + "solana-pubkey 4.1.0", + "solana-sha256-hasher", + "solana-svm-feature-set 4.0.0", ] [[package]] name = "agave-fs" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b650afb205ac1cc84288c83b6f6f69057990e33c12ba93772d7fa1ff82d798ad" +checksum = "52914a4a42e83ba41aa04c7db20eca446e441b8c50f45f7fea5f83c64655c06a" dependencies = [ "agave-io-uring", "io-uring", @@ -84,9 +113,9 @@ dependencies = [ [[package]] name = "agave-io-uring" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8440be5b6759e60ffe27c15da728568721b79f03524ca2b667b229c18b82eb6e" +checksum = "f413bd15f8ec5c983ee8a84586aa276ebf5278154344aefadebc2fc3d19fdc73" dependencies = [ "io-uring", "libc", @@ -97,9 +126,9 @@ dependencies = [ [[package]] name = "agave-logger" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37f62cea69e633631571de6c4641c0738a232158a4a2e4b4ceefd7f2b9a3bba4" +checksum = "8ab27ea965d7b0b0725a049b2c07a6b5700f37d095765718f3101824701536e4" dependencies = [ "env_logger", "libc", @@ -109,11 +138,11 @@ dependencies = [ [[package]] name = "agave-precompiles" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d65f7398d5e24334130de1f825c0ad1a78471a2a43c90e98914c792d29dfc89" +checksum = "5c0fa80ea1037091eff64931fc53b6a89f153f5a88c33035cedfa79265b985cb" dependencies = [ - "agave-feature-set", + "agave-feature-set 3.1.12", "bincode", "digest 0.10.7", "ed25519-dalek 1.0.1", @@ -129,22 +158,44 @@ dependencies = [ "solana-secp256r1-program", ] +[[package]] +name = "agave-precompiles" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a48ac39333d364f102b03ef00b59cd5b1f878ccb3c5f0f67df20f44824d51f" +dependencies = [ + "agave-feature-set 4.0.0", + "bincode", + "digest 0.10.7", + "ed25519-dalek 1.0.1", + "libsecp256k1", + "openssl", + "sha3", + "solana-ed25519-program", + "solana-message", + "solana-precompile-error", + "solana-pubkey 4.1.0", + "solana-sdk-ids", + "solana-secp256k1-program", + "solana-secp256r1-program", +] + [[package]] name = "agave-reserved-account-keys" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55fff3d170fbcf81afc8d30c504a1ae4a6ff64be025ee6c08012f3db2a243fc" +checksum = "2c3998a6ec388df954d8f78eeaf73ff487b50a8bdaebd48c8573af22c7b6db72" dependencies = [ - "agave-feature-set", + "agave-feature-set 3.1.12", "solana-pubkey 3.0.0", "solana-sdk-ids", ] [[package]] name = "agave-snapshots" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f19b2777041a651187f2c94ed07d92ea1aafb7ec43b48035f39adc4fe282b8f" +checksum = "fe3356a36f262ea9fd2405c6a06953aca6f02e4d33040d3ae45e2a4d81d705ca" dependencies = [ "agave-fs", "bincode", @@ -172,9 +223,9 @@ dependencies = [ [[package]] name = "agave-syscalls" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa34d30153e3c36d0d488a606a0aa01c85724f04048a02433623d55a28cf679a" +checksum = "5212f0b24fc37d4c844ae25b563d0cf5587d092912820c40f88d4b9b301148f8" dependencies = [ "bincode", "libsecp256k1", @@ -186,59 +237,103 @@ dependencies = [ "solana-bn254", "solana-clock", "solana-cpi", - "solana-curve25519", + "solana-curve25519 3.1.12", "solana-hash 3.1.0", "solana-instruction", "solana-keccak-hasher", "solana-loader-v3-interface", - "solana-poseidon", + "solana-poseidon 3.1.12", "solana-program-entrypoint", - "solana-program-runtime", + "solana-program-runtime 3.1.12", "solana-pubkey 3.0.0", - "solana-sbpf", + "solana-sbpf 0.13.1", + "solana-sdk-ids", + "solana-secp256k1-recover", + "solana-sha256-hasher", + "solana-stable-layout", + "solana-stake-interface", + "solana-svm-callback 3.1.12", + "solana-svm-feature-set 3.1.12", + "solana-svm-log-collector 3.1.12", + "solana-svm-measure 3.1.12", + "solana-svm-timings 3.1.12", + "solana-svm-type-overrides 3.1.12", + "solana-sysvar", + "solana-sysvar-id", + "solana-transaction-context 3.1.12", + "thiserror 2.0.18", +] + +[[package]] +name = "agave-syscalls" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84debd4abe0cbab5a6aac2ee50e3969ef0e0961f7dff7e8f96bda0be7998bca2" +dependencies = [ + "agave-bls12-381", + "bincode", + "libsecp256k1", + "num-traits", + "solana-account", + "solana-account-info", + "solana-big-mod-exp", + "solana-blake3-hasher", + "solana-bn254", + "solana-clock", + "solana-cpi", + "solana-curve25519 4.0.1", + "solana-hash 4.2.0", + "solana-instruction", + "solana-keccak-hasher", + "solana-loader-v3-interface", + "solana-poseidon 4.0.0", + "solana-program-entrypoint", + "solana-program-runtime 4.0.0", + "solana-pubkey 4.1.0", + "solana-sbpf 0.14.4", "solana-sdk-ids", "solana-secp256k1-recover", "solana-sha256-hasher", "solana-stable-layout", "solana-stake-interface", - "solana-svm-callback", - "solana-svm-feature-set", - "solana-svm-log-collector", - "solana-svm-measure", - "solana-svm-timings", - "solana-svm-type-overrides", + "solana-svm-callback 4.0.0", + "solana-svm-feature-set 4.0.0", + "solana-svm-log-collector 4.0.0", + "solana-svm-measure 4.0.0", + "solana-svm-timings 4.0.0", + "solana-svm-type-overrides 4.0.0", "solana-sysvar", "solana-sysvar-id", - "solana-transaction-context", + "solana-transaction-context 4.0.0", "thiserror 2.0.18", ] [[package]] name = "agave-transaction-view" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15a8e85dc7796559a1bd3bd292d62a841c7a564deefe783655974d2d8b49207" +checksum = "e9b36ce7792c4e48140ee2f9ef4eaa289fab1c383a0670588a6c7c755947608c" dependencies = [ "solana-hash 3.1.0", "solana-message", - "solana-packet", + "solana-packet 3.0.0", "solana-pubkey 3.0.0", "solana-sdk-ids", "solana-short-vec", "solana-signature", - "solana-svm-transaction", - "solana-transaction-context", + "solana-svm-transaction 3.1.12", + "solana-transaction-context 3.1.12", ] [[package]] name = "agave-votor-messages" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45cdc13389d98584389d139218d11f7cdb2508150727819f6343934b88954dd0" +checksum = "aabbb79ba0e6169080fd8d57e72c8d07d99ed78cfd6212943288e59b6c9ce568" dependencies = [ "agave-logger", "serde", - "solana-bls-signatures", + "solana-bls-signatures 1.0.0", "solana-clock", "solana-hash 3.1.0", ] @@ -364,9 +459,52 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "anza-quinn" +version = "0.11.9-rustsec20260037" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91bfa08f6e7e4187354ff4f793b81cc08218a6a95cc48f5de7616d44452bf6e0" +dependencies = [ + "anza-quinn-proto", + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "anza-quinn-proto" +version = "0.11.13-rustsec20260037" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "d00a4d8cf8d72ee56e0ee20d3b4eef4785ce1b05299c4982c6de7f251c458efe" +dependencies = [ + "bytes", + "fastbloom", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier 0.6.2", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] [[package]] name = "aquamarine" @@ -379,7 +517,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -508,7 +646,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" dependencies = [ "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -534,7 +672,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -609,7 +747,7 @@ checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -845,7 +983,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -862,7 +1000,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -1131,25 +1269,26 @@ dependencies = [ [[package]] name = "borsh" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" dependencies = [ "borsh-derive", + "bytes", "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +checksum = "3ae8fb4fb5740e4b2c4884ff95f5f32f5e8479db1e8fd8eb49ddbe09eb09bb7c" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -1243,7 +1382,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -1328,7 +1467,7 @@ checksum = "45565fc9416b9896014f5732ac776f810ee53a66730c17e4020c3ec064a8f88f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -1386,7 +1525,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -1416,7 +1555,7 @@ dependencies = [ "serde", "solana-accounts-db", "solana-lattice-hash", - "solana-pubkey 4.0.0", + "solana-pubkey 4.1.0", "tar", "tikv-jemallocator", "tokio", @@ -1431,10 +1570,14 @@ dependencies = [ name = "cloudbreak-api" version = "0.1.0" dependencies = [ + "agave-feature-set 4.0.0", + "agave-precompiles 4.0.0", + "agave-syscalls 4.0.0", "anyhow", "async-stream", "async-trait", "base64 0.22.1", + "bincode", "bs58", "bytes", "chrono", @@ -1460,10 +1603,32 @@ dependencies = [ "solana-account", "solana-account-decoder", "solana-account-decoder-client-types", + "solana-address-lookup-table-interface", + "solana-bpf-loader-program 4.0.0", + "solana-builtins 4.0.0", + "solana-clock", "solana-commitment-config", - "solana-pubkey 4.0.0", + "solana-compute-budget 4.0.0", + "solana-compute-budget-instruction 4.0.0", + "solana-fee-structure", + "solana-hash 4.2.0", + "solana-loader-v3-interface", + "solana-message", + "solana-nonce-account", + "solana-precompile-error", + "solana-program-runtime 4.0.0", + "solana-pubkey 4.1.0", + "solana-rent 3.1.0", "solana-rpc-client-api", - "solana-vote-interface", + "solana-sdk-ids", + "solana-svm 4.0.0", + "solana-svm-callback 4.0.0", + "solana-svm-feature-set 4.0.0", + "solana-svm-transaction 4.0.0", + "solana-transaction", + "solana-transaction-error", + "solana-transaction-status-client-types", + "solana-vote-interface 4.0.4", "spl-token-2022", "spl-token-2022-interface", "spl-token-interface", @@ -1491,7 +1656,7 @@ dependencies = [ "serde", "serde_json", "solana-program", - "solana-pubkey 4.0.0", + "solana-pubkey 4.1.0", "solana-rpc-client-api", "thiserror 2.0.18", "tokio", @@ -1511,7 +1676,7 @@ dependencies = [ "sea-orm", "serde", "serde_json", - "solana-pubkey 4.0.0", + "solana-pubkey 4.1.0", "tokio", "toml", ] @@ -1526,7 +1691,7 @@ dependencies = [ "solana-account", "solana-account-decoder", "solana-account-decoder-client-types", - "solana-pubkey 4.0.0", + "solana-pubkey 4.1.0", "solana-rpc-client-api", "thiserror 2.0.18", ] @@ -1557,9 +1722,9 @@ dependencies = [ "solana-client", "solana-lattice-hash", "solana-program", - "solana-pubkey 4.0.0", + "solana-pubkey 4.1.0", "solana-stake-interface", - "solana-vote-interface", + "solana-vote-interface 4.0.4", "thiserror 2.0.18", "tokio", "tracing", @@ -1575,7 +1740,7 @@ dependencies = [ "cloudbreak-core", "hex", "sea-orm-migration", - "solana-pubkey 4.0.0", + "solana-pubkey 4.1.0", ] [[package]] @@ -1598,7 +1763,7 @@ dependencies = [ "sea-orm", "serde", "serde_json", - "solana-pubkey 4.0.0", + "solana-pubkey 4.1.0", "solana-rpc-client-api", "thiserror 2.0.18", "tokio", @@ -1629,11 +1794,11 @@ dependencies = [ "solana-epoch-schedule", "solana-fee-calculator", "solana-hard-forks", - "solana-hash 4.1.0", + "solana-hash 4.2.0", "solana-inflation", "solana-lattice-hash", "solana-program", - "solana-pubkey 4.0.0", + "solana-pubkey 4.1.0", "solana-runtime", "solana-serde", "solana-stake-interface", @@ -1924,7 +2089,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -1947,6 +2112,16 @@ dependencies = [ "darling_macro 0.21.3", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + [[package]] name = "darling_core" version = "0.20.11" @@ -1957,7 +2132,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -1971,7 +2146,20 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.114", + "syn 2.0.118", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", ] [[package]] @@ -1982,7 +2170,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -1993,7 +2181,18 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.114", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.118", ] [[package]] @@ -2087,7 +2286,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.114", + "syn 2.0.118", "unicode-xid", ] @@ -2135,7 +2334,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -2158,7 +2357,7 @@ checksum = "a6cbae11b3de8fce2a456e8ea3dada226b35fe791f0dc1d360c0941f0bb681f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -2268,7 +2467,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -2323,6 +2522,15 @@ dependencies = [ "enum-iterator-derive", ] +[[package]] +name = "enum-iterator" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4549325971814bda7a44061bf3fe7e487d447cba01e4220a4b454d630d7a016" +dependencies = [ + "enum-iterator-derive", +] + [[package]] name = "enum-iterator-derive" version = "1.5.0" @@ -2331,7 +2539,7 @@ checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -2351,7 +2559,7 @@ checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -2439,7 +2647,7 @@ checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" dependencies = [ "getrandom 0.3.4", "libm", - "rand 0.9.2", + "rand 0.9.4", "siphasher 1.0.2", ] @@ -2683,7 +2891,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -3481,7 +3689,7 @@ checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -3509,7 +3717,7 @@ dependencies = [ "sea-orm", "serde", "serde_json", - "solana-pubkey 4.0.0", + "solana-pubkey 4.1.0", "tokio", "toml", "tracing", @@ -3613,7 +3821,7 @@ checksum = "ff7a39c8862fc1369215ccf0a8f12dd4598c7f6484704359f0351bd617034dbf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -3732,7 +3940,7 @@ dependencies = [ "jsonrpsee-types", "parking_lot", "pin-project", - "rand 0.9.2", + "rand 0.9.4", "rustc-hash", "serde", "serde_json", @@ -3777,7 +3985,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -4200,7 +4408,7 @@ checksum = "59b43b4fd69e3437618106f7754f34021b831a514f9e1a98ae863cabcd8d8dad" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -4351,7 +4559,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -4408,9 +4616,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -4418,14 +4626,14 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -4478,7 +4686,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -4585,7 +4793,7 @@ dependencies = [ "futures-util", "opentelemetry", "percent-encoding", - "rand 0.9.2", + "rand 0.9.4", "thiserror 2.0.18", "tokio", "tokio-stream", @@ -4621,7 +4829,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -4668,6 +4876,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + [[package]] name = "pbkdf2" version = "0.11.0" @@ -4747,7 +4961,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -4902,7 +5116,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -4933,7 +5147,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -4953,7 +5167,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", "version_check", "yansi", ] @@ -5002,7 +5216,7 @@ dependencies = [ "pulldown-cmark", "pulldown-cmark-to-cmark", "regex", - "syn 2.0.114", + "syn 2.0.118", "tempfile", ] @@ -5016,7 +5230,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -5114,7 +5328,7 @@ checksum = "9e2e25ee72f5b24d773cae88422baddefff7714f97aab68d96fe2b6fc4a28fb2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -5159,15 +5373,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", - "fastbloom", "getrandom 0.3.4", "lru-slab", - "rand 0.9.2", + "rand 0.9.4", "ring", "rustc-hash", "rustls", "rustls-pki-types", - "rustls-platform-verifier 0.6.2", "slab", "thiserror 2.0.18", "tinyvec", @@ -5191,9 +5403,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -5236,9 +5448,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -5807,7 +6019,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -5869,7 +6081,7 @@ dependencies = [ "proc-macro2", "quote", "sea-bae", - "syn 2.0.114", + "syn 2.0.118", "unicode-ident", ] @@ -5932,7 +6144,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", "thiserror 2.0.18", ] @@ -5958,7 +6170,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -6019,9 +6231,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "send_wrapper" @@ -6084,7 +6296,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -6140,7 +6352,7 @@ dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -6191,6 +6403,12 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + [[package]] name = "sha3" version = "0.10.8" @@ -6350,16 +6568,16 @@ dependencies = [ "solana-account-info", "solana-clock", "solana-instruction-error", - "solana-pubkey 4.0.0", + "solana-pubkey 4.1.0", "solana-sdk-ids", "solana-sysvar", ] [[package]] name = "solana-account-decoder" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66939b3e7aa0fab7a523bbb0d0518e3cdbb6a9b8675d5ae3a7bba5c1bef98622" +checksum = "8dbde78ccf7c3e14bc5ab230da1dfadfc2dc84b860cd2ffa39d0fd3030f7df4a" dependencies = [ "Inflector", "base64 0.22.1", @@ -6387,7 +6605,7 @@ dependencies = [ "solana-slot-history", "solana-stake-interface", "solana-sysvar", - "solana-vote-interface", + "solana-vote-interface 4.0.4", "spl-generic-token", "spl-token-2022-interface", "spl-token-group-interface", @@ -6399,9 +6617,9 @@ dependencies = [ [[package]] name = "solana-account-decoder-client-types" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcf86e96f5e986687edc572033df43723b885c668fa1a3280753232dc8f3656" +checksum = "22302265956e8f403cb0721bef0a79137dc1a9a25c95d732d101877629510700" dependencies = [ "base64 0.22.1", "bs58", @@ -6414,22 +6632,22 @@ dependencies = [ [[package]] name = "solana-account-info" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3397241392f5756925029acaa8515dc70fcbe3d8059d4885d7d6533baf64fd" +checksum = "a9cf16495d9eb53e3d04e72366a33bb1c20c24e78c171d8b8f5978357b63ae95" dependencies = [ "bincode", "serde_core", - "solana-address 2.1.0", + "solana-address 2.6.1", "solana-program-error", "solana-program-memory", ] [[package]] name = "solana-accounts-db" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "774af16dab3435a64c83f88027f17ef72a4c56afedbbf4a0f799cd242199beab" +checksum = "7c1b9b25dbe0ae7f7df8a578a8a99418720ddab92a0bac8ac11f4af7a91889c6" dependencies = [ "agave-fs", "ahash 0.8.12", @@ -6471,12 +6689,12 @@ dependencies = [ "solana-reward-info", "solana-sha256-hasher", "solana-slot-hashes", - "solana-svm-transaction", + "solana-svm-transaction 3.1.12", "solana-system-interface 2.0.0", "solana-sysvar", "solana-time-utils", "solana-transaction", - "solana-transaction-context", + "solana-transaction-context 3.1.12", "solana-transaction-error", "spl-generic-token", "static_assertions", @@ -6490,14 +6708,14 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2ecac8e1b7f74c2baa9e774c42817e3e75b20787134b76cc4d45e8a604488f5" dependencies = [ - "solana-address 2.1.0", + "solana-address 2.6.1", ] [[package]] name = "solana-address" -version = "2.1.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "998227476aed49e1c63dec0e89341b768a2cf3bd22913c3ed8baa985cda882c9" +checksum = "39c93e262f671bf402e1040e4a7e40b05d81da5956c7681948c975a0997517bb" dependencies = [ "borsh", "bytemuck", @@ -6505,15 +6723,16 @@ dependencies = [ "curve25519-dalek 4.1.3", "five8", "five8_const", - "rand 0.9.2", + "rand 0.9.4", "serde", "serde_derive", + "sha2-const-stable", "solana-atomic-u64", - "solana-define-syscall 5.0.0", + "solana-define-syscall 5.1.0", "solana-program-error", "solana-sanitize", "solana-sha256-hasher", - "wincode", + "wincode 0.5.5", ] [[package]] @@ -6529,7 +6748,7 @@ dependencies = [ "solana-clock", "solana-instruction", "solana-instruction-error", - "solana-pubkey 4.0.0", + "solana-pubkey 4.1.0", "solana-sdk-ids", "solana-slot-hashes", ] @@ -6573,7 +6792,7 @@ checksum = "7116e1d942a2432ca3f514625104757ab8a56233787e95144c93950029e31176" dependencies = [ "blake3", "solana-define-syscall 4.0.1", - "solana-hash 4.1.0", + "solana-hash 4.2.0", ] [[package]] @@ -6600,6 +6819,26 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "solana-bls-signatures" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a3d8a6e1a009bddbdbfe13ee6ff206c16afa9f8fae7d04612d779ac2254ad5f" +dependencies = [ + "base64 0.22.1", + "blst", + "blstrs", + "cfg_eval", + "ff", + "group", + "pairing", + "rand 0.8.5", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.18", +] + [[package]] name = "solana-bn254" version = "3.2.1" @@ -6611,26 +6850,26 @@ dependencies = [ "ark-ff 0.5.0", "ark-serialize 0.5.0", "bytemuck", - "solana-define-syscall 5.0.0", + "solana-define-syscall 5.1.0", "thiserror 2.0.18", ] [[package]] name = "solana-borsh" -version = "3.0.0" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc402b16657abbfa9991cd5cbfac5a11d809f7e7d28d3bb291baeb088b39060e" +checksum = "c04abbae16f57178a163125805637b8a076175bb5c0002fb04f4792bea901cf7" dependencies = [ "borsh", ] [[package]] name = "solana-bpf-loader-program" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc7d30e90589489d4ef93ada64811c0b23297736ca953c2ecc94bf1bd7087d4" +checksum = "044a6c5327755992853454ea5ec495edd987dab53a6f9029e695bf0d43ab55dc" dependencies = [ - "agave-syscalls", + "agave-syscalls 3.1.12", "bincode", "qualifier_attr", "solana-account", @@ -6639,25 +6878,54 @@ dependencies = [ "solana-instruction", "solana-loader-v3-interface", "solana-loader-v4-interface", - "solana-packet", + "solana-packet 3.0.0", "solana-program-entrypoint", - "solana-program-runtime", + "solana-program-runtime 3.1.12", "solana-pubkey 3.0.0", - "solana-sbpf", + "solana-sbpf 0.13.1", "solana-sdk-ids", - "solana-svm-feature-set", - "solana-svm-log-collector", - "solana-svm-measure", - "solana-svm-type-overrides", + "solana-svm-feature-set 3.1.12", + "solana-svm-log-collector 3.1.12", + "solana-svm-measure 3.1.12", + "solana-svm-type-overrides 3.1.12", "solana-system-interface 2.0.0", - "solana-transaction-context", + "solana-transaction-context 3.1.12", +] + +[[package]] +name = "solana-bpf-loader-program" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219bfba64973ac9e64aa181f03fd56ac319e2d50d8a23d16c54bbd7fa9807a47" +dependencies = [ + "agave-syscalls 4.0.0", + "bincode", + "qualifier_attr", + "solana-account", + "solana-bincode", + "solana-clock", + "solana-instruction", + "solana-loader-v3-interface", + "solana-loader-v4-interface", + "solana-packet 4.1.0", + "solana-program-entrypoint", + "solana-program-runtime 4.0.0", + "solana-pubkey 4.1.0", + "solana-sbpf 0.14.4", + "solana-sdk-ids", + "solana-svm-feature-set 4.0.0", + "solana-svm-log-collector 4.0.0", + "solana-svm-measure 4.0.0", + "solana-svm-type-overrides 4.0.0", + "solana-system-interface 3.1.0", + "solana-transaction-context 4.0.0", ] [[package]] name = "solana-bucket-map" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77b879a0ec6836148c4c571357f1852b8e7bf70e2f6212f560a9fd81f98a6965" +checksum = "86498fab926fe0989217966e6cae4d871152e94abdbaa47db5989fd94094cbca" dependencies = [ "bv", "bytemuck", @@ -6674,48 +6942,87 @@ dependencies = [ [[package]] name = "solana-builtins" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1eb800aef9a1dc85195c088e9c0e4786f0c7aa22348c53892f2773e234408be3" +checksum = "eaf44075aa3dbe16ce0112314d0dfd2435a18ddfd96f53e5269879f4006eb60d" dependencies = [ - "agave-feature-set", - "solana-bpf-loader-program", - "solana-compute-budget-program", + "agave-feature-set 3.1.12", + "solana-bpf-loader-program 3.1.12", + "solana-compute-budget-program 3.1.12", "solana-hash 3.1.0", - "solana-loader-v4-program", - "solana-program-runtime", + "solana-loader-v4-program 3.1.12", + "solana-program-runtime 3.1.12", "solana-pubkey 3.0.0", "solana-sdk-ids", - "solana-system-program", - "solana-vote-program", - "solana-zk-elgamal-proof-program", - "solana-zk-token-proof-program", + "solana-system-program 3.1.12", + "solana-vote-program 3.1.12", + "solana-zk-elgamal-proof-program 3.1.12", + "solana-zk-token-proof-program 3.1.12", +] + +[[package]] +name = "solana-builtins" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dda9d147935c741533496edf72c5b712885d4793a0bca13a21bd75d8f5dc30e9" +dependencies = [ + "agave-feature-set 4.0.0", + "solana-bpf-loader-program 4.0.0", + "solana-compute-budget-program 4.0.0", + "solana-hash 4.2.0", + "solana-loader-v4-program 4.0.0", + "solana-program-runtime 4.0.0", + "solana-pubkey 4.1.0", + "solana-sdk-ids", + "solana-system-program 4.0.0", + "solana-vote-program 4.0.0", + "solana-zk-elgamal-proof-program 4.0.0", + "solana-zk-token-proof-program 4.0.0", ] [[package]] name = "solana-builtins-default-costs" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abdf819d105e2afa3ecd651d06514764bb567395cd91c25b4a51ea8d0a6b426" +checksum = "5c6cd4d41f391f427e64e03fb00fb0c93443f18464dafd71e06f996ac03a3982" dependencies = [ - "agave-feature-set", + "agave-feature-set 3.1.12", "ahash 0.8.12", "log", - "solana-bpf-loader-program", - "solana-compute-budget-program", - "solana-loader-v4-program", + "solana-bpf-loader-program 3.1.12", + "solana-compute-budget-program 3.1.12", + "solana-loader-v4-program 3.1.12", "solana-pubkey 3.0.0", "solana-sdk-ids", - "solana-system-program", - "solana-vote-program", + "solana-system-program 3.1.12", + "solana-vote-program 3.1.12", +] + +[[package]] +name = "solana-builtins-default-costs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3167997e8ac0fe100c4ed54503568d22204aeda56f4d3549e0c09a700b609aa8" +dependencies = [ + "agave-feature-set 4.0.0", + "ahash 0.8.12", + "log", + "solana-bpf-loader-program 4.0.0", + "solana-compute-budget-program 4.0.0", + "solana-loader-v4-program 4.0.0", + "solana-pubkey 4.1.0", + "solana-sdk-ids", + "solana-system-program 4.0.0", + "solana-vote-program 4.0.0", ] [[package]] name = "solana-client" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061e7290051a639e0efe8078b6c8c7ebe99d13f56ee651b93c0529fba012b99d" +checksum = "44eb63815525a855b1f6d41a6b929d51c9fe8079b844075a48c645315ba01b60" dependencies = [ + "anza-quinn", "async-trait", "bincode", "dashmap", @@ -6724,7 +7031,6 @@ dependencies = [ "indexmap", "indicatif", "log", - "quinn", "rayon", "solana-account", "solana-client-traits", @@ -6781,12 +7087,13 @@ dependencies = [ [[package]] name = "solana-clock" -version = "3.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb62e9381182459a4520b5fe7fb22d423cae736239a6427fc398a88743d0ed59" +checksum = "f0acdace90d96e2c9e70d681465b4fe888b6bcf27c354ae9774e9f8a3b72923d" dependencies = [ "serde", "serde_derive", + "solana-get-sysvar", "solana-sdk-ids", "solana-sdk-macro", "solana-sysvar-id", @@ -6805,9 +7112,9 @@ dependencies = [ [[package]] name = "solana-commitment-config" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e41a3917076a8b5375809078ae3a6fb76a53e364b596ef8c4265e7f410876f3" +checksum = "1517aa49dcfa9cb793ef90e7aac81346d62ca4a546bb1a754030a033e3972e1c" dependencies = [ "serde", "serde_derive", @@ -6815,31 +7122,62 @@ dependencies = [ [[package]] name = "solana-compute-budget" -version = "3.1.8" +version = "3.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55267bed68ed018f6b2fd5e2f7ae694cbcb1a9bfd98b749803e94be7b5f08774" +dependencies = [ + "solana-fee-structure", + "solana-program-runtime 3.1.12", +] + +[[package]] +name = "solana-compute-budget" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2fe14d00d8e4092523c58b0ebfce03d8dd6a5cb778df7d7262bb2c2acff50e3" +checksum = "b591fbaed6d9ab4cba6a5a82eb5df208072ced2e5b74c59e9d309ff87af0615f" dependencies = [ "solana-fee-structure", - "solana-program-runtime", + "solana-program-runtime 4.0.0", ] [[package]] name = "solana-compute-budget-instruction" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f1a6de0397f6fe4a2a4d68ccc5925028d41e83fc424514e93095f9709c363" +checksum = "d6bf47dcc57b770cee0432899ac89cd7e85896f9e28e3d5e5542a0b8a8f5839f" dependencies = [ - "agave-feature-set", + "agave-feature-set 3.1.12", "log", "solana-borsh", - "solana-builtins-default-costs", - "solana-compute-budget", + "solana-builtins-default-costs 3.1.12", + "solana-compute-budget 3.1.12", "solana-compute-budget-interface", "solana-instruction", - "solana-packet", + "solana-packet 3.0.0", "solana-pubkey 3.0.0", "solana-sdk-ids", - "solana-svm-transaction", + "solana-svm-transaction 3.1.12", + "solana-transaction-error", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-compute-budget-instruction" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006d9b6a34f9d7b719100653317990ed55e572107702104c054133b40f587306" +dependencies = [ + "agave-feature-set 4.0.0", + "log", + "solana-borsh", + "solana-builtins-default-costs 4.0.0", + "solana-compute-budget 4.0.0", + "solana-compute-budget-interface", + "solana-instruction", + "solana-packet 4.1.0", + "solana-pubkey 4.1.0", + "solana-sdk-ids", + "solana-svm-transaction 4.0.0", "solana-transaction-error", "thiserror 2.0.18", ] @@ -6857,11 +7195,20 @@ dependencies = [ [[package]] name = "solana-compute-budget-program" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b83c297d29952206a455ec06fbe1e47f32711a13584ae1a6248f6ce0399a0f8" +checksum = "bcc87532d1ca1b871204cdb103514182bd9460cea403c16f22ca49e1ba063ea1" dependencies = [ - "solana-program-runtime", + "solana-program-runtime 3.1.12", +] + +[[package]] +name = "solana-compute-budget-program" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a22bcf5088ebe5cb2aa548580d0a466de813032b425707a7745a2a63a7764cdc" +dependencies = [ + "solana-program-runtime 4.0.0", ] [[package]] @@ -6883,9 +7230,9 @@ dependencies = [ [[package]] name = "solana-connection-cache" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0cf8656543f9c391b1fd06397038c1586162cbd4630274580cfc0993388b3ae" +checksum = "ba5aa88d3ead799eec9580bf9c3d3ca1ae966430809fdf565a407abdc90798b6" dependencies = [ "async-trait", "bincode", @@ -6906,30 +7253,30 @@ dependencies = [ [[package]] name = "solana-cost-model" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a72fa5a0dd6b1438574a1cce6b5c8db0f525d35680773ec58cd907261012fd" +checksum = "daa79df27ff7b933c4cb7612340d9af3b4f49b0219bec81ad6826e9352ad054f" dependencies = [ - "agave-feature-set", + "agave-feature-set 3.1.12", "ahash 0.8.12", "log", "solana-bincode", "solana-borsh", - "solana-builtins-default-costs", + "solana-builtins-default-costs 3.1.12", "solana-clock", - "solana-compute-budget", - "solana-compute-budget-instruction", + "solana-compute-budget 3.1.12", + "solana-compute-budget-instruction 3.1.12", "solana-compute-budget-interface", "solana-fee-structure", "solana-metrics", - "solana-packet", + "solana-packet 3.0.0", "solana-pubkey 3.0.0", "solana-runtime-transaction", "solana-sdk-ids", - "solana-svm-transaction", + "solana-svm-transaction 3.1.12", "solana-system-interface 2.0.0", "solana-transaction-error", - "solana-vote-program", + "solana-vote-program 3.1.12", ] [[package]] @@ -6942,15 +7289,15 @@ dependencies = [ "solana-define-syscall 4.0.1", "solana-instruction", "solana-program-error", - "solana-pubkey 4.0.0", + "solana-pubkey 4.1.0", "solana-stable-layout", ] [[package]] name = "solana-curve25519" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "737ede9143c36b8628cc11d920cdb762cd1ccbd7ca904c3bd63b39c58669fe38" +checksum = "2eb091ac9c1e4d51c3cd1893444aee607cd1b0173c4ba0b557f8960844f44a1b" dependencies = [ "bytemuck", "bytemuck_derive", @@ -6960,6 +7307,20 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "solana-curve25519" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b4d2a4bf0d0b0a86c22111917e86e8bd39a7b31420fb2c7d73eb83761fc7af" +dependencies = [ + "bytemuck", + "bytemuck_derive", + "curve25519-dalek 4.1.3", + "solana-define-syscall 5.1.0", + "subtle", + "thiserror 2.0.18", +] + [[package]] name = "solana-define-syscall" version = "3.0.0" @@ -6974,9 +7335,9 @@ checksum = "57e5b1c0bc1d4a4d10c88a4100499d954c09d3fecfae4912c1a074dff68b1738" [[package]] name = "solana-define-syscall" -version = "5.0.0" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03aacdd7a61e2109887a7a7f046caebafce97ddf1150f33722eeac04f9039c73" +checksum = "21e14a4f604117f379840956a8fc8695e4c84f5b0ebed192f31f60d9b85d581d" [[package]] name = "solana-derivation-path" @@ -7013,13 +7374,13 @@ dependencies = [ [[package]] name = "solana-epoch-rewards" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b319a4ed70390af911090c020571f0ff1f4ec432522d05ab89f5c08080381995" +checksum = "f5e7b0ba210593ba8ddd39d6d234d81795d1671cebf3026baa10d5dc23ac42f0" dependencies = [ "serde", "serde_derive", - "solana-hash 3.1.0", + "solana-hash 4.2.0", "solana-sdk-ids", "solana-sdk-macro", "solana-sysvar-id", @@ -7032,8 +7393,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ee8beac9bff4db9225e57d532d169b0be5e447f1e6601a2f50f27a01bf5518f" dependencies = [ "siphasher 0.3.11", - "solana-address 2.1.0", - "solana-hash 4.1.0", + "solana-address 2.6.1", + "solana-hash 4.2.0", ] [[package]] @@ -7093,28 +7454,28 @@ dependencies = [ "solana-account-info", "solana-instruction", "solana-program-error", - "solana-pubkey 4.0.0", - "solana-rent 4.0.0", + "solana-pubkey 4.1.0", + "solana-rent 4.3.0", "solana-sdk-ids", - "solana-system-interface 3.0.0", + "solana-system-interface 3.1.0", ] [[package]] name = "solana-fee" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1e6bdbeeaab926dee7830046ce8c18e8fc3ccb324f6eb4f100c240dfd61fe45" +checksum = "f0c7ab7f31d404a130c78c29a7a845cd599b2623fa335e5fcaef2bd7b3be83e6" dependencies = [ - "agave-feature-set", + "agave-feature-set 3.1.12", "solana-fee-structure", - "solana-svm-transaction", + "solana-svm-transaction 3.1.12", ] [[package]] name = "solana-fee-calculator" -version = "3.0.0" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a73cc03ca4bed871ca174558108835f8323e85917bb38b9c81c7af2ab853efe" +checksum = "ef67f01cc6a0c72e99a08d0d484683f995de4c80e9568728fa77d1537f9b7e09" dependencies = [ "log", "serde", @@ -7160,6 +7521,17 @@ dependencies = [ "solana-time-utils", ] +[[package]] +name = "solana-get-sysvar" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef3bc859fc036ed490146793557386cbfae614ebba4adc704c37d94350824ed4" +dependencies = [ + "solana-address 2.6.1", + "solana-define-syscall 5.1.0", + "solana-program-error", +] + [[package]] name = "solana-hard-forks" version = "3.0.0" @@ -7176,14 +7548,14 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "337c246447142f660f778cf6cb582beba8e28deb05b3b24bfb9ffd7c562e5f41" dependencies = [ - "solana-hash 4.1.0", + "solana-hash 4.2.0", ] [[package]] name = "solana-hash" -version = "4.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b6100d68f90726ddb4d2ac7d00e8b6cf9ce8e4ccdfbb9112b1d766045753241" +checksum = "8064ea1d591ec791be95245058ca40f4f5345d390c200069d0f79bbf55bfae55" dependencies = [ "borsh", "bytemuck", @@ -7193,14 +7565,14 @@ dependencies = [ "serde_derive", "solana-atomic-u64", "solana-sanitize", - "wincode", + "wincode 0.4.9", ] [[package]] name = "solana-inflation" -version = "3.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e92f37a14e7c660628752833250dd3dcd8e95309876aee751d7f8769a27947c6" +checksum = "ccf104167e42e747602b88e02b25cacfc5de699c3b7cbba60d3250437e6a22ed" dependencies = [ "serde", "serde_derive", @@ -7208,24 +7580,24 @@ dependencies = [ [[package]] name = "solana-instruction" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee1b699a2c1518028a9982e255e0eca10c44d90006542d9d7f9f40dbce3f7c78" +checksum = "c6a6d22d0a6fdf345be294bb9afdcd40c296cdc095e64e7ceaa3bb3c2f608c1c" dependencies = [ "bincode", "borsh", "serde", "serde_derive", - "solana-define-syscall 4.0.1", + "solana-define-syscall 5.1.0", "solana-instruction-error", - "solana-pubkey 4.0.0", + "solana-pubkey 4.1.0", ] [[package]] name = "solana-instruction-error" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b04259e03c05faf38a8c24217b5cfe4c90572ae6184ab49cddb1584fdd756d3f" +checksum = "7d3d048edaaeef5a3dc8c01853e585539a74417e4c2d43a9e2c161270045b838" dependencies = [ "num-traits", "serde", @@ -7259,20 +7631,21 @@ checksum = "ed1c0d16d6fdeba12291a1f068cdf0d479d9bff1141bf44afd7aa9d485f65ef8" dependencies = [ "sha3", "solana-define-syscall 4.0.1", - "solana-hash 4.1.0", + "solana-hash 4.2.0", ] [[package]] name = "solana-keypair" -version = "3.1.0" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8be597c9e231b0cab2928ce3bc3e4ee77d9c0ad92977b9d901f3879f25a7a" +checksum = "263d614c12aa267a3278703175fd6440552ca61bc960b5a02a4482720c53438b" dependencies = [ "ed25519-dalek 2.2.0", "ed25519-dalek-bip32", "five8", - "rand 0.8.5", - "solana-address 2.1.0", + "five8_core", + "rand 0.9.4", + "solana-address 2.6.1", "solana-derivation-path", "solana-seed-derivable", "solana-seed-phrase", @@ -7282,12 +7655,13 @@ dependencies = [ [[package]] name = "solana-last-restart-slot" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcda154ec827f5fc1e4da0af3417951b7e9b8157540f81f936c4a8b1156134d0" +checksum = "c22474b83d3c7c318e1c3a725784fc2d1d03b728e36369e58ce48769a61ed85e" dependencies = [ "serde", "serde_derive", + "solana-get-sysvar", "solana-sdk-ids", "solana-sdk-macro", "solana-sysvar-id", @@ -7295,9 +7669,9 @@ dependencies = [ [[package]] name = "solana-lattice-hash" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ed689cb9fdb3b8540686d7b023dbaef1ecc51f0fcad98c9000bbdbecd829e8" +checksum = "61adccfdcbb8d847813ebf2805bc51d3d05cf51af243d52712bf88e39660c965" dependencies = [ "base64 0.22.1", "blake3", @@ -7337,33 +7711,57 @@ dependencies = [ [[package]] name = "solana-loader-v4-program" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc6ca85532321c1e4ae6b0024f6b23267e742635aec19cac744a54a37ee5764" +checksum = "1e88b98ba6b408fe6fcf180784fc378d07ae63100c7dd413ff97474b6de30c5d" dependencies = [ "log", "solana-account", "solana-bincode", - "solana-bpf-loader-program", + "solana-bpf-loader-program 3.1.12", "solana-instruction", "solana-loader-v3-interface", "solana-loader-v4-interface", - "solana-packet", - "solana-program-runtime", + "solana-packet 3.0.0", + "solana-program-runtime 3.1.12", "solana-pubkey 3.0.0", - "solana-sbpf", + "solana-sbpf 0.13.1", "solana-sdk-ids", - "solana-svm-log-collector", - "solana-svm-measure", - "solana-svm-type-overrides", - "solana-transaction-context", + "solana-svm-log-collector 3.1.12", + "solana-svm-measure 3.1.12", + "solana-svm-type-overrides 3.1.12", + "solana-transaction-context 3.1.12", +] + +[[package]] +name = "solana-loader-v4-program" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b5191cd34f04e4ec9fd5f2ac8a431ba9ffd6c827511fd35f2cae0256a0c6b12" +dependencies = [ + "log", + "solana-account", + "solana-bincode", + "solana-bpf-loader-program 4.0.0", + "solana-instruction", + "solana-loader-v3-interface", + "solana-loader-v4-interface", + "solana-packet 4.1.0", + "solana-program-runtime 4.0.0", + "solana-pubkey 4.1.0", + "solana-sbpf 0.14.4", + "solana-sdk-ids", + "solana-svm-log-collector 4.0.0", + "solana-svm-measure 4.0.0", + "solana-svm-type-overrides 4.0.0", + "solana-transaction-context 4.0.0", ] [[package]] name = "solana-measure" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8c8288f2b0755aaec2bae772239a48408e076a9b90db40c936f1fa5debbc78" +checksum = "de496c3f0e0cee33abd4ae958a8703e04ae1011f3c3e73b641bf319a18301c01" [[package]] name = "solana-message" @@ -7376,8 +7774,8 @@ dependencies = [ "lazy_static", "serde", "serde_derive", - "solana-address 2.1.0", - "solana-hash 4.1.0", + "solana-address 2.6.1", + "solana-hash 4.2.0", "solana-instruction", "solana-sanitize", "solana-sdk-ids", @@ -7387,9 +7785,9 @@ dependencies = [ [[package]] name = "solana-metrics" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac53c2ae91e227cd1f0aa9f744beca638a2e971b84d7c2f008cf2c75af1b0d5" +checksum = "6c410a6dcc5e1d564bc7b6e3b361ecf9106d99535db74f27326e22f7ab7d6151" dependencies = [ "crossbeam-channel", "gethostname", @@ -7403,11 +7801,11 @@ dependencies = [ [[package]] name = "solana-msg" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "264275c556ea7e22b9d3f87d56305546a38d4eee8ec884f3b126236cb7dcbbb4" +checksum = "726b7cbbc6be6f1c6f29146ac824343b9415133eee8cce156452ad1db93f8008" dependencies = [ - "solana-define-syscall 3.0.0", + "solana-define-syscall 5.1.0", ] [[package]] @@ -7418,9 +7816,9 @@ checksum = "ae8dd4c280dca9d046139eb5b7a5ac9ad10403fbd64964c7d7571214950d758f" [[package]] name = "solana-net-utils" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23848218da169349b400780ee5d8d683792304115d4b675f0d9e0b8949433eb2" +checksum = "62092362c96ef693c50f00d98932eb3fca99efba0261bee18d57c71264befdb2" dependencies = [ "anyhow", "bincode", @@ -7434,7 +7832,7 @@ dependencies = [ "serde", "socket2 0.6.2", "solana-serde", - "solana-svm-type-overrides", + "solana-svm-type-overrides 3.1.12", "tokio", "url", ] @@ -7447,15 +7845,15 @@ checksum = "8b8a731ed60e89177c8a7ab05fe0f1511cedd3e70e773f288f9de33a9cfdc21e" [[package]] name = "solana-nonce" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abbdc6c8caf1c08db9f36a50967539d0f72b9f1d4aea04fec5430f532e5afadc" +checksum = "cbc469152a63284ef959b80c59cda015262a021da55d3b8fe42171d89c4b64f8" dependencies = [ "serde", "serde_derive", "solana-fee-calculator", - "solana-hash 3.1.0", - "solana-pubkey 3.0.0", + "solana-hash 4.2.0", + "solana-pubkey 4.1.0", "solana-sha256-hasher", ] @@ -7485,11 +7883,21 @@ dependencies = [ "serde_with", ] +[[package]] +name = "solana-packet" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad62e1045c2347a0c0e219a6ceb0abfe904be622920996bfcac8d116fabe3c7" +dependencies = [ + "bitflags 2.10.0", + "solana-pubkey 4.1.0", +] + [[package]] name = "solana-perf" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87608537c53ca0976f04166691fc11523e93802a50660b719e62e172dd47cf10" +checksum = "6efdfcbe201b25d6c30a69bb27201867d3da3c945dd06b181e61d5b956280663" dependencies = [ "ahash 0.8.12", "bincode", @@ -7508,14 +7916,14 @@ dependencies = [ "solana-hash 3.1.0", "solana-message", "solana-metrics", - "solana-packet", + "solana-packet 3.0.0", "solana-pubkey 3.0.0", "solana-rayon-threadlimit", "solana-sdk-ids", "solana-short-vec", "solana-signature", "solana-time-utils", - "solana-transaction-context", + "solana-transaction-context 3.1.12", ] [[package]] @@ -7530,9 +7938,9 @@ dependencies = [ [[package]] name = "solana-poseidon" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9b2cf3486543d8d5abf916b99ab383b5c8fc83ea091eafe3761e4af667e49e2" +checksum = "a44739c6c9f717fd057f7c16fba65fdd3628d4918c61c399520e6f4fba5ad854" dependencies = [ "ark-bn254 0.4.0", "ark-bn254 0.5.0", @@ -7542,6 +7950,20 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "solana-poseidon" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "737b8ab25bf4cc8e618f80f1fe40709b2ace708bc764a36b8a4c81eea8c07034" +dependencies = [ + "ark-bn254 0.4.0", + "ark-bn254 0.5.0", + "light-poseidon 0.2.0", + "light-poseidon 0.4.0", + "solana-define-syscall 4.0.1", + "thiserror 2.0.18", +] + [[package]] name = "solana-precompile-error" version = "3.0.0" @@ -7607,14 +8029,14 @@ dependencies = [ "solana-account-info", "solana-define-syscall 4.0.1", "solana-program-error", - "solana-pubkey 4.0.0", + "solana-pubkey 4.1.0", ] [[package]] name = "solana-program-error" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1af32c995a7b692a915bb7414d5f8e838450cf7c70414e763d8abcae7b51f28" +checksum = "4f04fa578707b3612b095f0c8e19b66a1233f7c42ca8082fcb3b745afcc0add6" dependencies = [ "borsh", "serde", @@ -7632,24 +8054,24 @@ dependencies = [ [[package]] name = "solana-program-option" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e7b4ddb464f274deb4a497712664c3b612e3f5f82471d4e47710fc4ab1c3095" +checksum = "7a88006a9b8594088cec9027ab77caaaa258a2aaa2083d3f086c44b42e50aeab" [[package]] name = "solana-program-pack" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c169359de21f6034a63ebf96d6b380980307df17a8d371344ff04a883ec4e9d0" +checksum = "3d7701cb15b90667ae1c89ef4ac35a59c61e66ce58ddee13d729472af7f41d59" dependencies = [ "solana-program-error", ] [[package]] name = "solana-program-runtime" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a03ee54e20e5562f347121517c1692489a6a8e04f86aefd5740af5097c33820" +checksum = "35a7bb03e9d73082c4eced105102b88f1d6ec6856b020f17ec89043bcf9dbdda" dependencies = [ "base64 0.22.1", "bincode", @@ -7671,22 +8093,68 @@ dependencies = [ "solana-program-entrypoint", "solana-pubkey 3.0.0", "solana-rent 3.1.0", - "solana-sbpf", + "solana-sbpf 0.13.1", "solana-sdk-ids", "solana-slot-hashes", "solana-stable-layout", "solana-stake-interface", - "solana-svm-callback", - "solana-svm-feature-set", - "solana-svm-log-collector", - "solana-svm-measure", - "solana-svm-timings", - "solana-svm-transaction", - "solana-svm-type-overrides", + "solana-svm-callback 3.1.12", + "solana-svm-feature-set 3.1.12", + "solana-svm-log-collector 3.1.12", + "solana-svm-measure 3.1.12", + "solana-svm-timings 3.1.12", + "solana-svm-transaction 3.1.12", + "solana-svm-type-overrides 3.1.12", "solana-system-interface 2.0.0", "solana-sysvar", "solana-sysvar-id", - "solana-transaction-context", + "solana-transaction-context 3.1.12", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-program-runtime" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6c7f89c89d5ff25f64a41c8cb00478b1d62f941f14a7dd8537c9e50bb2acc92" +dependencies = [ + "base64 0.22.1", + "bincode", + "cfg-if", + "itertools 0.14.0", + "log", + "percentage", + "rand 0.9.4", + "serde", + "solana-account", + "solana-account-info", + "solana-clock", + "solana-epoch-rewards", + "solana-epoch-schedule", + "solana-fee-structure", + "solana-hash 4.2.0", + "solana-instruction", + "solana-last-restart-slot", + "solana-loader-v3-interface", + "solana-program-entrypoint", + "solana-pubkey 4.1.0", + "solana-rent 3.1.0", + "solana-sbpf 0.14.4", + "solana-sdk-ids", + "solana-slot-hashes", + "solana-stable-layout", + "solana-stake-interface", + "solana-svm-callback 4.0.0", + "solana-svm-feature-set 4.0.0", + "solana-svm-log-collector 4.0.0", + "solana-svm-measure 4.0.0", + "solana-svm-timings 4.0.0", + "solana-svm-transaction 4.0.0", + "solana-svm-type-overrides 4.0.0", + "solana-system-interface 3.1.0", + "solana-sysvar", + "solana-sysvar-id", + "solana-transaction-context 4.0.0", "thiserror 2.0.18", ] @@ -7702,18 +8170,18 @@ dependencies = [ [[package]] name = "solana-pubkey" -version = "4.0.0" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f7104d456b58e1418c21a8581e89810278d1190f70f27ece7fc0b2c9282a57" +checksum = "1b06bd918d60111ee1f97de817113e2040ca0cedb740099ee8d646233f6b906c" dependencies = [ - "solana-address 2.1.0", + "solana-address 2.6.1", ] [[package]] name = "solana-pubsub-client" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1305d955b8da57ec22b0410630df61770fbae6b8e93aa91bb0fb76f01bc56f8a" +checksum = "1109810209e87785ec378ed7ce50c83bb4a7dc26fa6a992299534c11e0c21b9c" dependencies = [ "crossbeam-channel", "futures-util", @@ -7737,17 +8205,17 @@ dependencies = [ [[package]] name = "solana-quic-client" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e472c50da5a07aec4253857a507edfcdd3c0a03d0012f213776ccf18de0aafe0" +checksum = "d54d9e4da86cd29814dd0c6f79e071d4539030af48ff71af49db9f6f1862b9c3" dependencies = [ + "anza-quinn", + "anza-quinn-proto", "async-lock", "async-trait", "futures", "itertools 0.12.1", "log", - "quinn", - "quinn-proto", "rustls", "solana-connection-cache", "solana-keypair", @@ -7776,9 +8244,9 @@ dependencies = [ [[package]] name = "solana-rayon-threadlimit" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbd391cd6ef3d8a3da4a6981a38050ac1449d8472bcbd394fbe1e35fc039424c" +checksum = "16d9e58c21eb661d6683510afcd60cc280b92bfc107944f69912aab51ace47a4" dependencies = [ "log", "num_cpus", @@ -7799,9 +8267,9 @@ dependencies = [ [[package]] name = "solana-rent" -version = "4.0.0" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763fe5c88a76ce18235db595b21d38b7aebf6db56b324cdf9fc96059f4410823" +checksum = "39f0d780bf8e8a1fe8b5b5fce1acad6b209485b86dec246e7523d5e4a8b7c7fc" dependencies = [ "solana-sdk-macro", ] @@ -7818,9 +8286,9 @@ dependencies = [ [[package]] name = "solana-rpc-client" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7476104ef972be862a9c4989bb0d5798971a022a23becbd874f854733500d0a6" +checksum = "e4cc64e78e25d5545c8c275c697168b331cf074983a9eff2ec67de2d8c584854" dependencies = [ "async-trait", "base64 0.22.1", @@ -7852,15 +8320,15 @@ dependencies = [ "solana-transaction-error", "solana-transaction-status-client-types", "solana-version", - "solana-vote-interface", + "solana-vote-interface 4.0.4", "tokio", ] [[package]] name = "solana-rpc-client-api" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139578f2184aa2299d36dc2da71233d695fbbe7925fed5649bf614f96783383" +checksum = "657c1d331d8b0611327bbf3bf342d3b98ad7743a74cc59ddfcce2925a0d818fc" dependencies = [ "anyhow", "jsonrpc-core", @@ -7879,9 +8347,9 @@ dependencies = [ [[package]] name = "solana-rpc-client-nonce-utils" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28bd892ee8c80db85bf78594dde4dd2537ba11d419bf30676e7ab948da290675" +checksum = "b1fc7c6ad3eb7df35a1f97c820003439334ee0d5944b155e9cb78bf9e0ecce36" dependencies = [ "solana-account", "solana-commitment-config", @@ -7896,9 +8364,9 @@ dependencies = [ [[package]] name = "solana-rpc-client-types" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "104f40726fc48ad80b6b52ba7f3300a6ea2a87307cd5560afe943027d95e2b56" +checksum = "727f7cc8e29432b7efad120558a883629332a72c9853f0506b0453a9f11be51a" dependencies = [ "base64 0.22.1", "bs58", @@ -7923,16 +8391,16 @@ dependencies = [ [[package]] name = "solana-runtime" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791c6e7aac45a5307e4cda9cb3b1cbec6805ec1dcf1b5673181ff73fa5fbad88" +checksum = "0eb482d0c82ee826621758e7fdba331cd78857a94107247a2674216abb4ff808" dependencies = [ - "agave-feature-set", + "agave-feature-set 3.1.12", "agave-fs", - "agave-precompiles", + "agave-precompiles 3.1.12", "agave-reserved-account-keys", "agave-snapshots", - "agave-syscalls", + "agave-syscalls 3.1.12", "agave-votor-messages", "ahash 0.8.12", "aquamarine", @@ -7973,16 +8441,16 @@ dependencies = [ "solana-account-info", "solana-accounts-db", "solana-address-lookup-table-interface", - "solana-bls-signatures", - "solana-bpf-loader-program", + "solana-bls-signatures 1.0.0", + "solana-bpf-loader-program 3.1.12", "solana-bucket-map", - "solana-builtins", + "solana-builtins 3.1.12", "solana-client-traits", "solana-clock", "solana-cluster-type", "solana-commitment-config", - "solana-compute-budget", - "solana-compute-budget-instruction", + "solana-compute-budget 3.1.12", + "solana-compute-budget-instruction 3.1.12", "solana-compute-budget-interface", "solana-config-interface", "solana-cost-model", @@ -8011,11 +8479,11 @@ dependencies = [ "solana-nohash-hasher", "solana-nonce", "solana-nonce-account", - "solana-packet", + "solana-packet 3.0.0", "solana-perf", "solana-poh-config", "solana-precompile-error", - "solana-program-runtime", + "solana-program-runtime 3.1.12", "solana-pubkey 3.0.0", "solana-rayon-threadlimit", "solana-rent 3.1.0", @@ -8031,24 +8499,24 @@ dependencies = [ "solana-slot-hashes", "solana-slot-history", "solana-stake-interface", - "solana-svm", - "solana-svm-callback", - "solana-svm-timings", - "solana-svm-transaction", + "solana-svm 3.1.12", + "solana-svm-callback 3.1.12", + "solana-svm-timings 3.1.12", + "solana-svm-transaction 3.1.12", "solana-system-interface 2.0.0", "solana-system-transaction", "solana-sysvar", "solana-sysvar-id", "solana-time-utils", "solana-transaction", - "solana-transaction-context", + "solana-transaction-context 3.1.12", "solana-transaction-error", "solana-transaction-status-client-types", "solana-unified-scheduler-logic", "solana-version", "solana-vote", - "solana-vote-interface", - "solana-vote-program", + "solana-vote-interface 4.0.4", + "solana-vote-program 3.1.12", "spl-generic-token", "static_assertions", "strum 0.24.1", @@ -8060,22 +8528,22 @@ dependencies = [ [[package]] name = "solana-runtime-transaction" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c95ca737e7c2a3edfdb25c24a1576b5799a51c5f71bd737af6c6f2ab13172cc5" +checksum = "ab8a63214f8959f5eb0b2d2a53497688be7cf7db23faae319864af5ffaa8b99e" dependencies = [ "agave-transaction-view", "log", - "solana-compute-budget", - "solana-compute-budget-instruction", + "solana-compute-budget 3.1.12", + "solana-compute-budget-instruction 3.1.12", "solana-hash 3.1.0", "solana-message", "solana-pubkey 3.0.0", "solana-sdk-ids", "solana-signature", - "solana-svm-transaction", + "solana-svm-transaction 3.1.12", "solana-transaction", - "solana-transaction-context", + "solana-transaction-context 3.1.12", "solana-transaction-error", "thiserror 2.0.18", ] @@ -8103,32 +8571,49 @@ dependencies = [ "winapi", ] +[[package]] +name = "solana-sbpf" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "733b3657a0fab205102b799dbe17f85d3972cf984232c1b0b108fa6ba438e382" +dependencies = [ + "byteorder", + "combine 3.8.1", + "hash32", + "libc", + "log", + "rand 0.8.5", + "rustc-demangle", + "thiserror 2.0.18", + "winapi", +] + [[package]] name = "solana-sdk-ids" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "def234c1956ff616d46c9dd953f251fa7096ddbaa6d52b165218de97882b7280" dependencies = [ - "solana-address 2.1.0", + "solana-address 2.6.1", ] [[package]] name = "solana-sdk-macro" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6430000e97083460b71d9fbadc52a2ab2f88f53b3a4c5e58c5ae3640a0e8c00" +checksum = "8765316242300c48242d84a41614cb3388229ec353ba464f6fe62a733e41806f" dependencies = [ "bs58", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] name = "solana-secp256k1-program" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8efa767b0188f577edae7080e8bf080e5db9458e2b6ee5beaa73e2e6bb54e99d" +checksum = "ad4cf8232f7aef9ff2dd95d701f63e3c11909dec2400def5c361be29d24291e7" dependencies = [ "digest 0.10.7", "k256", @@ -8140,12 +8625,12 @@ dependencies = [ [[package]] name = "solana-secp256k1-recover" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de18cfdab99eeb940fbedd8c981fa130c0d76252da75d05446f22fae8b51932" +checksum = "e3a1ad3ed7846631c88c71c5d2f21a2ecb6b61da333d9be173b6b061b35609ae" dependencies = [ "k256", - "solana-define-syscall 4.0.1", + "solana-define-syscall 5.1.0", "thiserror 2.0.18", ] @@ -8163,12 +8648,9 @@ dependencies = [ [[package]] name = "solana-security-txt" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "156bb61a96c605fa124e052d630dba2f6fb57e08c7d15b757e1e958b3ed7b3fe" -dependencies = [ - "hashbrown 0.15.2", -] +checksum = "c94a02d486b28f219a4f8f5d7dd93cbfbb93c9f466cb7871c22e50cd5ae9a7a2" [[package]] name = "solana-seed-derivable" @@ -8201,21 +8683,21 @@ dependencies = [ [[package]] name = "solana-serde-varint" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5174c57d5ff3c1995f274d17156964664566e2cde18a07bba1586d35a70d3b" +checksum = "950e5b83e839dc0f92c66afc124bb8f40e89bc90f0579e8ec5499296d27f54e3" dependencies = [ "serde", ] [[package]] name = "solana-serialize-utils" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e41dd8feea239516c623a02f0a81c2367f4b604d7965237fed0751aeec33ed" +checksum = "5d7cc401931d178472358e6b78dc72d031dc08f752d7410f0e8bd259dd6f02fa" dependencies = [ "solana-instruction-error", - "solana-pubkey 3.0.0", + "solana-pubkey 4.1.0", "solana-sanitize", ] @@ -8227,14 +8709,14 @@ checksum = "db7dc3011ea4c0334aaaa7e7128cb390ecf546b28d412e9bf2064680f57f588f" dependencies = [ "sha2 0.10.9", "solana-define-syscall 4.0.1", - "solana-hash 4.1.0", + "solana-hash 4.2.0", ] [[package]] name = "solana-short-vec" -version = "3.2.0" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3bd991c2cc415291c86bb0b6b4d53e93d13bb40344e4c5a2884e0e4f5fa93f" +checksum = "7d8250a4495aad49ad20556a607da53bdcb20de78da10b65afbf918b7f1de647" dependencies = [ "serde_core", ] @@ -8252,9 +8734,9 @@ dependencies = [ [[package]] name = "solana-signature" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4028aeedd443d80f1dccbf64872593a80e6a9676ee9007f6eccb63b65983ebd" +checksum = "132a93134f1262aa832f1849b83bec6c9945669b866da18661a427943b9e801e" dependencies = [ "ed25519-dalek 2.2.0", "five8", @@ -8262,7 +8744,7 @@ dependencies = [ "serde-big-array", "serde_derive", "solana-sanitize", - "wincode", + "wincode 0.4.9", ] [[package]] @@ -8278,26 +8760,27 @@ dependencies = [ [[package]] name = "solana-slot-hashes" -version = "3.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80a293f952293281443c04f4d96afd9d547721923d596e92b4377ed2360f1746" +checksum = "2585f70191623887329dfb5078da3a00e15e3980ea67f42c2e10b07028419f43" dependencies = [ "serde", "serde_derive", - "solana-hash 3.1.0", + "solana-hash 4.2.0", "solana-sdk-ids", "solana-sysvar-id", ] [[package]] name = "solana-slot-history" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f914f6b108f5bba14a280b458d023e3621c9973f27f015a4d755b50e88d89e97" +checksum = "40427c04d3e808493cb5e3d1a97cef84d7c15cb6f89b15c5684d0d4027105600" dependencies = [ "bv", "serde", "serde_derive", + "solana-get-sysvar", "solana-sdk-ids", "solana-sysvar-id", ] @@ -8333,10 +8816,12 @@ dependencies = [ [[package]] name = "solana-streamer" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71881ba229a4dbdc1790ad6d367f40dd42b4eea0e8ef1076d867c27424d5d4c" +checksum = "03cc4445ae5c1299b80cf54ef89c39fa7dbd3bcc2d388e842c8c2ab54c6e3f30" dependencies = [ + "anza-quinn", + "anza-quinn-proto", "arc-swap", "bytes", "crossbeam-channel", @@ -8353,8 +8838,6 @@ dependencies = [ "num_cpus", "pem", "percentage", - "quinn", - "quinn-proto", "rand 0.8.5", "rustls", "smallvec", @@ -8363,7 +8846,7 @@ dependencies = [ "solana-measure", "solana-metrics", "solana-net-utils", - "solana-packet", + "solana-packet 3.0.0", "solana-perf", "solana-pubkey 3.0.0", "solana-quic-definitions", @@ -8381,9 +8864,9 @@ dependencies = [ [[package]] name = "solana-svm" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5b12459299179112ec6ff3e1a975006b8ac9aa1bb8a66377ba6128d05f099a2" +checksum = "810c10b868df289fec4463840642c0263d59c75cec097d88e11a27c5ac91a61c" dependencies = [ "ahash 0.8.12", "log", @@ -8397,26 +8880,69 @@ dependencies = [ "solana-instructions-sysvar", "solana-loader-v3-interface", "solana-loader-v4-interface", - "solana-loader-v4-program", + "solana-loader-v4-program 3.1.12", "solana-message", "solana-nonce", "solana-nonce-account", "solana-program-entrypoint", "solana-program-pack", - "solana-program-runtime", + "solana-program-runtime 3.1.12", "solana-pubkey 3.0.0", "solana-rent 3.1.0", "solana-sdk-ids", - "solana-svm-callback", - "solana-svm-feature-set", - "solana-svm-log-collector", - "solana-svm-measure", - "solana-svm-timings", - "solana-svm-transaction", - "solana-svm-type-overrides", + "solana-svm-callback 3.1.12", + "solana-svm-feature-set 3.1.12", + "solana-svm-log-collector 3.1.12", + "solana-svm-measure 3.1.12", + "solana-svm-timings 3.1.12", + "solana-svm-transaction 3.1.12", + "solana-svm-type-overrides 3.1.12", "solana-system-interface 2.0.0", "solana-sysvar-id", - "solana-transaction-context", + "solana-transaction-context 3.1.12", + "solana-transaction-error", + "spl-generic-token", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-svm" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd6f8535966cc5235c8c3b455809304ebcf65e217d3f88beb3e3a404bc6dd4b" +dependencies = [ + "ahash 0.8.12", + "log", + "percentage", + "serde", + "solana-account", + "solana-clock", + "solana-fee-structure", + "solana-hash 4.2.0", + "solana-instruction", + "solana-instructions-sysvar", + "solana-loader-v3-interface", + "solana-loader-v4-interface", + "solana-loader-v4-program 4.0.0", + "solana-message", + "solana-nonce", + "solana-nonce-account", + "solana-program-entrypoint", + "solana-program-pack", + "solana-program-runtime 4.0.0", + "solana-pubkey 4.1.0", + "solana-rent 3.1.0", + "solana-sdk-ids", + "solana-svm-callback 4.0.0", + "solana-svm-feature-set 4.0.0", + "solana-svm-log-collector 4.0.0", + "solana-svm-measure 4.0.0", + "solana-svm-timings 4.0.0", + "solana-svm-transaction 4.0.0", + "solana-svm-type-overrides 4.0.0", + "solana-system-interface 3.1.0", + "solana-sysvar-id", + "solana-transaction-context 4.0.0", "solana-transaction-error", "spl-generic-token", "thiserror 2.0.18", @@ -8424,9 +8950,9 @@ dependencies = [ [[package]] name = "solana-svm-callback" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c216afeef20cf86fd3d2ae812bebcdc23ee0e3d45fb4b3b28ad168cb56778ed" +checksum = "cb521c7f62db21661267a933f0d311a76b2b744a766b46f5a9a9395ce70f687c" dependencies = [ "solana-account", "solana-clock", @@ -8434,43 +8960,87 @@ dependencies = [ "solana-pubkey 3.0.0", ] +[[package]] +name = "solana-svm-callback" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4006b0da7e50cba514ced6b47bcf8f9591552458200e361fd4bdef4068cb2fed" +dependencies = [ + "solana-account", + "solana-clock", + "solana-precompile-error", + "solana-pubkey 4.1.0", +] + [[package]] name = "solana-svm-feature-set" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "641cddc667abba4cf3474d850a073c0a2b439ff0014c445cd09eaf5d79d70bab" +checksum = "08924d3b4918008d75a5807e73af8eb9f1c409067c772de518d1dd67dd4c03de" + +[[package]] +name = "solana-svm-feature-set" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ea15c0d91403375e3d017cc09780cf138b629abba4ccaaa7cf66b1afea1059" + +[[package]] +name = "solana-svm-log-collector" +version = "3.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c071b3e4e9b2f19f15659abc74bd7fcc40cec6d60c1f6024384ff78cb49d40" +dependencies = [ + "log", +] [[package]] name = "solana-svm-log-collector" -version = "3.1.8" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe6ce42b1620fd713e12cd52d62a7d4d370414d67ed9bfc5faa444fa54bb6f2" +checksum = "efb7d3ccd3a51b85807ff16b2f513069e8b55e220b280774a3e9b899bcb81987" dependencies = [ "log", ] [[package]] name = "solana-svm-measure" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea1d8035045fe47df97ee2a4695b09236161f82f1b4b6c2a49a5cb6a7c94fed6" +checksum = "c5e4f9972c93d50eaa299fadf1bee9de2055d47eef26af589dcefb487f22e71d" + +[[package]] +name = "solana-svm-measure" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d70c9972c1f03cb2bbc64d23dc2079419a66d89b49d6b44f79206530551ddc8c" [[package]] name = "solana-svm-timings" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9b6407ecacc9b1ca88bdb34f6afb10ab0e4c65f3f1b82bce637c3056deb456d" +checksum = "7c65d4887002f8105f8e49253d0956292c55d66a1a3ee3594e7f90e682ed9521" dependencies = [ "eager", - "enum-iterator", + "enum-iterator 1.5.0", "solana-pubkey 3.0.0", ] +[[package]] +name = "solana-svm-timings" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f3d66aa88c9001a076362108f7967d6a00d121ba38428e56928935566ed5bd" +dependencies = [ + "eager", + "enum-iterator 2.3.0", + "solana-pubkey 4.1.0", +] + [[package]] name = "solana-svm-transaction" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9ca13fa9a99ad8474c3867d56d81effcf5582bb6356ab0a9ed2fc373a3e4af7" +checksum = "3ef6ff55ce4c24e26ad8b0e67bc604cbd54eabfc94540c4c2c93e51fa087ead5" dependencies = [ "solana-hash 3.1.0", "solana-message", @@ -8480,15 +9050,38 @@ dependencies = [ "solana-transaction", ] +[[package]] +name = "solana-svm-transaction" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "067861db805d135a6fbe489bf2b74d701f270df8d03afd3257f7d51a2ff3467e" +dependencies = [ + "solana-hash 4.2.0", + "solana-message", + "solana-pubkey 4.1.0", + "solana-sdk-ids", + "solana-signature", + "solana-transaction", +] + [[package]] name = "solana-svm-type-overrides" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe572aba18afc347a699927720ddc8671da94663a6453e30e872f3ac3788da22" +checksum = "fa888be46794b88f130508f694e989fb8802c823b9048acd4d0240e9818502fe" dependencies = [ "rand 0.8.5", ] +[[package]] +name = "solana-svm-type-overrides" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e41661ebf0edcc296b15251c08fee0ad2da3257e6ab86cea2a0a8f6fba642c6" +dependencies = [ + "rand 0.9.4", +] + [[package]] name = "solana-system-interface" version = "2.0.0" @@ -8506,14 +9099,14 @@ dependencies = [ [[package]] name = "solana-system-interface" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14591d6508042ebefb110305d3ba761615927146a26917ade45dc332d8e1ecde" +checksum = "a95a6f2e23ed861d6444ad4a6d6896c418d7d101b960787e65a8e33157cee81b" dependencies = [ "num-traits", "serde", "serde_derive", - "solana-address 2.1.0", + "solana-address 2.6.1", "solana-instruction", "solana-msg", "solana-program-error", @@ -8521,9 +9114,9 @@ dependencies = [ [[package]] name = "solana-system-program" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c1c3a09bccfee48f072cbbeab3151578ac246f8af91309a9521ecd6129d4b92" +checksum = "578e44e2abb14c34efbc0074f9009132f12ff66cc3ab1a7715d24b6801bb7f32" dependencies = [ "bincode", "log", @@ -8534,15 +9127,41 @@ dependencies = [ "solana-instruction", "solana-nonce", "solana-nonce-account", - "solana-packet", - "solana-program-runtime", + "solana-packet 3.0.0", + "solana-program-runtime 3.1.12", "solana-pubkey 3.0.0", "solana-sdk-ids", - "solana-svm-log-collector", - "solana-svm-type-overrides", + "solana-svm-log-collector 3.1.12", + "solana-svm-type-overrides 3.1.12", "solana-system-interface 2.0.0", "solana-sysvar", - "solana-transaction-context", + "solana-transaction-context 3.1.12", +] + +[[package]] +name = "solana-system-program" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "450479004fee3396c88cc4aa2f9b2b8db9c77be42ee7c1c53e6fac9eaec5fd51" +dependencies = [ + "bincode", + "log", + "serde", + "solana-account", + "solana-bincode", + "solana-fee-calculator", + "solana-instruction", + "solana-nonce", + "solana-nonce-account", + "solana-packet 4.1.0", + "solana-program-runtime 4.0.0", + "solana-pubkey 4.1.0", + "solana-sdk-ids", + "solana-svm-log-collector 4.0.0", + "solana-svm-type-overrides 4.0.0", + "solana-system-interface 3.1.0", + "solana-sysvar", + "solana-transaction-context 4.0.0", ] [[package]] @@ -8579,13 +9198,13 @@ dependencies = [ "solana-epoch-rewards", "solana-epoch-schedule", "solana-fee-calculator", - "solana-hash 4.1.0", + "solana-hash 4.2.0", "solana-instruction", "solana-last-restart-slot", "solana-program-entrypoint", "solana-program-error", "solana-program-memory", - "solana-pubkey 4.0.0", + "solana-pubkey 4.1.0", "solana-rent 3.1.0", "solana-sdk-ids", "solana-sdk-macro", @@ -8600,7 +9219,7 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17358d1e9a13e5b9c2264d301102126cf11a47fd394cdf3dec174fe7bc96e1de" dependencies = [ - "solana-address 2.1.0", + "solana-address 2.6.1", "solana-sdk-ids", ] @@ -8612,9 +9231,9 @@ checksum = "0ced92c60aa76ec4780a9d93f3bd64dfa916e1b998eacc6f1c110f3f444f02c9" [[package]] name = "solana-tls-utils" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3460fa9475f912185e11e89d496ef08aae9db26d0c95a622d71b59e17dd0af8f" +checksum = "57a74352404ca3378d3bc6586a9a1e0d7362b687ce2218a0b646dccb767c7ba2" dependencies = [ "rustls", "solana-keypair", @@ -8625,9 +9244,9 @@ dependencies = [ [[package]] name = "solana-tpu-client" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b82d41c68b8ff70ef4a952aa40cbe1a4ffe41a4ff7d329f925568f096e6b3f8b" +checksum = "4fb908227ef3da285e4f12953075a83d06aabe53eab42364ea19c535bfe7c5aa" dependencies = [ "async-trait", "bincode", @@ -8666,8 +9285,8 @@ dependencies = [ "bincode", "serde", "serde_derive", - "solana-address 2.1.0", - "solana-hash 4.1.0", + "solana-address 2.6.1", + "solana-hash 4.2.0", "solana-instruction", "solana-instruction-error", "solana-message", @@ -8681,9 +9300,9 @@ dependencies = [ [[package]] name = "solana-transaction-context" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55a9c2e2af954fae402f08e210c7f01d6a8517ad358f8f0db11ed7de89b02d4" +checksum = "077ee5d6af12af9747cb14255a92ab79e830c5dabf9baaa8f4196299f476dfa0" dependencies = [ "bincode", "serde", @@ -8692,15 +9311,32 @@ dependencies = [ "solana-instructions-sysvar", "solana-pubkey 3.0.0", "solana-rent 3.1.0", - "solana-sbpf", + "solana-sbpf 0.13.1", + "solana-sdk-ids", +] + +[[package]] +name = "solana-transaction-context" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecefe8b30e334e2891ca82da35becd9a3f4c16021d9ca782e2a82adf31084fa3" +dependencies = [ + "bincode", + "serde", + "solana-account", + "solana-instruction", + "solana-instructions-sysvar", + "solana-pubkey 4.1.0", + "solana-rent 3.1.0", + "solana-sbpf 0.14.4", "solana-sdk-ids", ] [[package]] name = "solana-transaction-error" -version = "3.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4222065402340d7e6aec9dc3e54d22992ddcf923d91edcd815443c2bfca3144a" +checksum = "8396904805b0b385b9de115a652fe80fd01e5b98ce0513f4fcd8184ada9bb792" dependencies = [ "serde", "serde_derive", @@ -8710,15 +9346,15 @@ dependencies = [ [[package]] name = "solana-transaction-metrics-tracker" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0c780ebbf9ab558a9b9eff409d166c50c81dcbe012b8a488f3f12c042c39c4" +checksum = "17327deb9accf25ebb25a0422ab228de5d92acecf2ec178fdf926ae5ccd3ace1" dependencies = [ "base64 0.22.1", "bincode", "log", "rand 0.8.5", - "solana-packet", + "solana-packet 3.0.0", "solana-perf", "solana-short-vec", "solana-signature", @@ -8726,9 +9362,9 @@ dependencies = [ [[package]] name = "solana-transaction-status-client-types" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1341840c0ba1028b918b03c9ba9900019f739ee23946baf76574ec0a5dab8231" +checksum = "cf6050ff0021c138fd522283a743b8a62e39e9710590c17873ec232054dbc03a" dependencies = [ "base64 0.22.1", "bincode", @@ -8743,16 +9379,16 @@ dependencies = [ "solana-reward-info", "solana-signature", "solana-transaction", - "solana-transaction-context", + "solana-transaction-context 3.1.12", "solana-transaction-error", "thiserror 2.0.18", ] [[package]] name = "solana-udp-client" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02efe0168dc03038aadcf3915defa3e8440e705898d9c1cdac99cb70ef20c275" +checksum = "b32651092f28c7fa9fb622a055f21fcfb1a109e6851d964ce043336a0035a629" dependencies = [ "async-trait", "solana-connection-cache", @@ -8766,9 +9402,9 @@ dependencies = [ [[package]] name = "solana-unified-scheduler-logic" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "762ee8857761da481355ffa201131850426b46a7b1e7dfe1052d86f41a37a94e" +checksum = "d6f50281f494e916386e99175eb4806fb9554db9e790d3d45fd4d39a42d6d010" dependencies = [ "assert_matches", "solana-pubkey 3.0.0", @@ -8780,11 +9416,11 @@ dependencies = [ [[package]] name = "solana-version" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2642d930b9ef476bfb5d64bac62d35b37dfb415cdf7b0a642c3c0ca537f1a7b" +checksum = "f697aacc5aa4ac5534abdde8a91afdcf18c24d28bd52768e8001445dbda078db" dependencies = [ - "agave-feature-set", + "agave-feature-set 3.1.12", "rand 0.8.5", "semver", "serde", @@ -8794,9 +9430,9 @@ dependencies = [ [[package]] name = "solana-vote" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be38127756ac2aadf00dbff18a9dbd5b9f2044472fc9d395f9b4db85050814e3" +checksum = "bb12447f1d5a08ce4d6d6b8e50d146393ab24dfcde6d64541db3bd7a3ec1af58" dependencies = [ "itertools 0.12.1", "log", @@ -8807,15 +9443,15 @@ dependencies = [ "solana-hash 3.1.0", "solana-instruction", "solana-keypair", - "solana-packet", + "solana-packet 3.0.0", "solana-pubkey 3.0.0", "solana-sdk-ids", "solana-serialize-utils", "solana-signature", "solana-signer", - "solana-svm-transaction", + "solana-svm-transaction 3.1.12", "solana-transaction", - "solana-vote-interface", + "solana-vote-interface 4.0.4", "thiserror 2.0.18", ] @@ -8845,13 +9481,39 @@ dependencies = [ "solana-system-interface 2.0.0", ] +[[package]] +name = "solana-vote-interface" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d444ce30b6b4f9c281ba06061ea96638d063b53c2171b1e41ba02ebff79ed28f" +dependencies = [ + "bincode", + "cfg_eval", + "num-derive", + "num-traits", + "serde", + "serde_derive", + "serde_with", + "solana-clock", + "solana-hash 4.2.0", + "solana-instruction", + "solana-instruction-error", + "solana-pubkey 4.1.0", + "solana-rent 4.3.0", + "solana-sdk-ids", + "solana-serde-varint", + "solana-serialize-utils", + "solana-short-vec", + "solana-system-interface 3.1.0", +] + [[package]] name = "solana-vote-program" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de77cc3a9dc9c1247d779db24a5c3bb5cf533855ccfa0ddb12c0c773c26acf4e" +checksum = "ac7f37540da27c0ec132ee1b5380ad32d98663afba7ade3581f2d963fd2f63c2" dependencies = [ - "agave-feature-set", + "agave-feature-set 3.1.12", "bincode", "log", "num-derive", @@ -8864,34 +9526,95 @@ dependencies = [ "solana-hash 3.1.0", "solana-instruction", "solana-keypair", - "solana-packet", - "solana-program-runtime", + "solana-packet 3.0.0", + "solana-program-runtime 3.1.12", "solana-pubkey 3.0.0", "solana-rent 3.1.0", "solana-sdk-ids", "solana-signer", "solana-slot-hashes", "solana-transaction", - "solana-transaction-context", - "solana-vote-interface", + "solana-transaction-context 3.1.12", + "solana-vote-interface 4.0.4", "thiserror 2.0.18", ] +[[package]] +name = "solana-vote-program" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4537fd6efe65f53ccd28d54d2ad43275b024834a4a8ca4dfa4babfa01e6d11ab" +dependencies = [ + "agave-feature-set 4.0.0", + "bincode", + "log", + "num-derive", + "num-traits", + "serde", + "solana-account", + "solana-bincode", + "solana-bls-signatures 3.2.0", + "solana-clock", + "solana-epoch-schedule", + "solana-hash 4.2.0", + "solana-instruction", + "solana-keypair", + "solana-packet 4.1.0", + "solana-program-runtime 4.0.0", + "solana-pubkey 4.1.0", + "solana-rent 3.1.0", + "solana-sdk-ids", + "solana-signer", + "solana-slot-hashes", + "solana-system-interface 3.1.0", + "solana-transaction", + "solana-transaction-context 4.0.0", + "solana-vote-interface 5.1.1", + "thiserror 2.0.18", +] + +[[package]] +name = "solana-zero-copy" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea15126ebdc7e270c50d43884369af9f51d2308156d46a18e351522a164844d" +dependencies = [ + "bytemuck", + "bytemuck_derive", +] + +[[package]] +name = "solana-zk-elgamal-proof-program" +version = "3.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee65de587e6fe912668903e62f3f40c02a834f21967a18cc6c71f550c51a639" +dependencies = [ + "agave-feature-set 3.1.12", + "bytemuck", + "num-derive", + "num-traits", + "solana-instruction", + "solana-program-runtime 3.1.12", + "solana-sdk-ids", + "solana-svm-log-collector 3.1.12", + "solana-zk-sdk 4.0.0", +] + [[package]] name = "solana-zk-elgamal-proof-program" -version = "3.1.8" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ee82208a466bd448bcd1387a7c2c4def0b3f938398e04e5364ee24b10ed04a" +checksum = "fdf97ec816e8c6d45b5f05e21381bcc4b856cb3c89b69e465ee20972368b4c31" dependencies = [ - "agave-feature-set", + "agave-feature-set 4.0.0", "bytemuck", "num-derive", "num-traits", "solana-instruction", - "solana-program-runtime", + "solana-program-runtime 4.0.0", "solana-sdk-ids", - "solana-svm-log-collector", - "solana-zk-sdk", + "solana-svm-log-collector 4.0.0", + "solana-zk-sdk 5.0.1", ] [[package]] @@ -8931,28 +9654,71 @@ dependencies = [ "zeroize", ] +[[package]] +name = "solana-zk-sdk" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09670ff59f60e6ddc2209c2e4353992a9b9f01d4e244f3e9d67bd5146e33d388" +dependencies = [ + "aes-gcm-siv", + "base64 0.22.1", + "bincode", + "bytemuck", + "bytemuck_derive", + "curve25519-dalek 4.1.3", + "itertools 0.14.0", + "merlin", + "num-derive", + "num-traits", + "rand 0.8.5", + "serde", + "serde_derive", + "serde_json", + "sha3", + "solana-address 2.6.1", + "solana-derivation-path", + "solana-instruction", + "solana-sdk-ids", + "solana-seed-derivable", + "solana-seed-phrase", + "solana-signature", + "solana-signer", + "subtle", + "thiserror 2.0.18", + "zeroize", +] + [[package]] name = "solana-zk-token-proof-program" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50aa6a85620f94356acf313c13ae4464bbb0b981b1e80f45daec456695b2839d" +checksum = "56ecb5f2989632b030709c8aebdbc586a8c0f867d0ec154d0cb7feafb86f72fb" dependencies = [ - "agave-feature-set", + "agave-feature-set 3.1.12", "bytemuck", "num-derive", "num-traits", "solana-instruction", - "solana-program-runtime", + "solana-program-runtime 3.1.12", "solana-sdk-ids", - "solana-svm-log-collector", + "solana-svm-log-collector 3.1.12", "solana-zk-token-sdk", ] +[[package]] +name = "solana-zk-token-proof-program" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f08a8be7008cec95d74c0ded5ae10b6869bd06bd9761c800e7e098bd45097e6" +dependencies = [ + "solana-program-runtime 4.0.0", +] + [[package]] name = "solana-zk-token-sdk" -version = "3.1.8" +version = "3.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9135e607f31cd86f73052cbb37264564166c31e400e1c49a9dfca93f7fb661d7" +checksum = "c0d27bcbe061cc3d4f25527ee4f28b04a9408294d46dd9b817b93cd3dd98d72d" dependencies = [ "aes-gcm-siv", "base64 0.22.1", @@ -8968,7 +9734,7 @@ dependencies = [ "serde", "serde_json", "sha3", - "solana-curve25519", + "solana-curve25519 3.1.12", "solana-derivation-path", "solana-instruction", "solana-pubkey 3.0.0", @@ -9012,9 +9778,9 @@ dependencies = [ [[package]] name = "spl-discriminator" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48cc11459e265d5b501534144266620289720b4c44522a47bc6b63cd295d2f3" +checksum = "e597c5ff9ed7c74a54dbc47bae2f06e4db8c98f4356ad280200dc11878266db1" dependencies = [ "bytemuck", "solana-program-error", @@ -9030,7 +9796,7 @@ checksum = "d9e8418ea6269dcfb01c712f0444d2c75542c04448b480e87de59d2865edc750" dependencies = [ "quote", "spl-discriminator-syn", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -9042,7 +9808,7 @@ dependencies = [ "proc-macro2", "quote", "sha2 0.10.9", - "syn 2.0.114", + "syn 2.0.118", "thiserror 1.0.69", ] @@ -9057,7 +9823,7 @@ dependencies = [ "solana-program-error", "solana-pubkey 3.0.0", "solana-sdk-ids", - "solana-zk-sdk", + "solana-zk-sdk 4.0.0", "spl-token-confidential-transfer-proof-extraction", ] @@ -9096,7 +9862,7 @@ dependencies = [ "solana-program-error", "solana-program-option", "solana-pubkey 3.0.0", - "solana-zk-sdk", + "solana-zk-sdk 4.0.0", "thiserror 2.0.18", ] @@ -9124,7 +9890,7 @@ dependencies = [ "proc-macro2", "quote", "sha2 0.10.9", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -9175,7 +9941,7 @@ dependencies = [ "solana-security-txt", "solana-system-interface 2.0.0", "solana-sysvar", - "solana-zk-sdk", + "solana-zk-sdk 4.0.0", "spl-elgamal-registry-interface", "spl-memo-interface", "spl-pod", @@ -9207,7 +9973,7 @@ dependencies = [ "solana-program-pack", "solana-pubkey 3.0.0", "solana-sdk-ids", - "solana-zk-sdk", + "solana-zk-sdk 4.0.0", "spl-pod", "spl-token-confidential-transfer-proof-extraction", "spl-token-confidential-transfer-proof-generation", @@ -9225,8 +9991,8 @@ checksum = "afbeb07f737d868f145512a4bcf9f59da275b7a3483df0add3f71eb812b689fb" dependencies = [ "base64 0.22.1", "bytemuck", - "solana-curve25519", - "solana-zk-sdk", + "solana-curve25519 3.1.12", + "solana-zk-sdk 4.0.0", ] [[package]] @@ -9237,14 +10003,14 @@ checksum = "879a9ebad0d77383d3ea71e7de50503554961ff0f4ef6cbca39ad126e6f6da3a" dependencies = [ "bytemuck", "solana-account-info", - "solana-curve25519", + "solana-curve25519 3.1.12", "solana-instruction", "solana-instructions-sysvar", "solana-msg", "solana-program-error", "solana-pubkey 3.0.0", "solana-sdk-ids", - "solana-zk-sdk", + "solana-zk-sdk 4.0.0", "spl-pod", "thiserror 2.0.18", ] @@ -9256,7 +10022,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0cd59fce3dc00f563c6fa364d67c3f200d278eae681f4dc250240afcfe044b1" dependencies = [ "curve25519-dalek 4.1.3", - "solana-zk-sdk", + "solana-zk-sdk 4.0.0", "thiserror 2.0.18", ] @@ -9345,19 +10111,18 @@ dependencies = [ [[package]] name = "spl-type-length-value" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca20a1a19f4507a98ca4b28ff5ed54cac9b9d34ed27863e2bde50a3238f9a6ac" +checksum = "2504631748c48d2a937414d64a12dcac4588d34bd07d355d648619c189d29435" dependencies = [ "bytemuck", "num-derive", "num-traits", "num_enum", "solana-account-info", - "solana-msg", "solana-program-error", + "solana-zero-copy", "spl-discriminator", - "spl-pod", "thiserror 2.0.18", ] @@ -9425,7 +10190,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -9448,7 +10213,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.114", + "syn 2.0.118", "tokio", "url", ] @@ -9652,9 +10417,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -9696,7 +10461,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -9749,9 +10514,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.44" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", @@ -9803,7 +10568,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -9814,7 +10579,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -9936,7 +10701,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -10115,7 +10880,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -10153,7 +10918,7 @@ dependencies = [ "prost-build", "prost-types", "quote", - "syn 2.0.114", + "syn 2.0.118", "tempfile", "tonic-build", ] @@ -10232,7 +10997,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -10307,7 +11072,7 @@ dependencies = [ "http 1.4.0", "httparse", "log", - "rand 0.9.2", + "rand 0.9.4", "rustls", "rustls-pki-types", "sha1", @@ -10598,7 +11363,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", "wasm-bindgen-shared", ] @@ -10757,10 +11522,24 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "wincode" -version = "0.2.5" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5cec722a3274e47d1524cbe2cea762f2c19d615bd9d73ada21db9066349d57e" +checksum = "657690780ce23e6f66576a782ffd88eb353512381817029cc1d7a99154bb6d1f" dependencies = [ + "pastey", + "proc-macro2", + "quote", + "thiserror 2.0.18", + "wincode-derive", +] + +[[package]] +name = "wincode" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d967db7705dc29120bb6e8ce5b5a2e27734ed5976d1c904e95bd238d1c3c5a" +dependencies = [ + "pastey", "proc-macro2", "quote", "thiserror 2.0.18", @@ -10769,14 +11548,14 @@ dependencies = [ [[package]] name = "wincode-derive" -version = "0.2.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8961eb04054a1b2e026b5628e24da7e001350249a787e1a85aa961f33dc5f286" +checksum = "15ab90b719560d0fda79c74550ad1c948d17b118765942838055ebaf34d67071" dependencies = [ - "darling 0.21.3", + "darling 0.23.0", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -10800,7 +11579,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -10811,7 +11590,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -11195,7 +11974,7 @@ dependencies = [ "heck 0.5.0", "indexmap", "prettyplease", - "syn 2.0.114", + "syn 2.0.118", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -11211,7 +11990,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -11351,7 +12130,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", "synstructure 0.13.2", ] @@ -11372,7 +12151,7 @@ checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -11392,7 +12171,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", "synstructure 0.13.2", ] @@ -11413,7 +12192,7 @@ checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] @@ -11446,7 +12225,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.118", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 10894da..a0087c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,30 @@ http-body-util = "0.1.3" chrono = "0.4.41" prometheus = { version = "0.14.0", features = ["push"] } solana-lattice-hash = "3.1.5" +solana-svm = { version = "4.0.0", features = ["agave-unstable-api"] } +solana-svm-callback = { version = "=4.0.0", features = ["agave-unstable-api"] } +agave-precompiles = { version = "4.0.0", features = ["agave-unstable-api"] } +solana-program-runtime = { version = "4.0.0", features = ["agave-unstable-api"] } +agave-syscalls = { version = "4.0.0", features = ["agave-unstable-api"] } +solana-builtins = { version = "4.0.0", features = ["agave-unstable-api"] } +solana-bpf-loader-program = "4.0.0" +agave-feature-set = { version = "=4.0.0", features = ["agave-unstable-api"] } +solana-compute-budget = { version = "4.0.0", features = ["agave-unstable-api"] } +solana-compute-budget-instruction = { version = "4.0.0", features = ["agave-unstable-api"] } +solana-svm-feature-set = "=4.0.0" +solana-svm-transaction = "=4.0.0" +solana-transaction = { version = "3.1", features = ["bincode", "blake3", "verify", "serde"] } +solana-message = "3.1" +solana-address-lookup-table-interface = { version = "3.0", features = ["bincode", "bytemuck"] } +solana-loader-v3-interface = { version = "6.1", features = ["serde"] } +solana-sdk-ids = "3.1" +solana-clock = "3.0" +solana-fee-structure = "3.0" +solana-rent = "3.0" +solana-transaction-error = "3.1" +solana-precompile-error = "3.0" +solana-transaction-status-client-types = "3.1.5" +bincode = "1.3" blake3 = "1" tracing-opentelemetry = "0.32.0" opentelemetry = { version = "0.31.0", features = ["trace"] } diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index 8e85d77..736c1ff 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -62,4 +62,32 @@ chrono = { workspace = true } base64 = "0.22" serde_yaml = "0.9" reqwest = { version = "0.12", features = ["json"] } -urlencoding = "2.1.3" \ No newline at end of file +urlencoding = "2.1.3" + +# simulateTransaction execution engine (direct SVM API, 4.0.0 train). +solana-svm = { workspace = true } +solana-svm-callback = { workspace = true } +agave-precompiles = { workspace = true } +solana-program-runtime = { workspace = true } +agave-syscalls = { workspace = true } +solana-builtins = { workspace = true } +solana-bpf-loader-program = { workspace = true } +agave-feature-set = { workspace = true } +solana-compute-budget = { workspace = true } +solana-compute-budget-instruction = { workspace = true } +solana-svm-feature-set = { workspace = true } +solana-svm-transaction = { workspace = true } +solana-transaction = { workspace = true } +solana-message = { workspace = true } +solana-address-lookup-table-interface = { workspace = true } +solana-loader-v3-interface = { workspace = true } +solana-sdk-ids = { workspace = true } +solana-clock = { workspace = true } +solana-fee-structure = { workspace = true } +solana-rent = { workspace = true } +solana-transaction-error = { workspace = true } +solana-precompile-error = { workspace = true } +solana-hash = { workspace = true } +solana-transaction-status-client-types = { workspace = true } +solana-nonce-account = "3.0" +bincode = { workspace = true } diff --git a/crates/api/src/http/mod.rs b/crates/api/src/http/mod.rs index c0ac04f..143985a 100644 --- a/crates/api/src/http/mod.rs +++ b/crates/api/src/http/mod.rs @@ -38,6 +38,7 @@ pub struct CloudbreakRpcState { pub vote_accounts_supported: bool, pub stakes_cache: SharedStakesSnapshot, pub max_multiple_accounts: usize, + pub simulation_supported: bool, } impl CloudbreakRpcState { @@ -57,6 +58,7 @@ impl CloudbreakRpcState { vote_accounts_supported: bool, stakes_cache: SharedStakesSnapshot, max_multiple_accounts: usize, + simulation_supported: bool, ) -> Self { Self { database, @@ -73,6 +75,7 @@ impl CloudbreakRpcState { vote_accounts_supported, stakes_cache, max_multiple_accounts, + simulation_supported, } } } diff --git a/crates/api/src/http/rpc.rs b/crates/api/src/http/rpc.rs index 55b2b9e..a9ac9b6 100644 --- a/crates/api/src/http/rpc.rs +++ b/crates/api/src/http/rpc.rs @@ -10,7 +10,9 @@ use hyper::{Request, StatusCode}; use serde::Serialize; use solana_commitment_config::CommitmentConfig; use cloudbreak_core::modules::rpc_filter_type::RpcProgramAccountsConfig; -use solana_rpc_client_api::config::{RpcAccountInfoConfig, RpcContextConfig}; +use solana_rpc_client_api::config::{ + RpcAccountInfoConfig, RpcContextConfig, RpcSimulateTransactionConfig, +}; use std::convert::Infallible; use std::sync::Arc; use tokio::time::Instant; @@ -159,6 +161,18 @@ async fn process_single_request( let result = methods::vote_accounts::get_vote_accounts(state, config).await; json_serialize_response(id, result).await } + "simulateTransaction" => { + let transaction: String = match extract_param(&rpc_request.params, 0) { + Ok(p) => p, + Err(e) => return make_error_response(id, -32602, e), + }; + let config: Option = + extract_param(&rpc_request.params, 1).ok().flatten(); + let result = + methods::simulate_transaction::simulate_transaction(state, transaction, config) + .await; + json_serialize_response(id, result).await + } "getAccountInfo" => { let start_time = Instant::now(); diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index f9ef5f8..c343724 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -85,6 +85,9 @@ pub async fn run(config: &str) -> cloudbreak_core::Result<()> { // Setup optional module cache let gpa_processor = GpaProcessor::new(config.gpa_cache.clone()); + let simulation_supported = indexer_filter.supports_simulation(); + info!("simulateTransaction: supported: {}", simulation_supported); + let vote_accounts_supported = indexer_filter.supports_vote_accounts(); let stakes_cache: vote_accounts_cache::SharedStakesSnapshot = Arc::new(RwLock::new(Arc::new( vote_accounts_cache::StakesSnapshot::empty(), @@ -133,6 +136,7 @@ pub async fn run(config: &str) -> cloudbreak_core::Result<()> { vote_accounts_supported, stakes_cache, max_multiple_accounts, + simulation_supported, ); info!("Server is starting..."); diff --git a/crates/api/src/methods/mod.rs b/crates/api/src/methods/mod.rs index 7e06b25..d51e07b 100644 --- a/crates/api/src/methods/mod.rs +++ b/crates/api/src/methods/mod.rs @@ -18,6 +18,7 @@ pub mod get_token_account_balance; pub mod mint; pub mod mint_accounts; pub mod program; +pub mod simulate_transaction; pub mod slot; pub mod token; pub mod version; diff --git a/crates/api/src/methods/simulate_transaction.rs b/crates/api/src/methods/simulate_transaction.rs new file mode 100644 index 0000000..dc8bc34 --- /dev/null +++ b/crates/api/src/methods/simulate_transaction.rs @@ -0,0 +1,834 @@ +use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, RwLock}; + +use agave_feature_set::FeatureSet; +use agave_syscalls::create_program_runtime_environment_v1; +use base64::Engine as _; +use rust_decimal::prelude::ToPrimitive; +use sea_orm::EntityTrait; +use sea_orm::sqlx::{self, Row}; +use solana_account::{AccountSharedData, ReadableAccount}; +use solana_account_decoder::{UiAccountEncoding, encode_ui_account}; +use solana_account_decoder_client_types::{UiAccount, token::UiTokenAmount}; +use solana_address_lookup_table_interface::state::AddressLookupTable; +use solana_clock::{Epoch, Slot}; +use solana_commitment_config::CommitmentLevel; +use solana_compute_budget_instruction::instructions_processor::process_compute_budget_instructions; +use solana_fee_structure::FeeDetails; +use solana_hash::Hash; +use solana_loader_v3_interface::state::UpgradeableLoaderState; +use solana_message::v0::{LoadedAddresses, MessageAddressTableLookup}; +use solana_message::{AddressLoader, VersionedMessage}; +use solana_precompile_error::PrecompileError; +use solana_program_runtime::execution_budget::SVMTransactionExecutionBudget; +use solana_program_runtime::loaded_programs::{ + BlockRelation, ForkGraph, ProgramCacheEntry, ProgramRuntimeEnvironments, +}; +use solana_pubkey::Pubkey; +use solana_rpc_client_api::config::RpcSimulateTransactionConfig; +use solana_rpc_client_api::response::{ + Response as RpcResponse, RpcBlockhash, RpcResponseContext, RpcSimulateTransactionResult, +}; +use solana_svm::account_loader::{CheckedTransactionDetails, TransactionCheckResult}; +use solana_svm::transaction_balances::{BalanceCollector, SvmTokenInfo}; +use solana_svm::transaction_processing_result::ProcessedTransaction; +use solana_svm::transaction_processor::{ + ExecutionRecordingConfig, LoadAndExecuteSanitizedTransactionsOutput, TransactionBatchProcessor, + TransactionProcessingConfig, TransactionProcessingEnvironment, +}; +use solana_svm_callback::{InvokeContextCallback, TransactionProcessingCallback}; +use solana_svm_transaction::svm_message::SVMStaticMessage; +use solana_nonce_account::verify_nonce_account; +use solana_transaction::sanitized::{MessageHash, SanitizedTransaction}; +use solana_transaction::versioned::VersionedTransaction; +use solana_transaction_error::{AddressLoaderError, TransactionError}; +use solana_transaction_status_client_types::option_serializer::OptionSerializer; +use solana_transaction_status_client_types::{ + UiCompiledInstruction, UiInnerInstructions, UiInstruction, UiLoadedAddresses, + UiReturnDataEncoding, UiTransactionEncoding, UiTransactionError, UiTransactionReturnData, + UiTransactionTokenBalance, +}; +use tokio::time::timeout; +use tracing::Instrument; + +use cloudbreak_entity::slots; + +use crate::db_query; +use crate::error::RpcError; +use crate::http::CloudbreakRpcState; + +/// Default cluster lamports-per-signature +const LAMPORTS_PER_SIGNATURE: u64 = 5000; +/// Cap on captured log bytes, mirroring Agave's simulation default. +const LOG_MESSAGES_BYTES_LIMIT: usize = 100 * 1000; + +#[tracing::instrument(name = "simtx_rpc", skip_all)] +pub async fn simulate_transaction( + state: &CloudbreakRpcState, + transaction: String, + config: Option, +) -> Result, RpcError> { + if !state.simulation_supported { + return Err(RpcError::InvalidParamsWithMessage( + "simulateTransaction is not supported on this node (requires a full, unfiltered index)" + .to_string(), + )); + } + + let config = config.unwrap_or_default(); + + if config.sig_verify && config.replace_recent_blockhash { + return Err(RpcError::InvalidParamsWithMessage( + "sigVerify may not be used with replaceRecentBlockhash".to_string(), + )); + } + + let encoding = config.encoding.unwrap_or(UiTransactionEncoding::Base58); + let tx_bytes = decode_transaction(&transaction, encoding)?; + let versioned_tx: VersionedTransaction = bincode::deserialize(&tx_bytes).map_err(|e| { + RpcError::InvalidParamsWithMessage(format!("failed to deserialize transaction: {e}")) + })?; + + if config.sig_verify && !versioned_tx.verify_with_results().iter().all(|ok| *ok) { + return Err(RpcError::InvalidParamsWithMessage( + "Transaction signature verification failure".to_string(), + )); + } + + let recent_blockhash = *versioned_tx.message.recent_blockhash(); + let env_blockhash = Hash::new_from_array(recent_blockhash.to_bytes()); + + let slot = resolve_slot(state, &config).await?; + + let address_table_lookups: Vec = match &versioned_tx.message { + VersionedMessage::V0(m) => m.address_table_lookups.clone(), + VersionedMessage::Legacy(_) => Vec::new(), + }; + let table_keys: Vec = address_table_lookups.iter().map(|l| l.account_key).collect(); + let tables = fetch_lookup_tables(state, slot, &table_keys).await?; + let address_loader = SimulationAddressLoader::new(tables); + + // Resolve ALT addresses for the response's `loadedAddresses` (None for legacy). + let loaded_addresses = if address_table_lookups.is_empty() { + None + } else { + address_loader + .clone() + .load_addresses(&address_table_lookups) + .ok() + .map(|la| UiLoadedAddresses { + writable: la.writable.iter().map(|p| p.to_string()).collect(), + readonly: la.readonly.iter().map(|p| p.to_string()).collect(), + }) + }; + + let reserved_keys: HashSet = HashSet::new(); + let sanitized_tx = SanitizedTransaction::try_create( + versioned_tx, + MessageHash::Compute, + None, + address_loader, + &reserved_keys, + ) + .map_err(|e| RpcError::InvalidParamsWithMessage(format!("invalid transaction: {e}")))?; + + let mut account_keys: Vec = sanitized_tx + .message() + .account_keys() + .iter() + .copied() + .collect(); + account_keys.extend(sysvar_account_ids()); + let mut fetched = fetch_accounts(state, slot, &account_keys).await?; + + if let Some(accounts_config) = &config.accounts + && accounts_config.addresses.len() > sanitized_tx.message().account_keys().len() + { + return Err(RpcError::InvalidParamsWithMessage( + "Too many accounts provided; max is the number of accounts in the transaction" + .to_string(), + )); + } + + let programdata_keys = programdata_addresses(&fetched); + if !programdata_keys.is_empty() { + fetched.extend(fetch_accounts(state, slot, &programdata_keys).await?); + } + + let nonce = resolve_nonce(&sanitized_tx, &fetched); + let blockhash_recent = config.replace_recent_blockhash + || blockhash_is_recent(state, &recent_blockhash.to_string()).await?; + let (nonce_address, blockhash_lamports_per_signature, runnable) = if blockhash_recent { + (None, LAMPORTS_PER_SIGNATURE, true) + } else if let Some((address, lamports_per_signature)) = nonce { + (Some(address), lamports_per_signature, true) + } else { + (None, LAMPORTS_PER_SIGNATURE, false) + }; + + let feature_set = FeatureSet::all_enabled(); + let bank = SimulationBank::new(fetched, feature_set.clone()); + + let compute_budget_limits = match process_compute_budget_instructions( + sanitized_tx.program_instructions_iter(), + &feature_set, + ) { + Ok(limits) => limits, + Err(e) => return Ok(response(slot, error_only(e))), + }; + + let signature_fee = sanitized_tx + .num_transaction_signatures() + .saturating_mul(blockhash_lamports_per_signature); + let fee_details = FeeDetails::new(signature_fee, compute_budget_limits.get_prioritization_fee()); + let budget_and_limits = compute_budget_limits.get_compute_budget_and_limits( + compute_budget_limits.loaded_accounts_bytes, + fee_details, + false, + ); + + let check_result: TransactionCheckResult = if runnable { + Ok(CheckedTransactionDetails::new(nonce_address, budget_and_limits)) + } else { + Err(TransactionError::BlockhashNotFound) + }; + + let output = execute( + &bank, + &sanitized_tx, + slot, + 0, + env_blockhash, + blockhash_lamports_per_signature, + &feature_set, + check_result, + )?; + + let value = map_result( + output, + &config, + loaded_addresses, + recent_blockhash.to_string(), + slot, + )?; + Ok(response(slot, value)) +} + +async fn resolve_slot( + state: &CloudbreakRpcState, + config: &RpcSimulateTransactionConfig, +) -> Result { + let commitment = config + .commitment + .map(|c| crate::methods::resolve_commitment(c.commitment, state.processed_commitment)) + .transpose()? + .unwrap_or(CommitmentLevel::Finalized); + + let slot = match &state.slot_syncronizer_data { + Some(data) => data + .read() + .expect("Failed to read slot syncronizer data") + .get_slot_for_commitment(commitment), + None => { + let slot_model = slots::Entity::find_by_id(commitment as i32) + .one(&state.database) + .instrument(tracing::info_span!("simtx_slot_db")) + .await?; + slot_model.ok_or(RpcError::InternalError)?.slot as u64 + } + }; + + if let Some(min_context_slot) = config.min_context_slot + && slot < min_context_slot + { + return Err(RpcError::RpcSlotBehindMinContextSlot { rpc_slot: slot }); + } + + Ok(slot) +} + +async fn blockhash_is_recent(state: &CloudbreakRpcState, blockhash: &str) -> Result { + let pool = state.database.get_postgres_connection_pool(); + timeout(state.queries_timeout, async { + sqlx::query_scalar::<_, bool>( + "SELECT EXISTS(SELECT 1 FROM recent_blockhashes WHERE blockhash = $1)", + ) + .bind(blockhash) + .fetch_one(pool) + .await + }) + .await + .map_err(|_| { + tracing::error!(target: "simtx", "recent_blockhashes query timed out"); + RpcError::InternalError + })? + .map_err(|e| { + tracing::error!(target: "simtx", "recent_blockhashes query error: {e}"); + RpcError::InternalError + }) +} + +fn resolve_nonce( + sanitized_tx: &SanitizedTransaction, + accounts: &HashMap, +) -> Option<(Pubkey, u64)> { + let message = sanitized_tx.message(); + let nonce_address = message.get_durable_nonce()?; + let (account, _) = accounts.get(nonce_address)?; + let data = verify_nonce_account(account, message.recent_blockhash())?; + message + .get_ix_signers(0) + .any(|signer| signer.to_bytes() == data.authority.to_bytes()) + .then_some((*nonce_address, data.fee_calculator.lamports_per_signature)) +} + +/// Decode the wire transaction per the requested encoding (base58 or base64). +fn decode_transaction(data: &str, encoding: UiTransactionEncoding) -> Result, RpcError> { + match encoding { + UiTransactionEncoding::Base58 | UiTransactionEncoding::Binary => bs58::decode(data) + .into_vec() + .map_err(|e| RpcError::InvalidParamsWithMessage(format!("invalid base58: {e}"))), + UiTransactionEncoding::Base64 => base64::prelude::BASE64_STANDARD + .decode(data) + .map_err(|e| RpcError::InvalidParamsWithMessage(format!("invalid base64: {e}"))), + other => Err(RpcError::InvalidParamsWithMessage(format!( + "unsupported transaction encoding: {other:?}" + ))), + } +} + +fn response(slot: u64, value: RpcSimulateTransactionResult) -> RpcResponse { + RpcResponse { + context: RpcResponseContext { + slot, + api_version: None, + }, + value, + } +} + +fn error_only(err: TransactionError) -> RpcSimulateTransactionResult { + RpcSimulateTransactionResult { + err: Some(err.into()), + logs: None, + accounts: None, + units_consumed: None, + loaded_accounts_data_size: None, + return_data: None, + inner_instructions: None, + replacement_blockhash: None, + fee: None, + pre_balances: None, + post_balances: None, + pre_token_balances: None, + post_token_balances: None, + loaded_addresses: None, + } +} + +/// Map the SVM output for the single simulated transaction into the RPC result +fn map_result( + output: LoadAndExecuteSanitizedTransactionsOutput, + config: &RpcSimulateTransactionConfig, + loaded_addresses: Option, + recent_blockhash: String, + slot: u64, +) -> Result { + let (pre_balances, post_balances, pre_token_balances, post_token_balances) = + extract_balances(output.balance_collector); + + let processing = output + .processing_results + .into_iter() + .next() + .ok_or(RpcError::InternalError)?; + + let replacement_blockhash = config.replace_recent_blockhash.then(|| RpcBlockhash { + blockhash: recent_blockhash, + last_valid_block_height: slot.saturating_add(150), + }); + + // On any failure Agave returns all-`None` for the requested accounts. + let none_accounts = config + .accounts + .as_ref() + .map(|c| vec![None; c.addresses.len()]); + + #[allow(clippy::type_complexity)] + let (err, logs, accounts, units_consumed, loaded_accounts_data_size, fee, return_data, inner_instructions): ( + Option, + Option>, + Option>>, + Option, + Option, + Option, + Option, + Option>, + ) = match processing { + Ok(ProcessedTransaction::Executed(executed)) => { + let details = &executed.execution_details; + let err: Option = + details.status.as_ref().err().cloned().map(Into::into); + let logs = Some(details.log_messages.clone().unwrap_or_default()); + let return_data = details.return_data.as_ref().map(|rd| UiTransactionReturnData { + program_id: rd.program_id.to_string(), + data: ( + base64::prelude::BASE64_STANDARD.encode(&rd.data), + UiReturnDataEncoding::Base64, + ), + }); + let inner_instructions = config + .inner_instructions + .then(|| details.inner_instructions.as_ref().map(map_inner_instructions)) + .flatten(); + let accounts = if err.is_some() { + none_accounts + } else { + build_requested_accounts(config, &executed.loaded_transaction.accounts) + }; + ( + err, + logs, + accounts, + Some(details.executed_units), + Some(executed.loaded_transaction.loaded_accounts_data_size), + Some(executed.loaded_transaction.fee_details.total_fee()), + return_data, + inner_instructions, + ) + } + Ok(ProcessedTransaction::FeesOnly(fees)) => ( + Some(UiTransactionError::from(TransactionError::clone(&fees.load_error))), + Some(Vec::new()), + none_accounts, + None, + None, + Some(fees.fee_details.total_fee()), + None, + None, + ), + Err(e) => ( + Some(UiTransactionError::from(e)), + Some(Vec::new()), + none_accounts, + None, + None, + None, + None, + None, + ), + }; + + Ok(RpcSimulateTransactionResult { + err, + logs, + accounts, + units_consumed, + loaded_accounts_data_size, + return_data, + inner_instructions, + replacement_blockhash, + fee, + pre_balances, + post_balances, + pre_token_balances, + post_token_balances, + loaded_addresses, + }) +} + +#[allow(clippy::type_complexity)] +fn extract_balances( + collector: Option, +) -> ( + Option>, + Option>, + Option>, + Option>, +) { + let Some(collector) = collector else { + return (None, None, None, None); + }; + let (mut native_pre, mut native_post, mut token_pre, mut token_post) = collector.into_vecs(); + let native = |v: &mut Vec>| (!v.is_empty()).then(|| v.swap_remove(0)); + let token = |v: &mut Vec>| { + (!v.is_empty()).then(|| v.swap_remove(0).iter().map(to_ui_token_balance).collect()) + }; + ( + native(&mut native_pre), + native(&mut native_post), + token(&mut token_pre), + token(&mut token_post), + ) +} + +fn to_ui_token_balance(info: &SvmTokenInfo) -> UiTransactionTokenBalance { + UiTransactionTokenBalance { + account_index: info.account_index, + mint: info.mint.to_string(), + ui_token_amount: ui_token_amount(info.amount, info.decimals), + owner: OptionSerializer::Some(info.owner.to_string()), + program_id: OptionSerializer::Some(info.program_id.to_string()), + } +} + +fn ui_token_amount(amount: u64, decimals: u8) -> UiTokenAmount { + let ui_amount_string = + rust_decimal::Decimal::try_from_i128_with_scale(amount as i128, decimals as u32) + .map(|d| d.normalize().to_string()) + .unwrap_or_else(|_| amount.to_string()); + let ui_amount = ui_amount_string.parse::().ok(); + UiTokenAmount { + ui_amount, + decimals, + amount: amount.to_string(), + ui_amount_string, + } +} + +/// Map recorded inner instructions; the outer index is the top-level instruction +/// index, and empty inner lists are skipped (matching Agave). +fn map_inner_instructions( + list: &solana_message::inner_instruction::InnerInstructionsList, +) -> Vec { + list.iter() + .enumerate() + .filter(|(_, inner)| !inner.is_empty()) + .map(|(index, inner)| UiInnerInstructions { + index: index as u8, + instructions: inner + .iter() + .map(|ix| { + UiInstruction::Compiled(UiCompiledInstruction::from( + &ix.instruction, + Some(u32::from(ix.stack_height)), + )) + }) + .collect(), + }) + .collect() +} + +/// Post-execution state of each requested address, or `None` if the tx did not load it +fn build_requested_accounts( + config: &RpcSimulateTransactionConfig, + post_accounts: &[(Pubkey, AccountSharedData)], +) -> Option>> { + let accounts_config = config.accounts.as_ref()?; + let encoding = accounts_config.encoding.unwrap_or(UiAccountEncoding::Base64); + let out = accounts_config + .addresses + .iter() + .map(|addr| { + let pubkey: Pubkey = addr.parse().ok()?; + let account = post_accounts + .iter() + .find(|(key, _)| key == &pubkey) + .map(|(_, acc)| acc)?; + Some(encode_ui_account(&pubkey, account, encoding, None, None)) + }) + .collect(); + Some(out) +} + +/// In-memory account store the SVM reads through; the callback only serves what was pre-fetched here +struct SimulationBank { + accounts: HashMap, + feature_set: FeatureSet, +} + +impl SimulationBank { + fn new(accounts: HashMap, feature_set: FeatureSet) -> Self { + Self { + accounts, + feature_set, + } + } +} + +impl InvokeContextCallback for SimulationBank { + fn is_precompile(&self, program_id: &Pubkey) -> bool { + agave_precompiles::is_precompile(program_id, |feature_id| { + self.feature_set.is_active(feature_id) + }) + } + + fn process_precompile( + &self, + program_id: &Pubkey, + data: &[u8], + instruction_datas: Vec<&[u8]>, + ) -> Result<(), PrecompileError> { + match agave_precompiles::get_precompile(program_id, |feature_id| { + self.feature_set.is_active(feature_id) + }) { + Some(precompile) => precompile.verify(data, &instruction_datas, &self.feature_set), + None => Err(PrecompileError::InvalidPublicKey), + } + } +} + +impl TransactionProcessingCallback for SimulationBank { + fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, Slot)> { + self.accounts.get(pubkey).cloned() + } +} + +/// Resolves Address Lookup Tables from a pre-fetched `table -> addresses` map. +/// Cheap to clone (the trait takes `self` by value); the map is behind an `Arc`. +#[derive(Clone)] +struct SimulationAddressLoader { + tables: Arc>>, +} + +impl SimulationAddressLoader { + fn new(tables: HashMap>) -> Self { + Self { + tables: Arc::new(tables), + } + } +} + +impl AddressLoader for SimulationAddressLoader { + fn load_addresses( + self, + lookups: &[MessageAddressTableLookup], + ) -> Result { + let mut loaded = LoadedAddresses::default(); + for lookup in lookups { + let table = self + .tables + .get(&lookup.account_key) + .ok_or(AddressLoaderError::LookupTableAccountNotFound)?; + for &index in &lookup.writable_indexes { + loaded.writable.push( + *table + .get(index as usize) + .ok_or(AddressLoaderError::InvalidLookupIndex)?, + ); + } + for &index in &lookup.readonly_indexes { + loaded.readonly.push( + *table + .get(index as usize) + .ok_or(AddressLoaderError::InvalidLookupIndex)?, + ); + } + } + Ok(loaded) + } +} + +async fn fetch_accounts( + state: &CloudbreakRpcState, + slot_bound: u64, + keys: &[Pubkey], +) -> Result, RpcError> { + if keys.is_empty() { + return Ok(HashMap::new()); + } + + let template = include_str!("../db/getMultipleAccounts.sql"); + + let mut array_literal = String::with_capacity(keys.len() * 95 + 8); + array_literal.push_str("ARRAY["); + for (i, pk) in keys.iter().enumerate() { + if i > 0 { + array_literal.push_str(", "); + } + array_literal.push_str(&format!("'\\x{}'::bytea", hex::encode(pk.as_ref()))); + } + array_literal.push(']'); + + let sql = template.replace("$1", &array_literal); + let sql = sql.replace("$2", &slot_bound.to_string()); + let sql = db_query::add_trace_traceparent_to_query(&sql); + + let pool = state.database.get_postgres_connection_pool(); + let rows = timeout(state.queries_timeout, async { + let span = tracing::info_span!("simtx_accounts_db"); + sqlx::raw_sql(&sql).fetch_all(pool).instrument(span).await + }) + .await + .map_err(|_elapsed| { + tracing::error!(target: "simtx", "simulateTransaction account query timed out"); + RpcError::InternalError + })? + .map_err(|e| { + tracing::error!(target: "simtx", "simulateTransaction account query error: {e}"); + RpcError::InternalError + })?; + + let mut map = HashMap::with_capacity(rows.len()); + for row in &rows { + let pubkey_bytes: Vec = row.get("pubkey"); + let Ok(pubkey) = Pubkey::try_from(pubkey_bytes.as_slice()) else { + continue; + }; + let owner_bytes: Vec = row.get("owner"); + let owner = Pubkey::try_from(owner_bytes.as_slice()).map_err(|_| RpcError::InternalError)?; + let lamports = row.get::("lamports") as u64; + let slot = row.get::("slot") as u64; + let executable: bool = row.get("executable"); + let rent_epoch = row + .get::("rent_epoch") + .to_u64() + .unwrap_or(0); + let data: Vec = row.get("data"); + + let account = AccountSharedData::create_from_existing_shared_data( + lamports, + Arc::new(data), + owner, + executable, + rent_epoch, + ); + map.insert(pubkey, (account, slot)); + } + + Ok(map) +} + +/// Fetch the lookup-table accounts and decode each into its ordered address list. +async fn fetch_lookup_tables( + state: &CloudbreakRpcState, + slot_bound: u64, + table_keys: &[Pubkey], +) -> Result>, RpcError> { + let accounts = fetch_accounts(state, slot_bound, table_keys).await?; + let mut tables = HashMap::with_capacity(accounts.len()); + for (key, (account, _)) in &accounts { + let table = AddressLookupTable::deserialize(account.data()).map_err(|e| { + RpcError::InvalidParamsWithMessage(format!("invalid address lookup table {key}: {e}")) + })?; + tables.insert(*key, table.addresses.to_vec()); + } + Ok(tables) +} + +/// Sysvar accounts the SVM sysvar cache reads through the callback. Fetched +/// explicitly since transactions rarely list them; the Instructions sysvar is +/// synthesised per-tx by the runtime and is not fetched. +fn sysvar_account_ids() -> [Pubkey; 9] { + [ + Pubkey::from_str_const("SysvarC1ock11111111111111111111111111111111"), + Pubkey::from_str_const("SysvarEpochSchedu1e111111111111111111111111"), + Pubkey::from_str_const("SysvarEpochRewards1111111111111111111111111"), + Pubkey::from_str_const("SysvarRent111111111111111111111111111111111"), + Pubkey::from_str_const("SysvarS1otHashes111111111111111111111111111"), + Pubkey::from_str_const("SysvarStakeHistory1111111111111111111111111"), + Pubkey::from_str_const("SysvarLastRestartS1ot1111111111111111111111"), + Pubkey::from_str_const("SysvarRecentB1ockHashes11111111111111111111"), + Pubkey::from_str_const("SysvarFees111111111111111111111111111111111"), + ] +} + +/// ProgramData addresses of upgradeable programs in the fetched map (their ELF +/// lives in a separate account not listed by the transaction). +fn programdata_addresses(accounts: &HashMap) -> Vec { + let loader = solana_sdk_ids::bpf_loader_upgradeable::id(); + let mut out = Vec::new(); + for (_key, (account, _)) in accounts { + if account.owner() != &loader { + continue; + } + if let Ok(UpgradeableLoaderState::Program { + programdata_address, + }) = bincode::deserialize::(account.data()) + { + out.push(programdata_address); + } + } + out +} + +// Engine: stand up a `TransactionBatchProcessor` (builtins, program-runtime environment, sysvar cache) over the pre-fetched bank and execute read-only. + +struct SimulationForkGraph; + +impl ForkGraph for SimulationForkGraph { + fn relationship(&self, a: Slot, b: Slot) -> BlockRelation { + match a.cmp(&b) { + std::cmp::Ordering::Less => BlockRelation::Ancestor, + std::cmp::Ordering::Equal => BlockRelation::Equal, + std::cmp::Ordering::Greater => BlockRelation::Descendant, + } + } +} + +/// Build the processor and execute the transaction read-only; nothing is committed. +#[allow(clippy::too_many_arguments)] +fn execute( + bank: &SimulationBank, + sanitized_tx: &SanitizedTransaction, + slot: Slot, + epoch: Epoch, + blockhash: Hash, + blockhash_lamports_per_signature: u64, + feature_set: &FeatureSet, + check_result: TransactionCheckResult, +) -> Result { + let svm_feature_set = feature_set.runtime_features(); + + let program_runtime_v1 = create_program_runtime_environment_v1( + &svm_feature_set, + &SVMTransactionExecutionBudget::new_with_defaults(false), + false, + false, + ) + .map_err(|e| { + tracing::error!(target: "simtx", "failed to create program runtime env: {e}"); + RpcError::InternalError + })?; + + let fork_graph = Arc::new(RwLock::new(SimulationForkGraph)); + let mut processor = + TransactionBatchProcessor::::new_uninitialized(slot, epoch); + { + let mut cache = processor.global_program_cache.write().unwrap(); + cache.set_fork_graph(Arc::downgrade(&fork_graph)); + cache.latest_root_slot = slot; + } + + // Install v1; v2 stays the default empty loader (loader-v4 compiles via v1). + let mut environments = ProgramRuntimeEnvironments::default(); + environments.program_runtime_v1 = Arc::new(program_runtime_v1); + processor.set_environments(environments); + + for builtin in solana_builtins::BUILTINS { + processor.add_builtin( + builtin.program_id, + ProgramCacheEntry::new_builtin(0, builtin.name.len(), builtin.entrypoint), + ); + } + + processor.fill_missing_sysvar_cache_entries(bank); + + let environment = TransactionProcessingEnvironment { + blockhash, + blockhash_lamports_per_signature, + epoch_total_stake: 0, + feature_set: svm_feature_set, + program_runtime_environments_for_execution: processor.get_environments_for_epoch(epoch), + program_runtime_environments_for_deployment: processor.get_environments_for_epoch(epoch), + rent: solana_rent::Rent::default(), + }; + + let config = TransactionProcessingConfig { + account_overrides: None, + check_program_deployment_slot: false, + log_messages_bytes_limit: Some(LOG_MESSAGES_BYTES_LIMIT), + limit_to_load_programs: true, + recording_config: ExecutionRecordingConfig::new_single_setting(true), + drop_on_failure: false, + all_or_nothing: false, + }; + + Ok(processor.load_and_execute_sanitized_transactions( + bank, + std::slice::from_ref(sanitized_tx), + vec![check_result], + &environment, + &config, + )) +} diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index d802f7b..fc96a28 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -117,6 +117,11 @@ impl AccountSelectorConfig { pub fn supports_vote_accounts(&self) -> bool { self.is_program_selected(&VOTE_PROGRAM_ID) && self.is_program_selected(&STAKE_PROGRAM_ID) } + + /// `simulateTransaction` requires a full unfiltered index + pub fn supports_simulation(&self) -> bool { + self.include.is_empty() && self.exclude.is_empty() + } } pub const VOTE_PROGRAM_ID: Pubkey = diff --git a/crates/index/src/db_queries.rs b/crates/index/src/db_queries.rs index 53707b5..6a2e697 100644 --- a/crates/index/src/db_queries.rs +++ b/crates/index/src/db_queries.rs @@ -10,8 +10,8 @@ use std::{ use sea_orm::{ ActiveValue::{NotSet, Set}, - ColumnTrait, Condition, ConnectionTrait, DatabaseConnection, EntityTrait, QueryFilter, - Statement, Value, + ColumnTrait, Condition, ConnectionTrait, DatabaseBackend, DatabaseConnection, EntityTrait, + QueryFilter, Statement, Value, prelude::Expr, sea_query::{Alias, OnConflict}, }; @@ -354,6 +354,52 @@ pub async fn insert_slot( } } +/// Retained window of `recent_blockhashes` rows (~5 minutes of slots at mainnet cadence). +const RECENT_BLOCKHASH_RETENTION_SLOTS: u64 = 900; + +/// Records a block's blockhash for simulation's blockhash-age check, pruning older rows. +pub async fn insert_recent_blockhash( + slot: u64, + blockhash: String, + db: &DatabaseConnection, + config: &IndexConfig, +) { + if blockhash.is_empty() { + return; + } + + let query_timeout = Duration::from_secs(config.database.finalize_slot_queries_timeout); + let prune_before = slot.saturating_sub(RECENT_BLOCKHASH_RETENTION_SLOTS) as i64; + + let insert = Statement::from_sql_and_values( + DatabaseBackend::Postgres, + "INSERT INTO recent_blockhashes (slot, blockhash) VALUES ($1, $2) \ + ON CONFLICT (slot) DO UPDATE SET blockhash = EXCLUDED.blockhash", + [Value::from(slot as i64), Value::from(blockhash)], + ); + let prune = Statement::from_sql_and_values( + DatabaseBackend::Postgres, + "DELETE FROM recent_blockhashes WHERE slot < $1", + [Value::from(prune_before)], + ); + + let result = timeout(query_timeout, async { + db.execute(insert).await?; + db.execute(prune).await?; + Ok::<(), sea_orm::DbErr>(()) + }) + .await + .unwrap_or_else(|elapsed| { + tracing::error!("insert_recent_blockhash timeout ERROR: {}", elapsed); + Err(sea_orm::DbErr::RecordNotInserted) + }); + + if let Err(e) = result { + tracing::error!("insert_recent_blockhash: failed for slot {}: {}", slot, e); + metrics::increment_db_errors(); + } +} + /// The latest persisted slot for each commitment level, plus the finalized→confirmed lag. /// /// The `slots` table holds exactly one row per commitment (its primary key), updated to the diff --git a/crates/index/src/modules/save_block.rs b/crates/index/src/modules/save_block.rs index a0644f1..c29ec82 100644 --- a/crates/index/src/modules/save_block.rs +++ b/crates/index/src/modules/save_block.rs @@ -237,6 +237,8 @@ pub async fn save_block( ) .await; + db_queries::insert_recent_blockhash(slot, block.blockhash.clone(), db, &config).await; + let elapsed = start_time.elapsed().as_secs_f64(); metrics::record_block_processing(elapsed, "block"); } diff --git a/crates/migration/src/lib.rs b/crates/migration/src/lib.rs index 314add6..09cbfd9 100644 --- a/crates/migration/src/lib.rs +++ b/crates/migration/src/lib.rs @@ -18,6 +18,7 @@ mod m20260325_000000_drop_temp_tables; mod m20260414_000000_create_indexer_filters_table; mod m20260522_000000_create_environment_info_table; mod m20260528_000000_create_epoch_stakes_table; +mod m20260703_000000_create_recent_blockhashes_table; pub struct Migrator; @@ -36,6 +37,7 @@ impl MigratorTrait for Migrator { Box::new(m20260414_000000_create_indexer_filters_table::Migration), Box::new(m20260522_000000_create_environment_info_table::Migration), Box::new(m20260528_000000_create_epoch_stakes_table::Migration), + Box::new(m20260703_000000_create_recent_blockhashes_table::Migration), ] } } diff --git a/crates/migration/src/m20260703_000000_create_recent_blockhashes_table.rs b/crates/migration/src/m20260703_000000_create_recent_blockhashes_table.rs new file mode 100644 index 0000000..dd908dc --- /dev/null +++ b/crates/migration/src/m20260703_000000_create_recent_blockhashes_table.rs @@ -0,0 +1,35 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let connection = manager.get_connection(); + + connection + .execute_unprepared( + r#" + CREATE TABLE IF NOT EXISTS recent_blockhashes ( + slot BIGINT NOT NULL PRIMARY KEY, + blockhash TEXT NOT NULL + ); + CREATE INDEX IF NOT EXISTS recent_blockhashes_blockhash_idx + ON recent_blockhashes (blockhash); + "#, + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .get_connection() + .execute_unprepared("DROP TABLE IF EXISTS recent_blockhashes;") + .await?; + + Ok(()) + } +} From 4f544d08f485cb0605b8e24c7e125af3d3caacbe Mon Sep 17 00:00:00 2001 From: ArthurPaivaT Date: Tue, 7 Jul 2026 12:52:42 +0000 Subject: [PATCH 02/13] Enable feature sets on transaction simulation --- crates/api/src/http/mod.rs | 11 ++- .../api/src/methods/simulate_transaction.rs | 68 ++++++++++++++++++- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/crates/api/src/http/mod.rs b/crates/api/src/http/mod.rs index 143985a..b299042 100644 --- a/crates/api/src/http/mod.rs +++ b/crates/api/src/http/mod.rs @@ -9,14 +9,21 @@ use crate::modules::cache::GpaProcessor; use crate::modules::vote_accounts_cache::SharedStakesSnapshot; use crate::query_tracker_client::QueryTrackerClient; use crate::slot_syncronizer::SlotSyncronizerData; +use agave_feature_set::FeatureSet; use hyper::StatusCode; use sea_orm::DatabaseConnection; use serde::{Deserialize, Serialize}; use solana_rpc_client_api::response::Response as RpcResponse; use std::sync::{Arc, RwLock}; -use std::time::Duration; +use std::time::{Duration, Instant}; use cloudbreak_core::{AccountSelectorConfig, ProcessedCommitmentBehavior}; +#[derive(Clone)] +pub struct CachedFeatureSet { + pub set: Arc, + pub built_at: Instant, +} + pub mod operational_endpoints; pub mod rpc; pub mod server; @@ -39,6 +46,7 @@ pub struct CloudbreakRpcState { pub stakes_cache: SharedStakesSnapshot, pub max_multiple_accounts: usize, pub simulation_supported: bool, + pub feature_set_cache: Arc>>, } impl CloudbreakRpcState { @@ -76,6 +84,7 @@ impl CloudbreakRpcState { stakes_cache, max_multiple_accounts, simulation_supported, + feature_set_cache: Arc::new(RwLock::new(None)), } } } diff --git a/crates/api/src/methods/simulate_transaction.rs b/crates/api/src/methods/simulate_transaction.rs index dc8bc34..b4c2c44 100644 --- a/crates/api/src/methods/simulate_transaction.rs +++ b/crates/api/src/methods/simulate_transaction.rs @@ -1,7 +1,8 @@ use std::collections::{HashMap, HashSet}; use std::sync::{Arc, RwLock}; +use std::time::{Duration, Instant}; -use agave_feature_set::FeatureSet; +use agave_feature_set::{FEATURE_NAMES, FeatureSet}; use agave_syscalls::create_program_runtime_environment_v1; use base64::Engine as _; use rust_decimal::prelude::ToPrimitive; @@ -55,7 +56,7 @@ use cloudbreak_entity::slots; use crate::db_query; use crate::error::RpcError; -use crate::http::CloudbreakRpcState; +use crate::http::{CachedFeatureSet, CloudbreakRpcState}; /// Default cluster lamports-per-signature const LAMPORTS_PER_SIGNATURE: u64 = 5000; @@ -166,7 +167,8 @@ pub async fn simulate_transaction( (None, LAMPORTS_PER_SIGNATURE, false) }; - let feature_set = FeatureSet::all_enabled(); + // Real cluster feature set, not all_enabled(): the latter enables gates mainnet hasn't (e.g. direct mapping) and diverges from mainnet. + let feature_set = active_feature_set(state, slot).await?.as_ref().clone(); let bank = SimulationBank::new(fetched, feature_set.clone()); let compute_budget_limits = match process_compute_budget_instructions( @@ -214,6 +216,66 @@ pub async fn simulate_transaction( Ok(response(slot, value)) } +const FEATURE_SET_TTL: Duration = Duration::from_secs(120); + +// Feature program id as raw bytes (avoids a cross-crate Pubkey version mismatch). +const FEATURE_PROGRAM_ID: [u8; 32] = [ + 3, 192, 160, 205, 203, 6, 210, 218, 239, 174, 130, 209, 111, 238, 122, 207, 97, 236, 115, 123, + 35, 72, 27, 33, 148, 106, 118, 112, 0, 0, 0, 0, +]; + +// Mirrors Bank::compute_active_feature_set; requires the full unfiltered index. +async fn active_feature_set( + state: &CloudbreakRpcState, + slot: Slot, +) -> Result, RpcError> { + // Scoped so the read guard is dropped before the await below. + { + let guard = state.feature_set_cache.read().unwrap(); + if let Some(cached) = guard.as_ref() + && cached.built_at.elapsed() < FEATURE_SET_TTL + { + return Ok(cached.set.clone()); + } + } + + let feature_set = Arc::new(build_active_feature_set(state, slot).await?); + *state.feature_set_cache.write().unwrap() = Some(CachedFeatureSet { + set: feature_set.clone(), + built_at: Instant::now(), + }); + Ok(feature_set) +} + +async fn build_active_feature_set( + state: &CloudbreakRpcState, + slot: Slot, +) -> Result { + let feature_ids: Vec = FEATURE_NAMES.keys().copied().collect(); + let accounts = fetch_accounts(state, slot, &feature_ids).await?; + + let mut feature_set = FeatureSet::default(); + for (feature_id, (account, _account_slot)) in &accounts { + if account.owner().to_bytes() != FEATURE_PROGRAM_ID { + continue; + } + if let Some(activated_at) = parse_feature_activated_at(account.data()) + && activated_at <= slot + { + feature_set.activate(feature_id, activated_at); + } + } + Ok(feature_set) +} + +// Feature account `activated_at`: bincode `Option` — 1 tag byte, then LE slot. +fn parse_feature_activated_at(data: &[u8]) -> Option { + if data.len() < 9 || data[0] == 0 { + return None; + } + Some(u64::from_le_bytes(data[1..9].try_into().ok()?)) +} + async fn resolve_slot( state: &CloudbreakRpcState, config: &RpcSimulateTransactionConfig, From cf3f85c6d3b094f4419e30af58108e6f278ffec3 Mon Sep 17 00:00:00 2001 From: ArthurPaivaT Date: Tue, 7 Jul 2026 18:31:19 +0000 Subject: [PATCH 03/13] Fix compute units on transaction simulation on feature gates --- crates/api/src/methods/simulate_transaction.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/api/src/methods/simulate_transaction.rs b/crates/api/src/methods/simulate_transaction.rs index b4c2c44..eb4dc08 100644 --- a/crates/api/src/methods/simulate_transaction.rs +++ b/crates/api/src/methods/simulate_transaction.rs @@ -21,7 +21,9 @@ use solana_loader_v3_interface::state::UpgradeableLoaderState; use solana_message::v0::{LoadedAddresses, MessageAddressTableLookup}; use solana_message::{AddressLoader, VersionedMessage}; use solana_precompile_error::PrecompileError; -use solana_program_runtime::execution_budget::SVMTransactionExecutionBudget; +use solana_program_runtime::execution_budget::{ + SVMTransactionExecutionBudget, SVMTransactionExecutionCost, +}; use solana_program_runtime::loaded_programs::{ BlockRelation, ForkGraph, ProgramCacheEntry, ProgramRuntimeEnvironments, }; @@ -186,7 +188,7 @@ pub async fn simulate_transaction( let budget_and_limits = compute_budget_limits.get_compute_budget_and_limits( compute_budget_limits.loaded_accounts_bytes, fee_details, - false, + feature_set.is_active(&agave_feature_set::raise_cpi_nesting_limit_to_8::id()), ); let check_result: TransactionCheckResult = if runnable { @@ -831,10 +833,12 @@ fn execute( check_result: TransactionCheckResult, ) -> Result { let svm_feature_set = feature_set.runtime_features(); + let simd_0268 = feature_set.is_active(&agave_feature_set::raise_cpi_nesting_limit_to_8::id()); + let simd_0339 = feature_set.is_active(&agave_feature_set::increase_cpi_account_info_limit::id()); let program_runtime_v1 = create_program_runtime_environment_v1( &svm_feature_set, - &SVMTransactionExecutionBudget::new_with_defaults(false), + &SVMTransactionExecutionBudget::new_with_defaults(simd_0268), false, false, ) @@ -846,6 +850,7 @@ fn execute( let fork_graph = Arc::new(RwLock::new(SimulationForkGraph)); let mut processor = TransactionBatchProcessor::::new_uninitialized(slot, epoch); + processor.set_execution_cost(SVMTransactionExecutionCost::new_with_defaults(simd_0339)); { let mut cache = processor.global_program_cache.write().unwrap(); cache.set_fork_graph(Arc::downgrade(&fork_graph)); From 4378787661aa197313463e6429cd22bdbc505a03 Mon Sep 17 00:00:00 2001 From: ArthurPaivaT Date: Fri, 10 Jul 2026 12:26:51 +0000 Subject: [PATCH 04/13] Multiple minor fixes on simulateTransaction and integration tests --- Cargo.lock | 15 +- Cargo.toml | 1 + crates/api/Cargo.toml | 2 + .../api/src/methods/simulate_transaction.rs | 292 +++++++++++++----- crates/index/src/db_queries.rs | 12 +- crates/index/src/modules/save_block.rs | 9 +- crates/integration_tests/README.md | 49 ++- crates/integration_tests/src/benchmark.rs | 39 ++- crates/integration_tests/src/config.rs | 18 ++ .../src/response_comparison.rs | 77 +++++ crates/integration_tests/src/sources/mod.rs | 38 ++- .../src/sources/victoria_logs.rs | 28 +- crates/integration_tests/src/utils.rs | 7 +- crates/migration/src/lib.rs | 2 + ..._add_block_height_to_recent_blockhashes.rs | 29 ++ 15 files changed, 510 insertions(+), 108 deletions(-) create mode 100644 crates/migration/src/m20260709_000000_add_block_height_to_recent_blockhashes.rs diff --git a/Cargo.lock b/Cargo.lock index 955b9ba..be0c4f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,6 +191,17 @@ dependencies = [ "solana-sdk-ids", ] +[[package]] +name = "agave-reserved-account-keys" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798e559c514af005950ea81586a3856f9297ecb80a7359057c19bf6717f5f537" +dependencies = [ + "agave-feature-set 4.0.0", + "solana-pubkey 4.1.0", + "solana-sdk-ids", +] + [[package]] name = "agave-snapshots" version = "3.1.12" @@ -1572,6 +1583,7 @@ version = "0.1.0" dependencies = [ "agave-feature-set 4.0.0", "agave-precompiles 4.0.0", + "agave-reserved-account-keys 4.0.0", "agave-syscalls 4.0.0", "anyhow", "async-stream", @@ -1610,6 +1622,7 @@ dependencies = [ "solana-commitment-config", "solana-compute-budget 4.0.0", "solana-compute-budget-instruction 4.0.0", + "solana-epoch-schedule", "solana-fee-structure", "solana-hash 4.2.0", "solana-loader-v3-interface", @@ -8398,7 +8411,7 @@ dependencies = [ "agave-feature-set 3.1.12", "agave-fs", "agave-precompiles 3.1.12", - "agave-reserved-account-keys", + "agave-reserved-account-keys 3.1.12", "agave-snapshots", "agave-syscalls 3.1.12", "agave-votor-messages", diff --git a/Cargo.toml b/Cargo.toml index a0087c7..7aceb40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,7 @@ agave-syscalls = { version = "4.0.0", features = ["agave-unstable-api"] } solana-builtins = { version = "4.0.0", features = ["agave-unstable-api"] } solana-bpf-loader-program = "4.0.0" agave-feature-set = { version = "=4.0.0", features = ["agave-unstable-api"] } +agave-reserved-account-keys = { version = "=4.0.0", features = ["agave-unstable-api"] } solana-compute-budget = { version = "4.0.0", features = ["agave-unstable-api"] } solana-compute-budget-instruction = { version = "4.0.0", features = ["agave-unstable-api"] } solana-svm-feature-set = "=4.0.0" diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index 736c1ff..e75f2d8 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -73,6 +73,8 @@ agave-syscalls = { workspace = true } solana-builtins = { workspace = true } solana-bpf-loader-program = { workspace = true } agave-feature-set = { workspace = true } +agave-reserved-account-keys = { workspace = true } +solana-epoch-schedule = { workspace = true } solana-compute-budget = { workspace = true } solana-compute-budget-instruction = { workspace = true } solana-svm-feature-set = { workspace = true } diff --git a/crates/api/src/methods/simulate_transaction.rs b/crates/api/src/methods/simulate_transaction.rs index eb4dc08..4712831 100644 --- a/crates/api/src/methods/simulate_transaction.rs +++ b/crates/api/src/methods/simulate_transaction.rs @@ -1,4 +1,5 @@ use std::collections::{HashMap, HashSet}; +use std::str::FromStr; use std::sync::{Arc, RwLock}; use std::time::{Duration, Instant}; @@ -62,8 +63,7 @@ use crate::http::{CachedFeatureSet, CloudbreakRpcState}; /// Default cluster lamports-per-signature const LAMPORTS_PER_SIGNATURE: u64 = 5000; -/// Cap on captured log bytes, mirroring Agave's simulation default. -const LOG_MESSAGES_BYTES_LIMIT: usize = 100 * 1000; +const MAX_PROCESSING_AGE: u64 = 150; #[tracing::instrument(name = "simtx_rpc", skip_all)] pub async fn simulate_transaction( @@ -88,7 +88,7 @@ pub async fn simulate_transaction( let encoding = config.encoding.unwrap_or(UiTransactionEncoding::Base58); let tx_bytes = decode_transaction(&transaction, encoding)?; - let versioned_tx: VersionedTransaction = bincode::deserialize(&tx_bytes).map_err(|e| { + let mut versioned_tx: VersionedTransaction = bincode::deserialize(&tx_bytes).map_err(|e| { RpcError::InvalidParamsWithMessage(format!("failed to deserialize transaction: {e}")) })?; @@ -98,11 +98,53 @@ pub async fn simulate_transaction( )); } - let recent_blockhash = *versioned_tx.message.recent_blockhash(); - let env_blockhash = Hash::new_from_array(recent_blockhash.to_bytes()); + let requested_addresses: Vec = match &config.accounts { + Some(accounts_config) => { + let encoding = accounts_config.encoding.unwrap_or(UiAccountEncoding::Base64); + if matches!( + encoding, + UiAccountEncoding::Binary | UiAccountEncoding::Base58 + ) { + return Err(RpcError::InvalidParamsWithMessage( + "base58 encoding not supported".to_string(), + )); + } + accounts_config + .addresses + .iter() + .map(|addr| { + addr.parse::().map_err(|e| { + RpcError::InvalidParamsWithMessage(format!("invalid account address: {e}")) + }) + }) + .collect::, _>>()? + } + None => Vec::new(), + }; + + let original_blockhash = *versioned_tx.message.recent_blockhash(); let slot = resolve_slot(state, &config).await?; + let tip = latest_blockhash(state, slot).await?; + let replacement_blockhash = if config.replace_recent_blockhash { + let (hash, block_height) = tip.ok_or(RpcError::InternalError)?; + versioned_tx + .message + .set_recent_blockhash(hash.to_bytes().into()); + Some(RpcBlockhash { + blockhash: hash.to_string(), + last_valid_block_height: block_height.saturating_add(MAX_PROCESSING_AGE), + }) + } else { + None + }; + + let env_blockhash = match tip { + Some((hash, _)) => hash, + None => Hash::new_from_array(original_blockhash.to_bytes()), + }; + let address_table_lookups: Vec = match &versioned_tx.message { VersionedMessage::V0(m) => m.address_table_lookups.clone(), VersionedMessage::Legacy(_) => Vec::new(), @@ -111,21 +153,25 @@ pub async fn simulate_transaction( let tables = fetch_lookup_tables(state, slot, &table_keys).await?; let address_loader = SimulationAddressLoader::new(tables); - // Resolve ALT addresses for the response's `loadedAddresses` (None for legacy). - let loaded_addresses = if address_table_lookups.is_empty() { - None - } else { - address_loader - .clone() - .load_addresses(&address_table_lookups) - .ok() - .map(|la| UiLoadedAddresses { - writable: la.writable.iter().map(|p| p.to_string()).collect(), - readonly: la.readonly.iter().map(|p| p.to_string()).collect(), - }) - }; + let loaded_addresses = address_loader + .clone() + .load_addresses(&address_table_lookups) + .map(|la| UiLoadedAddresses { + writable: la.writable.iter().map(|p| p.to_string()).collect(), + readonly: la.readonly.iter().map(|p| p.to_string()).collect(), + }) + .unwrap_or_else(|_| UiLoadedAddresses { + writable: Vec::new(), + readonly: Vec::new(), + }); + + // Real cluster feature set, not all_enabled(): the latter enables gates mainnet hasn't (e.g. direct mapping) and diverges from mainnet. + let feature_set = active_feature_set(state, slot).await?.as_ref().clone(); + + let mut reserved = agave_reserved_account_keys::ReservedAccountKeys::default(); + reserved.update_active_set(&feature_set); + let reserved_keys: HashSet = reserved.active; - let reserved_keys: HashSet = HashSet::new(); let sanitized_tx = SanitizedTransaction::try_create( versioned_tx, MessageHash::Compute, @@ -135,6 +181,13 @@ pub async fn simulate_transaction( ) .map_err(|e| RpcError::InvalidParamsWithMessage(format!("invalid transaction: {e}")))?; + if requested_addresses.len() > sanitized_tx.message().account_keys().len() { + return Err(RpcError::InvalidParamsWithMessage( + "Too many accounts provided; max is the number of accounts in the transaction" + .to_string(), + )); + } + let mut account_keys: Vec = sanitized_tx .message() .account_keys() @@ -142,25 +195,22 @@ pub async fn simulate_transaction( .copied() .collect(); account_keys.extend(sysvar_account_ids()); + account_keys.extend(requested_addresses.iter().copied()); let mut fetched = fetch_accounts(state, slot, &account_keys).await?; - if let Some(accounts_config) = &config.accounts - && accounts_config.addresses.len() > sanitized_tx.message().account_keys().len() - { - return Err(RpcError::InvalidParamsWithMessage( - "Too many accounts provided; max is the number of accounts in the transaction" - .to_string(), - )); - } - let programdata_keys = programdata_addresses(&fetched); if !programdata_keys.is_empty() { fetched.extend(fetch_accounts(state, slot, &programdata_keys).await?); } + let fallback_accounts: HashMap = requested_addresses + .iter() + .filter_map(|key| fetched.get(key).map(|entry| (*key, entry.clone()))) + .collect(); + let nonce = resolve_nonce(&sanitized_tx, &fetched); let blockhash_recent = config.replace_recent_blockhash - || blockhash_is_recent(state, &recent_blockhash.to_string()).await?; + || blockhash_is_recent(state, slot, &original_blockhash.to_string()).await?; let (nonce_address, blockhash_lamports_per_signature, runnable) = if blockhash_recent { (None, LAMPORTS_PER_SIGNATURE, true) } else if let Some((address, lamports_per_signature)) = nonce { @@ -169,8 +219,6 @@ pub async fn simulate_transaction( (None, LAMPORTS_PER_SIGNATURE, false) }; - // Real cluster feature set, not all_enabled(): the latter enables gates mainnet hasn't (e.g. direct mapping) and diverges from mainnet. - let feature_set = active_feature_set(state, slot).await?.as_ref().clone(); let bank = SimulationBank::new(fetched, feature_set.clone()); let compute_budget_limits = match process_compute_budget_instructions( @@ -178,13 +226,21 @@ pub async fn simulate_transaction( &feature_set, ) { Ok(limits) => limits, - Err(e) => return Ok(response(slot, error_only(e))), + Err(e) => { + return Ok(response( + slot, + error_only(e, &config, loaded_addresses, replacement_blockhash), + )); + } }; let signature_fee = sanitized_tx .num_transaction_signatures() .saturating_mul(blockhash_lamports_per_signature); - let fee_details = FeeDetails::new(signature_fee, compute_budget_limits.get_prioritization_fee()); + let fee_details = FeeDetails::new( + signature_fee, + compute_budget_limits.get_prioritization_fee(), + ); let budget_and_limits = compute_budget_limits.get_compute_budget_and_limits( compute_budget_limits.loaded_accounts_bytes, fee_details, @@ -192,16 +248,20 @@ pub async fn simulate_transaction( ); let check_result: TransactionCheckResult = if runnable { - Ok(CheckedTransactionDetails::new(nonce_address, budget_and_limits)) + Ok(CheckedTransactionDetails::new( + nonce_address, + budget_and_limits, + )) } else { Err(TransactionError::BlockhashNotFound) }; + let epoch = solana_epoch_schedule::EpochSchedule::default().get_epoch(slot); let output = execute( &bank, &sanitized_tx, slot, - 0, + epoch, env_blockhash, blockhash_lamports_per_signature, &feature_set, @@ -212,8 +272,8 @@ pub async fn simulate_transaction( output, &config, loaded_addresses, - recent_blockhash.to_string(), - slot, + replacement_blockhash, + &fallback_accounts, )?; Ok(response(slot, value)) } @@ -311,12 +371,21 @@ async fn resolve_slot( Ok(slot) } -async fn blockhash_is_recent(state: &CloudbreakRpcState, blockhash: &str) -> Result { +async fn blockhash_is_recent( + state: &CloudbreakRpcState, + slot: Slot, + blockhash: &str, +) -> Result { let pool = state.database.get_postgres_connection_pool(); timeout(state.queries_timeout, async { sqlx::query_scalar::<_, bool>( - "SELECT EXISTS(SELECT 1 FROM recent_blockhashes WHERE blockhash = $1)", + "SELECT EXISTS(SELECT 1 FROM \ + (SELECT blockhash FROM recent_blockhashes WHERE slot <= $1 \ + ORDER BY slot DESC LIMIT $2) recent \ + WHERE recent.blockhash = $3)", ) + .bind(slot as i64) + .bind(MAX_PROCESSING_AGE as i64) .bind(blockhash) .fetch_one(pool) .await @@ -332,6 +401,41 @@ async fn blockhash_is_recent(state: &CloudbreakRpcState, blockhash: &str) -> Res }) } +async fn latest_blockhash( + state: &CloudbreakRpcState, + slot: Slot, +) -> Result, RpcError> { + let pool = state.database.get_postgres_connection_pool(); + let row = timeout(state.queries_timeout, async { + sqlx::query( + "SELECT blockhash, COALESCE(block_height, slot) AS height \ + FROM recent_blockhashes WHERE slot <= $1 ORDER BY slot DESC LIMIT 1", + ) + .bind(slot as i64) + .fetch_optional(pool) + .await + }) + .await + .map_err(|_| { + tracing::error!(target: "simtx", "latest_blockhash query timed out"); + RpcError::InternalError + })? + .map_err(|e| { + tracing::error!(target: "simtx", "latest_blockhash query error: {e}"); + RpcError::InternalError + })?; + + let Some(row) = row else { + return Ok(None); + }; + let blockhash: String = row + .try_get("blockhash") + .map_err(|_| RpcError::InternalError)?; + let height: i64 = row.try_get("height").map_err(|_| RpcError::InternalError)?; + let hash = Hash::from_str(&blockhash).map_err(|_| RpcError::InternalError)?; + Ok(Some((hash, height as u64))) +} + fn resolve_nonce( sanitized_tx: &SanitizedTransaction, accounts: &HashMap, @@ -361,7 +465,10 @@ fn decode_transaction(data: &str, encoding: UiTransactionEncoding) -> Result RpcResponse { +fn response( + slot: u64, + value: RpcSimulateTransactionResult, +) -> RpcResponse { RpcResponse { context: RpcResponseContext { slot, @@ -371,22 +478,30 @@ fn response(slot: u64, value: RpcSimulateTransactionResult) -> RpcResponse RpcSimulateTransactionResult { +fn error_only( + err: TransactionError, + config: &RpcSimulateTransactionConfig, + loaded_addresses: UiLoadedAddresses, + replacement_blockhash: Option, +) -> RpcSimulateTransactionResult { RpcSimulateTransactionResult { err: Some(err.into()), - logs: None, - accounts: None, - units_consumed: None, - loaded_accounts_data_size: None, + logs: Some(Vec::new()), + accounts: config + .accounts + .as_ref() + .map(|c| vec![None; c.addresses.len()]), + units_consumed: Some(0), + loaded_accounts_data_size: Some(0), return_data: None, inner_instructions: None, - replacement_blockhash: None, + replacement_blockhash, fee: None, pre_balances: None, post_balances: None, pre_token_balances: None, post_token_balances: None, - loaded_addresses: None, + loaded_addresses: Some(loaded_addresses), } } @@ -394,9 +509,9 @@ fn error_only(err: TransactionError) -> RpcSimulateTransactionResult { fn map_result( output: LoadAndExecuteSanitizedTransactionsOutput, config: &RpcSimulateTransactionConfig, - loaded_addresses: Option, - recent_blockhash: String, - slot: u64, + loaded_addresses: UiLoadedAddresses, + replacement_blockhash: Option, + fallback_accounts: &HashMap, ) -> Result { let (pre_balances, post_balances, pre_token_balances, post_token_balances) = extract_balances(output.balance_collector); @@ -407,19 +522,22 @@ fn map_result( .next() .ok_or(RpcError::InternalError)?; - let replacement_blockhash = config.replace_recent_blockhash.then(|| RpcBlockhash { - blockhash: recent_blockhash, - last_valid_block_height: slot.saturating_add(150), - }); - - // On any failure Agave returns all-`None` for the requested accounts. let none_accounts = config .accounts .as_ref() .map(|c| vec![None; c.addresses.len()]); #[allow(clippy::type_complexity)] - let (err, logs, accounts, units_consumed, loaded_accounts_data_size, fee, return_data, inner_instructions): ( + let ( + err, + logs, + accounts, + units_consumed, + loaded_accounts_data_size, + fee, + return_data, + inner_instructions, + ): ( Option, Option>, Option>>, @@ -434,21 +552,33 @@ fn map_result( let err: Option = details.status.as_ref().err().cloned().map(Into::into); let logs = Some(details.log_messages.clone().unwrap_or_default()); - let return_data = details.return_data.as_ref().map(|rd| UiTransactionReturnData { - program_id: rd.program_id.to_string(), - data: ( - base64::prelude::BASE64_STANDARD.encode(&rd.data), - UiReturnDataEncoding::Base64, - ), - }); + let return_data = details + .return_data + .as_ref() + .map(|rd| UiTransactionReturnData { + program_id: rd.program_id.to_string(), + data: ( + base64::prelude::BASE64_STANDARD.encode(&rd.data), + UiReturnDataEncoding::Base64, + ), + }); let inner_instructions = config .inner_instructions - .then(|| details.inner_instructions.as_ref().map(map_inner_instructions)) + .then(|| { + details + .inner_instructions + .as_ref() + .map(map_inner_instructions) + }) .flatten(); let accounts = if err.is_some() { none_accounts } else { - build_requested_accounts(config, &executed.loaded_transaction.accounts) + build_requested_accounts( + config, + &executed.loaded_transaction.accounts, + fallback_accounts, + ) }; ( err, @@ -462,11 +592,13 @@ fn map_result( ) } Ok(ProcessedTransaction::FeesOnly(fees)) => ( - Some(UiTransactionError::from(TransactionError::clone(&fees.load_error))), + Some(UiTransactionError::from(TransactionError::clone( + &fees.load_error, + ))), Some(Vec::new()), none_accounts, - None, - None, + Some(0), + Some(fees.rollback_accounts.data_size() as u32), Some(fees.fee_details.total_fee()), None, None, @@ -475,8 +607,8 @@ fn map_result( Some(UiTransactionError::from(e)), Some(Vec::new()), none_accounts, - None, - None, + Some(0), + Some(0), None, None, None, @@ -497,7 +629,7 @@ fn map_result( post_balances, pre_token_balances, post_token_balances, - loaded_addresses, + loaded_addresses: Some(loaded_addresses), }) } @@ -577,9 +709,12 @@ fn map_inner_instructions( fn build_requested_accounts( config: &RpcSimulateTransactionConfig, post_accounts: &[(Pubkey, AccountSharedData)], + fallback_accounts: &HashMap, ) -> Option>> { let accounts_config = config.accounts.as_ref()?; - let encoding = accounts_config.encoding.unwrap_or(UiAccountEncoding::Base64); + let encoding = accounts_config + .encoding + .unwrap_or(UiAccountEncoding::Base64); let out = accounts_config .addresses .iter() @@ -588,7 +723,8 @@ fn build_requested_accounts( let account = post_accounts .iter() .find(|(key, _)| key == &pubkey) - .map(|(_, acc)| acc)?; + .map(|(_, acc)| acc) + .or_else(|| fallback_accounts.get(&pubkey).map(|(acc, _)| acc))?; Some(encode_ui_account(&pubkey, account, encoding, None, None)) }) .collect(); @@ -730,7 +866,8 @@ async fn fetch_accounts( continue; }; let owner_bytes: Vec = row.get("owner"); - let owner = Pubkey::try_from(owner_bytes.as_slice()).map_err(|_| RpcError::InternalError)?; + let owner = + Pubkey::try_from(owner_bytes.as_slice()).map_err(|_| RpcError::InternalError)?; let lamports = row.get::("lamports") as u64; let slot = row.get::("slot") as u64; let executable: bool = row.get("executable"); @@ -834,7 +971,8 @@ fn execute( ) -> Result { let svm_feature_set = feature_set.runtime_features(); let simd_0268 = feature_set.is_active(&agave_feature_set::raise_cpi_nesting_limit_to_8::id()); - let simd_0339 = feature_set.is_active(&agave_feature_set::increase_cpi_account_info_limit::id()); + let simd_0339 = + feature_set.is_active(&agave_feature_set::increase_cpi_account_info_limit::id()); let program_runtime_v1 = create_program_runtime_environment_v1( &svm_feature_set, @@ -884,7 +1022,7 @@ fn execute( let config = TransactionProcessingConfig { account_overrides: None, check_program_deployment_slot: false, - log_messages_bytes_limit: Some(LOG_MESSAGES_BYTES_LIMIT), + log_messages_bytes_limit: None, limit_to_load_programs: true, recording_config: ExecutionRecordingConfig::new_single_setting(true), drop_on_failure: false, diff --git a/crates/index/src/db_queries.rs b/crates/index/src/db_queries.rs index 6a2e697..0e25a79 100644 --- a/crates/index/src/db_queries.rs +++ b/crates/index/src/db_queries.rs @@ -361,6 +361,7 @@ const RECENT_BLOCKHASH_RETENTION_SLOTS: u64 = 900; pub async fn insert_recent_blockhash( slot: u64, blockhash: String, + block_height: Option, db: &DatabaseConnection, config: &IndexConfig, ) { @@ -373,9 +374,14 @@ pub async fn insert_recent_blockhash( let insert = Statement::from_sql_and_values( DatabaseBackend::Postgres, - "INSERT INTO recent_blockhashes (slot, blockhash) VALUES ($1, $2) \ - ON CONFLICT (slot) DO UPDATE SET blockhash = EXCLUDED.blockhash", - [Value::from(slot as i64), Value::from(blockhash)], + "INSERT INTO recent_blockhashes (slot, blockhash, block_height) VALUES ($1, $2, $3) \ + ON CONFLICT (slot) DO UPDATE SET blockhash = EXCLUDED.blockhash, \ + block_height = EXCLUDED.block_height", + [ + Value::from(slot as i64), + Value::from(blockhash), + Value::from(block_height.map(|h| h as i64)), + ], ); let prune = Statement::from_sql_and_values( DatabaseBackend::Postgres, diff --git a/crates/index/src/modules/save_block.rs b/crates/index/src/modules/save_block.rs index c29ec82..491c6fa 100644 --- a/crates/index/src/modules/save_block.rs +++ b/crates/index/src/modules/save_block.rs @@ -237,7 +237,14 @@ pub async fn save_block( ) .await; - db_queries::insert_recent_blockhash(slot, block.blockhash.clone(), db, &config).await; + db_queries::insert_recent_blockhash( + slot, + block.blockhash.clone(), + block.block_height.map(|h| h.block_height), + db, + &config, + ) + .await; let elapsed = start_time.elapsed().as_secs_f64(); metrics::record_block_processing(elapsed, "block"); diff --git a/crates/integration_tests/README.md b/crates/integration_tests/README.md index 70a7b8b..6792fb8 100644 --- a/crates/integration_tests/README.md +++ b/crates/integration_tests/README.md @@ -79,6 +79,7 @@ cargo run --bin integration_tests -- benchmark -c custom.toml gpa | `get-multiple-accounts` | `getMultipleAccounts` (up to `[server].max-multiple-accounts` pubkeys, default `100`; positional comparison) | | `get-balance` | `getBalance` (returns `u64`, no encoding) | | `get-token-account-balance` | `getTokenAccountBalance` (returns `UiTokenAmount`, no encoding, no `minContextSlot`) | +| `simulate-transaction` | `simulateTransaction` (replays real captured requests; see [Replaying simulateTransaction](#replaying-simulatetransaction)) | **Per-method response comparison** (when `[comparison]` is configured): @@ -89,6 +90,7 @@ cargo run --bin integration_tests -- benchmark -c custom.toml gpa | `get-account-info` | Single `UiAccount \| null` — direct compare with zstd-aware account-equality | | `get-balance` | `u64` — direct JSON equality | | `get-token-account-balance` | `UiTokenAmount` object — direct JSON equality | +| `simulate-transaction` | `result.value` field-by-field: `err`, `logs`, `unitsConsumed`, `loadedAccountsDataSize`, `returnData`, `fee`, `loadedAddresses`, `innerInstructions`, `accounts`, `pre`/`postBalances`; `pre`/`postTokenBalances` are `(accountIndex, mint)`-keyed so ordering is ignored. `replacementBlockhash` is compared only for presence, since each endpoint substitutes its own blockhash. | **Default encodings used by `extract_encoding_from_request` when the request omits `encoding`** (mirrors Agave's per-method defaults): @@ -164,17 +166,42 @@ Loads a static array of JSON-RPC request bodies from a file. Requests cycle inde #### `type = "victoria_logs"` -Fetches real request bodies from a VictoriaLogs instance. Refreshes automatically every 60 seconds during the benchmark run with fresh logs. - -| Field | Default | Description | -| ------------------ | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `url` | _(required)_ | VictoriaLogs query endpoint URL | -| `minutes` | _(none)_ | Time window in minutes for the log query. If omitted, no time constraint is applied | -| `limit` | `1000` | Maximum number of log entries to fetch per query | -| `min_request_size` | _(none)_ | Only include requests where the response was at least this many bytes | -| `max_request_size` | _(none)_ | Only include requests where the response was at most this many bytes | -| `encoding` | _(none)_ | Filter requests by encoding. See [VictoriaLogs encoding filter](#victorialogs-encoding-filter) below. Supports `\|` for multiple (e.g. `"base58\|jsonParsed"`) | -| `inject_context` | `false` | If `true`, when a no-context mismatch is detected, re-sends the request with `withContext: true` injected and runs slot compensation before deciding if it's a real mismatch. Eliminates false positives from slot lag on requests originally sent without context | +Fetches real request bodies from a VictoriaLogs instance. Refreshes automatically every `poll_interval_secs` (default 60) during the benchmark run with fresh logs. + +| Field | Default | Description | +| -------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `url` | _(required)_ | VictoriaLogs query endpoint URL | +| `minutes` | _(none)_ | Time window in minutes for the log query. If omitted, no time constraint is applied | +| `window_seconds` | _(none)_ | Trailing time window in seconds (`_time:>now-Ns`). Takes precedence over `minutes`. Use for request types that expire quickly | +| `limit` | `1000` | Maximum number of log entries to fetch per query | +| `min_request_size` | _(none)_ | Only include requests where the response was at least this many bytes | +| `max_request_size` | _(none)_ | Only include requests where the response was at most this many bytes | +| `encoding` | _(none)_ | Filter requests by encoding. See [VictoriaLogs encoding filter](#victorialogs-encoding-filter) below. Supports `\|` for multiple (e.g. `"base58\|jsonParsed"`) | +| `inject_context` | `false` | If `true`, when a no-context mismatch is detected, re-sends the request with `withContext: true` injected and runs slot compensation before deciding if it's a real mismatch. Eliminates false positives from slot lag on requests originally sent without context | +| `poll_interval_secs` | `60` | How often the background fetcher re-queries VictoriaLogs | +| `replay_once` | `false` | Deduplicate by the log's `req_id` and send each request exactly once instead of cycling the pool. Required for requests that expire | + +##### Replaying simulateTransaction + +A `simulateTransaction` request carries a `recentBlockhash` that the cluster only accepts for +~150 blocks (~60s). VictoriaLogs ingestion lag is 1–9s, so a request must be replayed within +seconds of being logged. Three settings make that work, and all three are required: + +- `window_seconds = 30` — a short trailing window instead of whole minutes. +- `poll_interval_secs = 5` — fetch new arrivals promptly. +- `replay_once = true` — send each `req_id` once. Without it the spawner cycles the pool at + `target_rps`, replaying the same transactions until their blockhashes expire. + +Pair with `[benchmark].start_on_first_request = true` so the `duration_secs` countdown begins +when the first request is picked up rather than while waiting for the first poll. + +Two known, non-bug divergence classes when comparing against an Agave validator: + +- **`AlreadyProcessed` on the Agave side.** Clients typically submit the transaction a second + after simulating it, so by replay time it has landed and Agave's signature status cache + rejects it. Cloudbreak has no status cache and re-executes. Only affects requests that do + *not* set `replaceRecentBlockhash: true` (replacing the blockhash changes the status-cache key). +- **`BlockhashNotFound` on one side only** when a request's original blockhash expires mid-run. ##### VictoriaLogs tips diff --git a/crates/integration_tests/src/benchmark.rs b/crates/integration_tests/src/benchmark.rs index 9e941fe..1f38c3d 100644 --- a/crates/integration_tests/src/benchmark.rs +++ b/crates/integration_tests/src/benchmark.rs @@ -15,7 +15,7 @@ use anyhow::Result; use clap::ValueEnum; use serde_json::Value as JsonValue; use std::{ - collections::HashMap, + collections::{HashMap, VecDeque}, sync::Arc, time::{Duration, Instant, SystemTime}, }; @@ -32,6 +32,7 @@ pub enum RequestType { GetMultipleAccounts, GetBalance, GetTokenAccountBalance, + SimulateTransaction, } pub async fn run(args: &BenchmarkArgs) -> Result<()> { @@ -60,10 +61,12 @@ pub async fn run(args: &BenchmarkArgs) -> Result<()> { max_in_flight, duration_secs, timeout_secs, + start_on_first_request, } = benchmark; let retry_with_context = source.retry_with_context(); - let requests_rx = sources::load_requests_from_source(&source, request_type).await?; + let replay_once = source.replay_once(); + let mut requests_rx = sources::load_requests_from_source(&source, request_type).await?; let client = reqwest::Client::builder() // .pool_max_idle_per_host(0) @@ -104,15 +107,33 @@ pub async fn run(args: &BenchmarkArgs) -> Result<()> { // Spawner loop (runs for `duration` seconds) tokio::spawn(async move { - let deadline = Instant::now() + Duration::from_secs(duration_secs); + let mut deadline = (!start_on_first_request) + .then(|| Instant::now() + Duration::from_secs(duration_secs)); let mut idx: usize = 0; + let mut pending: VecDeque = VecDeque::new(); + let mut drained_initial = false; // Will make the requests loop infinitely until the deadline is reached - while Instant::now() < deadline { + loop { + if let Some(deadline) = deadline + && Instant::now() >= deadline + { + break; + } ticker.tick().await; let permit = semaphore.clone().try_acquire_owned(); - let request = { + let request = if replay_once { + if pending.is_empty() && (!drained_initial || requests_rx.has_changed().unwrap_or(false)) + { + drained_initial = true; + pending.extend(requests_rx.borrow_and_update().iter().cloned()); + } + match pending.pop_front() { + Some(req) => req, + None => continue, + } + } else { let requests = requests_rx.borrow(); if requests.is_empty() { continue; @@ -123,6 +144,14 @@ pub async fn run(args: &BenchmarkArgs) -> Result<()> { req }; + if deadline.is_none() { + tracing::info!( + target: "bench_source", + "First request picked up; starting {}s countdown", duration_secs + ); + deadline = Some(Instant::now() + Duration::from_secs(duration_secs)); + } + match permit { Ok(permit) => { let tx = tx.clone(); diff --git a/crates/integration_tests/src/config.rs b/crates/integration_tests/src/config.rs index bda1321..3606076 100644 --- a/crates/integration_tests/src/config.rs +++ b/crates/integration_tests/src/config.rs @@ -62,6 +62,9 @@ pub struct BenchmarkConfig { #[serde(default = "defaults::timeout_secs")] pub timeout_secs: u64, + + #[serde(default)] + pub start_on_first_request: bool, } #[derive(Deserialize, Debug)] @@ -75,6 +78,7 @@ pub enum SourceConfig { url: String, /// Time window in minutes. If omitted, no time constraint is applied. minutes: Option, + window_seconds: Option, #[serde(default = "defaults::vlogs_limit")] limit: u32, min_request_size: Option, @@ -85,6 +89,10 @@ pub enum SourceConfig { /// deciding whether it's a real mismatch. #[serde(default)] inject_context: bool, + #[serde(default = "defaults::poll_interval_secs")] + poll_interval_secs: u64, + #[serde(default)] + replay_once: bool, }, #[serde(rename = "mismatch_dir")] @@ -108,6 +116,13 @@ impl SourceConfig { _ => false, } } + + pub fn replay_once(&self) -> bool { + match self { + SourceConfig::VictoriaLogs { replay_once, .. } => *replay_once, + _ => false, + } + } } #[derive(Deserialize, Debug, Clone)] @@ -171,6 +186,9 @@ mod defaults { pub fn vlogs_limit() -> u32 { 1000 } + pub fn poll_interval_secs() -> u64 { + 60 + } pub fn compare_ratio() -> f64 { 1.0 } diff --git a/crates/integration_tests/src/response_comparison.rs b/crates/integration_tests/src/response_comparison.rs index aaced0e..b7b1c56 100644 --- a/crates/integration_tests/src/response_comparison.rs +++ b/crates/integration_tests/src/response_comparison.rs @@ -181,6 +181,7 @@ pub fn compare_responses( RequestType::GetBalance | RequestType::GetTokenAccountBalance => { compare_value_direct(response1, response2) } + RequestType::SimulateTransaction => compare_simulate_responses(response1, response2), }; CompareResponsesResult::new_with_matching_context(matches, compare_context_result) @@ -194,6 +195,82 @@ fn compare_value_direct(response1: &JsonValue, response2: &JsonValue) -> bool { v1 == v2 } +static NULL_VALUE: JsonValue = JsonValue::Null; + +fn field<'a>(value: &'a JsonValue, key: &str) -> &'a JsonValue { + value.get(key).unwrap_or(&NULL_VALUE) +} + +const SIMULATE_EXACT_FIELDS: [&str; 9] = [ + "err", + "logs", + "unitsConsumed", + "loadedAccountsDataSize", + "returnData", + "fee", + "loadedAddresses", + "innerInstructions", + "accounts", +]; + +fn token_balance_map(balances: &JsonValue) -> Option> { + let array = balances.as_array()?; + Some( + array + .iter() + .map(|b| { + let key = format!( + "{}:{}", + field(b, "accountIndex"), + field(b, "mint"), + ); + (key, b) + }) + .collect(), + ) +} + +fn compare_token_balances(value1: &JsonValue, value2: &JsonValue, key: &str) -> bool { + let b1 = field(value1, key); + let b2 = field(value2, key); + + match (token_balance_map(b1), token_balance_map(b2)) { + (Some(m1), Some(m2)) => m1 == m2, + (None, None) => b1 == b2, + _ => false, + } +} + +fn compare_simulate_responses(response1: &JsonValue, response2: &JsonValue) -> bool { + let v1 = response1.get("result").and_then(|r| r.get("value")); + let v2 = response2.get("result").and_then(|r| r.get("value")); + + let (v1, v2) = match (v1, v2) { + (Some(v1), Some(v2)) => (v1, v2), + _ => return v1 == v2, + }; + + if SIMULATE_EXACT_FIELDS + .iter() + .any(|key| field(v1, key) != field(v2, key)) + { + return false; + } + + if field(v1, "replacementBlockhash").is_null() != field(v2, "replacementBlockhash").is_null() { + return false; + } + + if field(v1, "preBalances") != field(v2, "preBalances") + || field(v1, "postBalances") != field(v2, "postBalances") + { + return false; + } + + compare_token_balances(v1, v2, "preTokenBalances") + && compare_token_balances(v1, v2, "postTokenBalances") +} + /// Compares two `result.value: UiAccount | null` responses (`getAccountInfo`). /// Handles zstd-encoded data so non-deterministic compression isn't flagged. fn compare_value_with_account( diff --git a/crates/integration_tests/src/sources/mod.rs b/crates/integration_tests/src/sources/mod.rs index 793e346..b304ad3 100644 --- a/crates/integration_tests/src/sources/mod.rs +++ b/crates/integration_tests/src/sources/mod.rs @@ -7,6 +7,7 @@ pub mod json_file; pub mod mismatch_files; pub mod victoria_logs; +use std::collections::HashSet; use std::time::Duration; use crate::{benchmark::RequestType, config::SourceConfig}; @@ -14,6 +15,24 @@ use anyhow::Result; use serde_json::Value as JsonValue; use tokio::sync::watch; +fn retain_unseen( + requests: Vec, + seen: &mut HashSet, + replay_once: bool, +) -> Vec { + if !replay_once { + return requests.into_iter().map(|r| r.body).collect(); + } + requests + .into_iter() + .filter(|r| match &r.req_id { + Some(id) => seen.insert(id.clone()), + None => true, + }) + .map(|r| r.body) + .collect() +} + /// Loads the requests from the source and returns a watch::Receiver that will be updated with new requests depending on the source type pub async fn load_requests_from_source( source: &SourceConfig, @@ -28,11 +47,14 @@ pub async fn load_requests_from_source( SourceConfig::VictoriaLogs { url, minutes, + window_seconds, limit, min_request_size, max_request_size, encoding, inject_context: _, + poll_interval_secs, + replay_once, } => { let client = reqwest::Client::builder() .pool_max_idle_per_host(0) @@ -43,6 +65,9 @@ pub async fn load_requests_from_source( "Fetching initial requests from VictoriaLogs" ); + let replay_once = *replay_once; + let mut seen: HashSet = HashSet::new(); + let initial_requests = victoria_logs::get_requests( &client, url, @@ -51,9 +76,11 @@ pub async fn load_requests_from_source( *max_request_size, encoding.as_deref(), *minutes, + *window_seconds, *limit, ) .await?; + let initial_requests = retain_unseen(initial_requests, &mut seen, replay_once); tracing::info!( target: "bench_source", @@ -63,17 +90,18 @@ pub async fn load_requests_from_source( let (tx, rx) = watch::channel(initial_requests); - // Background fetcher — refreshes every minute let url = url.clone(); let min_request_size = *min_request_size; let max_request_size = *max_request_size; let encoding = encoding.clone(); let minutes = *minutes; + let window_seconds = *window_seconds; let limit = *limit; + let poll_interval_secs = *poll_interval_secs; tokio::spawn(async move { loop { - tokio::time::sleep(Duration::from_secs(60)).await; + tokio::time::sleep(Duration::from_secs(poll_interval_secs)).await; match victoria_logs::get_requests( &client, &url, @@ -82,11 +110,17 @@ pub async fn load_requests_from_source( max_request_size, encoding.as_deref(), minutes, + window_seconds, limit, ) .await { Ok(new_requests) => { + let new_requests = + retain_unseen(new_requests, &mut seen, replay_once); + if replay_once && new_requests.is_empty() { + continue; + } tracing::info!( target: "bench_source", "Refreshed {} requests from VictoriaLogs", diff --git a/crates/integration_tests/src/sources/victoria_logs.rs b/crates/integration_tests/src/sources/victoria_logs.rs index 88a5448..f60c23b 100644 --- a/crates/integration_tests/src/sources/victoria_logs.rs +++ b/crates/integration_tests/src/sources/victoria_logs.rs @@ -8,21 +8,31 @@ use serde_json::Value as JsonValue; use crate::benchmark::RequestType; use crate::utils; +pub struct LoggedRequest { + pub req_id: Option, + pub body: JsonValue, +} + +#[allow(clippy::too_many_arguments)] pub fn get_body_query( request_type: RequestType, min_filter: &str, max_filter: &str, encoding: Option<&str>, minutes: Option, + window_seconds: Option, limit: u32, ) -> String { let encoding_filter = encoding .map(|e| format!(r#" AND body:~`"encoding":\s*"{}"`"#, e)) .unwrap_or_default(); - let time_filter = minutes - .map(|m| format!("_time:>now-{m}m AND _time:<=now-{}m AND ", m - 1)) - .unwrap_or_default(); + let time_filter = match window_seconds { + Some(s) => format!("_time:>now-{s}s AND "), + None => minutes + .map(|m| format!("_time:>now-{m}m AND _time:<=now-{}m AND ", m - 1)) + .unwrap_or_default(), + }; match request_type { RequestType::Gtabo => format!( @@ -52,6 +62,9 @@ pub fn get_body_query( RequestType::GetTokenAccountBalance => format!( "query={time_filter}rpc_call:=\"getTokenAccountBalance\" AND pool_dedicated:~\"liquid\" | limit {limit}" ), + RequestType::SimulateTransaction => format!( + "query={time_filter}rpc_call:=\"simulateTransaction\" AND pool_dedicated:~\"foundation\" AND body:* | limit {limit}" + ), } } @@ -64,8 +77,9 @@ pub async fn get_requests( max_request_size: Option, encoding: Option<&str>, minutes: Option, + window_seconds: Option, limit: u32, -) -> Result, anyhow::Error> { +) -> Result, anyhow::Error> { let min_filter = if let Some(min_request_size) = min_request_size { format!(" AND bytes:>{}", min_request_size) } else { @@ -84,6 +98,7 @@ pub async fn get_requests( &max_filter, encoding, minutes, + window_seconds, limit, ); @@ -97,7 +112,7 @@ pub async fn get_requests( let data = response.text().await?; // Parse NDJSON (Newline Delimited JSON) - let logs: Vec = data + let logs: Vec = data .trim() .lines() .filter(|line| !line.trim().is_empty()) @@ -114,6 +129,7 @@ pub async fn get_requests( return None; } + let req_id = log["req_id"].as_str().map(String::from); let mut body: JsonValue = serde_json::from_str(log["body"].as_str().unwrap()).ok()?; if request_type == RequestType::GpaTokenMint @@ -161,7 +177,7 @@ pub async fn get_requests( } } - Some(body) + Some(LoggedRequest { req_id, body }) }) .collect(); diff --git a/crates/integration_tests/src/utils.rs b/crates/integration_tests/src/utils.rs index 848c691..433d006 100644 --- a/crates/integration_tests/src/utils.rs +++ b/crates/integration_tests/src/utils.rs @@ -97,7 +97,8 @@ pub fn extract_commitment_from_request(request: &JsonValue, request_type: Reques | RequestType::GetAccountInfo | RequestType::GetMultipleAccounts | RequestType::GetBalance - | RequestType::GetTokenAccountBalance => 1, + | RequestType::GetTokenAccountBalance + | RequestType::SimulateTransaction => 1, RequestType::Gtabo | RequestType::Gtabd => 2, }; request @@ -138,7 +139,8 @@ pub fn extract_encoding_from_request(request: &JsonValue, request_type: RequestT | RequestType::GpaTokenOwner | RequestType::GpaTokenMint | RequestType::GetAccountInfo - | RequestType::GetMultipleAccounts => 1, + | RequestType::GetMultipleAccounts + | RequestType::SimulateTransaction => 1, RequestType::Gtabo | RequestType::Gtabd => 2, RequestType::GetBalance | RequestType::GetTokenAccountBalance => unreachable!(), }; @@ -158,6 +160,7 @@ pub fn extract_encoding_from_request(request: &JsonValue, request_type: RequestT RequestType::Gtabo | RequestType::Gtabd => "jsonParsed".to_string(), RequestType::GetAccountInfo => "binary".to_string(), RequestType::GetMultipleAccounts => "base64".to_string(), + RequestType::SimulateTransaction => "base58".to_string(), RequestType::GetBalance | RequestType::GetTokenAccountBalance => unreachable!(), }, } diff --git a/crates/migration/src/lib.rs b/crates/migration/src/lib.rs index 09cbfd9..cb51c3b 100644 --- a/crates/migration/src/lib.rs +++ b/crates/migration/src/lib.rs @@ -19,6 +19,7 @@ mod m20260414_000000_create_indexer_filters_table; mod m20260522_000000_create_environment_info_table; mod m20260528_000000_create_epoch_stakes_table; mod m20260703_000000_create_recent_blockhashes_table; +mod m20260709_000000_add_block_height_to_recent_blockhashes; pub struct Migrator; @@ -38,6 +39,7 @@ impl MigratorTrait for Migrator { Box::new(m20260522_000000_create_environment_info_table::Migration), Box::new(m20260528_000000_create_epoch_stakes_table::Migration), Box::new(m20260703_000000_create_recent_blockhashes_table::Migration), + Box::new(m20260709_000000_add_block_height_to_recent_blockhashes::Migration), ] } } diff --git a/crates/migration/src/m20260709_000000_add_block_height_to_recent_blockhashes.rs b/crates/migration/src/m20260709_000000_add_block_height_to_recent_blockhashes.rs new file mode 100644 index 0000000..669b7b9 --- /dev/null +++ b/crates/migration/src/m20260709_000000_add_block_height_to_recent_blockhashes.rs @@ -0,0 +1,29 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .get_connection() + .execute_unprepared( + "ALTER TABLE recent_blockhashes ADD COLUMN IF NOT EXISTS block_height BIGINT;", + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .get_connection() + .execute_unprepared( + "ALTER TABLE recent_blockhashes DROP COLUMN IF EXISTS block_height;", + ) + .await?; + + Ok(()) + } +} From a14454af651f94e6c5ea10c8dbb6982648f8b2b6 Mon Sep 17 00:00:00 2001 From: ArthurPaivaT Date: Wed, 15 Jul 2026 11:52:57 +0000 Subject: [PATCH 05/13] Cleanup and document simulateTransaction changes --- README.md | 10 +++++++++- crates/api/src/methods/simulate_transaction.rs | 9 +++++---- crates/index/src/db_queries.rs | 4 ++++ crates/integration_tests/README.md | 3 ++- example.cloudbreak.integration_tests.toml | 4 ++++ 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index dadc372..2d474ff 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ The API server exposes the following JSON-RPC methods: | `getVersion` | Returns the cluster version, Agave-compatible (`{"solana-core": ""}`). See note below for the composite-string format Cloudbreak uses. | | `getGenesisHash` | Returns the cluster genesis hash as a base58 string. | | `getVoteAccounts` | Returns the cluster's `current` and `delinquent` vote accounts with per-voter activated stake, commission, last vote, and recent epoch credits. Optional; only available when the Vote and Stake programs are indexed. See [Vote Accounts](#vote-accounts-getvoteaccounts). | +| `simulateTransaction` | Simulates a transaction against indexed account state at the requested slot and returns logs, compute units, return data, requested account state, and balance changes. Supports `sigVerify`, `replaceRecentBlockhash`, `accounts`, `innerInstructions`, and `minContextSlot`. Optional; only available on a full unfiltered index. See [Simulate Transaction](#simulate-transaction-simulatetransaction). | Only **confirmed** and **finalized** commitment levels are fully supported. By default, requests with `processed` commitment return an error. This can be overridden via the `processed-commitment` configuration option (see [API Configuration](#api-server-cloudbreakapitoml)). @@ -633,6 +634,12 @@ A snapshot captures stake at a single point in time, but activated stake drifts The recomputer detects drift by comparing the total activated stake across runs. It recomputes every 60 s and treats an epoch as converged once the total is unchanged for three consecutive polls and the indexed `EpochRewards` sysvar reports that reward distribution has finished. If that sysvar is not indexed, it converges on stability alone after roughly 30 minutes. After converging it keeps recomputing on a slower 600 s heartbeat, returning to the 60 s cadence if a late reward write or a healed ingestion gap moves the total again. +### Simulate Transaction (`simulateTransaction`) + +`simulateTransaction` is optional and only served on a **full, unfiltered index** (empty `[programs]` include and exclude lists). Simulation must be able to load any account a transaction touches program, lookup-table, sysvar, and feature-gate accounts included so a filtered index cannot serve it. Cloudbreak checks this at startup, if the index is filtered, the method returns a `simulateTransaction is not supported on this node` error. + +The transaction is executed read-only against the indexed account state at the requested slot, nothing is committed. Cloudbreak reconstructs the cluster's actually-activated feature set at that slot from the on-chain feature accounts (rather than enabling all features), so compute-unit accounting and execution behaviour match mainnet. `replaceRecentBlockhash` substitutes the latest recorded blockhash before execution and reports it with its `lastValidBlockHeight`, `sigVerify` verifies signatures, `accounts` returns post-simulation state for the requested addresses, `innerInstructions` includes decoded inner instructions. + ### Snapshot on Indexer Startup When the `[snapshot]` section (with `[snapshot.tracker_endpoint]`) is present in the indexer config, the indexer queries the cluster tracker, downloads the latest covering snapshot pair from the source the tracker reports, and processes the archives before beginning gRPC streaming. This provides fast bootstrapping of account state. Omit the entire `[snapshot]` section to skip this. @@ -800,13 +807,14 @@ cp example.cloudbreak.integration_tests.toml cloudbreak.integration_tests.toml cargo run --bin integration_tests -- benchmark gpa cargo run --bin integration_tests -- benchmark gtabo cargo run --bin integration_tests -- benchmark gtabd +cargo run --bin integration_tests -- benchmark simulate-transaction ``` ### Commands | Command | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | -| `benchmark ` | Main command. Load test with optional dual-endpoint comparison. Types: `gpa`, `gtabo`, `gtabd`, `gpa-token-owner`, `gpa-token-mint`. | +| `benchmark ` | Main command. Load test with optional dual-endpoint comparison. Types: `gpa`, `gtabo`, `gtabd`, `gpa-token-owner`, `gpa-token-mint`, `simulate-transaction`. | | `compare` | (Legacy) Full pubkey set comparison between two endpoints with transaction history checks. | | `get-slot` | (Legacy) Polls `getSlot` on rpc1 every 100ms. | diff --git a/crates/api/src/methods/simulate_transaction.rs b/crates/api/src/methods/simulate_transaction.rs index 4712831..7ceead8 100644 --- a/crates/api/src/methods/simulate_transaction.rs +++ b/crates/api/src/methods/simulate_transaction.rs @@ -929,7 +929,7 @@ fn sysvar_account_ids() -> [Pubkey; 9] { fn programdata_addresses(accounts: &HashMap) -> Vec { let loader = solana_sdk_ids::bpf_loader_upgradeable::id(); let mut out = Vec::new(); - for (_key, (account, _)) in accounts { + for (account, _) in accounts.values() { if account.owner() != &loader { continue; } @@ -996,9 +996,10 @@ fn execute( } // Install v1; v2 stays the default empty loader (loader-v4 compiles via v1). - let mut environments = ProgramRuntimeEnvironments::default(); - environments.program_runtime_v1 = Arc::new(program_runtime_v1); - processor.set_environments(environments); + processor.set_environments(ProgramRuntimeEnvironments { + program_runtime_v1: Arc::new(program_runtime_v1), + ..Default::default() + }); for builtin in solana_builtins::BUILTINS { processor.add_builtin( diff --git a/crates/index/src/db_queries.rs b/crates/index/src/db_queries.rs index 0e25a79..b438b2a 100644 --- a/crates/index/src/db_queries.rs +++ b/crates/index/src/db_queries.rs @@ -366,6 +366,10 @@ pub async fn insert_recent_blockhash( config: &IndexConfig, ) { if blockhash.is_empty() { + tracing::warn!( + "insert_recent_blockhash: empty blockhash for slot {}. Expected only for snapshot-repaired self-healing blocks", + slot + ); return; } diff --git a/crates/integration_tests/README.md b/crates/integration_tests/README.md index 6792fb8..70812a1 100644 --- a/crates/integration_tests/README.md +++ b/crates/integration_tests/README.md @@ -56,6 +56,7 @@ cargo run --bin integration_tests -- benchmark get-account-info cargo run --bin integration_tests -- benchmark get-multiple-accounts cargo run --bin integration_tests -- benchmark get-balance cargo run --bin integration_tests -- benchmark get-token-account-balance +cargo run --bin integration_tests -- benchmark simulate-transaction cargo run --bin integration_tests -- benchmark -c custom.toml gpa ``` @@ -63,7 +64,7 @@ cargo run --bin integration_tests -- benchmark -c custom.toml gpa | Argument | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `` | Required. One of: `gpa`, `gtabo`, `gtabd`, `gpa-token-owner`, `gpa-token-mint`, `get-account-info`, `get-multiple-accounts`, `get-balance`, `get-token-account-balance` | +| `` | Required. One of: `gpa`, `gtabo`, `gtabd`, `gpa-token-owner`, `gpa-token-mint`, `get-account-info`, `get-multiple-accounts`, `get-balance`, `get-token-account-balance`, `simulate-transaction` | | `-c, --config ` | Path to TOML config file (default: `cloudbreak.integration_tests.toml`) | **Request types:** diff --git a/example.cloudbreak.integration_tests.toml b/example.cloudbreak.integration_tests.toml index d37e932..68d64e7 100644 --- a/example.cloudbreak.integration_tests.toml +++ b/example.cloudbreak.integration_tests.toml @@ -11,6 +11,7 @@ target_rps = 5.0 max_in_flight = 100 duration_secs = 100 timeout_secs = 60 +# start_on_first_request = true # Start the countdown when the first request is caught (use for simulateTransaction) [source] type = "json_file" @@ -28,6 +29,9 @@ path = "crates/integration_tests/gpa_benchmark_requests.json" # # "base64" | "base58" | "base64+zstd" | "jsonParsed" # encoding = "jsonParsed" # inject_context = true # On no-context mismatch, re-sends with withContext:true and slot compensation before deciding +# window_seconds = 30 # Trailing window in seconds; takes precedence over `minutes` (use for simulateTransaction: keeps replayed blockhashes fresh) +# poll_interval_secs = 5 # How often the background fetcher re-queries VictoriaLogs +# replay_once = true # Send each req_id exactly once instead of cycling the pool (recommended for simulateTransaction) # Example of mismatch_dir source (re-run previously mismatched requests) # [source] From f23dce0222006f3de7c2796d5d35d858075c74ac Mon Sep 17 00:00:00 2001 From: ArthurPaivaT Date: Mon, 20 Jul 2026 00:47:56 +0000 Subject: [PATCH 06/13] Implement getSupply rpc method --- README.md | 7 + crates/api/src/http/mod.rs | 7 + crates/api/src/http/rpc.rs | 8 +- crates/api/src/lib.rs | 31 +- crates/api/src/methods/get_supply.rs | 104 ++++++ crates/api/src/methods/mod.rs | 1 + crates/api/src/modules/mod.rs | 1 + crates/api/src/modules/supply_cache.rs | 111 ++++++ crates/command/src/main.rs | 4 +- crates/core/src/config.rs | 3 + crates/core/src/modules/mod.rs | 1 + crates/core/src/modules/supply_tracker.rs | 205 ++++++++++ crates/index/src/db_queries.rs | 75 +++- crates/index/src/indexer.rs | 24 +- crates/index/src/metrics.rs | 18 + crates/index/src/modules/epoch_stakes.rs | 4 +- crates/index/src/modules/mod.rs | 2 + crates/index/src/modules/non_circulating.rs | 158 ++++++++ .../src/modules/non_circulating_lists.rs | 349 ++++++++++++++++++ crates/index/src/modules/save_block.rs | 11 + crates/index/src/modules/self_healing.rs | 18 +- crates/index/src/modules/snapshot.rs | 5 +- crates/integration_tests/src/benchmark.rs | 1 + .../src/response_comparison.rs | 32 +- .../src/sources/victoria_logs.rs | 3 + crates/integration_tests/src/utils.rs | 11 +- crates/migration/src/lib.rs | 2 + .../m20260717_000000_create_supply_tables.rs | 43 +++ crates/snapshot/src/db_queries.rs | 54 ++- crates/snapshot/src/lib.rs | 49 ++- crates/snapshot/src/sidecar.rs | 12 + example.cloudbreak.index.toml | 1 + 32 files changed, 1336 insertions(+), 19 deletions(-) create mode 100644 crates/api/src/methods/get_supply.rs create mode 100644 crates/api/src/modules/supply_cache.rs create mode 100644 crates/core/src/modules/supply_tracker.rs create mode 100644 crates/index/src/modules/non_circulating.rs create mode 100644 crates/index/src/modules/non_circulating_lists.rs create mode 100644 crates/migration/src/m20260717_000000_create_supply_tables.rs diff --git a/README.md b/README.md index 2d474ff..2b0a808 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ The API server exposes the following JSON-RPC methods: | `getGenesisHash` | Returns the cluster genesis hash as a base58 string. | | `getVoteAccounts` | Returns the cluster's `current` and `delinquent` vote accounts with per-voter activated stake, commission, last vote, and recent epoch credits. Optional; only available when the Vote and Stake programs are indexed. See [Vote Accounts](#vote-accounts-getvoteaccounts). | | `simulateTransaction` | Simulates a transaction against indexed account state at the requested slot and returns logs, compute units, return data, requested account state, and balance changes. Supports `sigVerify`, `replaceRecentBlockhash`, `accounts`, `innerInstructions`, and `minContextSlot`. Optional; only available on a full unfiltered index. See [Simulate Transaction](#simulate-transaction-simulatetransaction). | +| `getSupply` | Returns the total and circulating supply in lamports plus the non-circulating account list. Optional; only available on a full unfiltered index with `supply-tracker-enabled = true` on the indexer. See [Supply](#supply-getsupply). | Only **confirmed** and **finalized** commitment levels are fully supported. By default, requests with `processed` commitment return an error. This can be overridden via the `processed-commitment` configuration option (see [API Configuration](#api-server-cloudbreakapitoml)). @@ -640,6 +641,12 @@ The recomputer detects drift by comparing the total activated stake across runs. The transaction is executed read-only against the indexed account state at the requested slot, nothing is committed. Cloudbreak reconstructs the cluster's actually-activated feature set at that slot from the on-chain feature accounts (rather than enabling all features), so compute-unit accounting and execution behaviour match mainnet. `replaceRecentBlockhash` substitutes the latest recorded blockhash before execution and reports it with its `lastValidBlockHeight`, `sigVerify` verifies signatures, `accounts` returns post-simulation state for the requested addresses, `innerInstructions` includes decoded inner instructions. +### Supply (`getSupply`) + +`getSupply` is optional and only served on a **full, unfiltered index** (empty `[programs]` include and exclude lists), with `supply-tracker-enabled = true` set on the indexer. The indexer anchors the total supply to the snapshot bank's `capitalization` on every (re)start, keeps an in-memory pubkey → lamports map seeded during the same snapshot ingest pass, and advances the total per confirmed block by summing per-account lamport deltas from the block's account updates. One `supply` row carrying the total and the non-circulating lamports is persisted per confirmed block (pruned to the last 128 slots), and a background recomputer derives the non-circulating set from Agave's pinned non-circulating account and withdraw-authority lists plus locked stake accounts, refreshing on the same converge-then-heartbeat cadence as the epoch-stakes recomputer. + +The API serves from a polled cache. Requests return a node-unhealthy error until the bootstrap pass has completed and the non-circulating set has been computed. On a detected slot gap the tracker marks itself stale (requests fail as node-unhealthy) and re-anchors from the in-memory map once the gap is repaired. For `finalized` commitment the response picks the newest persisted total at or below the finalized slot; `confirmed` returns the newest total. `context.slot` is the slot of the served row; the non-circulating account list may lag it by up to the recomputer heartbeat. + ### Snapshot on Indexer Startup When the `[snapshot]` section (with `[snapshot.tracker_endpoint]`) is present in the indexer config, the indexer queries the cluster tracker, downloads the latest covering snapshot pair from the source the tracker reports, and processes the archives before beginning gRPC streaming. This provides fast bootstrapping of account state. Omit the entire `[snapshot]` section to skip this. diff --git a/crates/api/src/http/mod.rs b/crates/api/src/http/mod.rs index b299042..301c0ca 100644 --- a/crates/api/src/http/mod.rs +++ b/crates/api/src/http/mod.rs @@ -6,6 +6,7 @@ use crate::http::server::HttpHandlerResponse; use crate::http::server::ResponseBody; use crate::modules::cache::GpaProcessor; +use crate::modules::supply_cache::SharedSupplySnapshot; use crate::modules::vote_accounts_cache::SharedStakesSnapshot; use crate::query_tracker_client::QueryTrackerClient; use crate::slot_syncronizer::SlotSyncronizerData; @@ -46,6 +47,8 @@ pub struct CloudbreakRpcState { pub stakes_cache: SharedStakesSnapshot, pub max_multiple_accounts: usize, pub simulation_supported: bool, + pub supply_supported: bool, + pub supply_cache: SharedSupplySnapshot, pub feature_set_cache: Arc>>, } @@ -67,6 +70,8 @@ impl CloudbreakRpcState { stakes_cache: SharedStakesSnapshot, max_multiple_accounts: usize, simulation_supported: bool, + supply_supported: bool, + supply_cache: SharedSupplySnapshot, ) -> Self { Self { database, @@ -84,6 +89,8 @@ impl CloudbreakRpcState { stakes_cache, max_multiple_accounts, simulation_supported, + supply_supported, + supply_cache, feature_set_cache: Arc::new(RwLock::new(None)), } } diff --git a/crates/api/src/http/rpc.rs b/crates/api/src/http/rpc.rs index a9ac9b6..14b1835 100644 --- a/crates/api/src/http/rpc.rs +++ b/crates/api/src/http/rpc.rs @@ -11,7 +11,7 @@ use serde::Serialize; use solana_commitment_config::CommitmentConfig; use cloudbreak_core::modules::rpc_filter_type::RpcProgramAccountsConfig; use solana_rpc_client_api::config::{ - RpcAccountInfoConfig, RpcContextConfig, RpcSimulateTransactionConfig, + RpcAccountInfoConfig, RpcContextConfig, RpcSimulateTransactionConfig, RpcSupplyConfig, }; use std::convert::Infallible; use std::sync::Arc; @@ -173,6 +173,12 @@ async fn process_single_request( .await; json_serialize_response(id, result).await } + "getSupply" => { + let config: Option = + extract_param(&rpc_request.params, 0).ok().flatten(); + let result = methods::get_supply::get_supply(state, config).await; + json_serialize_response(id, result).await + } "getAccountInfo" => { let start_time = Instant::now(); diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index c343724..74100a0 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -12,7 +12,7 @@ use cloudbreak_core::{ApiConfig, EnvironmentInfo, TryLoadConfig}; use crate::{ http::CloudbreakRpcState, metrics::setup_metrics, - modules::{cache::GpaProcessor, vote_accounts_cache}, + modules::{cache::GpaProcessor, supply_cache, vote_accounts_cache}, query_tracker_client::QueryTrackerClient, }; use std::sync::RwLock; @@ -121,6 +121,33 @@ pub async fn run(config: &str) -> cloudbreak_core::Result<()> { ); } + let supply_supported = indexer_filter.supports_simulation(); + let supply_cache: supply_cache::SharedSupplySnapshot = Arc::default(); + if supply_supported { + match supply_cache::load_latest_supply(&database).await { + Ok(Some(snapshot)) => { + info!( + "Loaded initial supply snapshot ({} rows)", + snapshot.rows.len() + ); + *supply_cache.write().unwrap() = Arc::new(snapshot); + } + Ok(None) => { + tracing::warn!( + "supply table is empty at startup; getSupply will fail until the \ + indexer processes a snapshot" + ); + } + Err(e) => { + tracing::error!("Failed to load initial supply snapshot: {:?}", e); + } + } + supply_cache::spawn_poll_task(database.clone(), supply_cache.clone()); + info!("getSupply: supported=true (full unfiltered index)"); + } else { + info!("getSupply: supported=false (indexer filter is not a full unfiltered index)"); + } + let state = CloudbreakRpcState::new( database, queries_timeout, @@ -137,6 +164,8 @@ pub async fn run(config: &str) -> cloudbreak_core::Result<()> { stakes_cache, max_multiple_accounts, simulation_supported, + supply_supported, + supply_cache, ); info!("Server is starting..."); diff --git a/crates/api/src/methods/get_supply.rs b/crates/api/src/methods/get_supply.rs new file mode 100644 index 0000000..cfc4168 --- /dev/null +++ b/crates/api/src/methods/get_supply.rs @@ -0,0 +1,104 @@ +use solana_commitment_config::CommitmentLevel; +use solana_rpc_client_api::{ + config::RpcSupplyConfig, + response::{Response as RpcResponse, RpcResponseContext, RpcSupply}, +}; + +use crate::{error::RpcError, http::CloudbreakRpcState, methods::resolve_commitment}; + +const MAX_SUPPLY_STALENESS_SLOTS: u64 = 150; + +pub async fn get_supply( + state: &CloudbreakRpcState, + config: Option, +) -> Result, RpcError> { + if !state.supply_supported { + return Err(RpcError::InvalidParamsWithMessage( + "getSupply is not supported on this node".to_string(), + )); + } + + let config = config.unwrap_or_default(); + let commitment = resolve_commitment( + config + .commitment + .map(|c| c.commitment) + .unwrap_or(CommitmentLevel::Finalized), + state.processed_commitment, + )?; + + let snapshot = state.supply_cache.read().unwrap().clone(); + let Some(accounts_list) = snapshot.non_circulating_accounts.as_ref() else { + tracing::warn!( + target: "get_supply", + "non-circulating accounts not computed yet; returning node unhealthy" + ); + return Err(RpcError::NodeUnhealthy); + }; + + let finalized_slot = match state + .slot_syncronizer_data + .as_ref() + .map(|d| d.read().unwrap().finalized_slot.slot) + { + Some(finalized) if finalized > 0 => finalized, + _ => crate::db_query::get_slot_data(&state.database) + .await + .map(|d| d.finalized_slot.slot) + .unwrap_or(0), + }; + + let row = match commitment { + CommitmentLevel::Finalized => snapshot + .rows + .iter() + .rev() + .find(|row| row.slot <= finalized_slot), + _ => snapshot.rows.last(), + }; + let Some(row) = row.copied() else { + tracing::warn!( + target: "get_supply", + "no cached supply row for {:?} commitment; returning node unhealthy", + commitment + ); + return Err(RpcError::NodeUnhealthy); + }; + let Some(non_circulating) = row.non_circulating else { + tracing::warn!( + target: "get_supply", + "supply row at slot {} has no non-circulating lamports yet; returning node unhealthy", + row.slot + ); + return Err(RpcError::NodeUnhealthy); + }; + if finalized_slot.saturating_sub(row.slot) > MAX_SUPPLY_STALENESS_SLOTS { + tracing::warn!( + target: "get_supply", + "supply slot {} is more than {} slots behind finalized slot {}; returning node unhealthy", + row.slot, + MAX_SUPPLY_STALENESS_SLOTS, + finalized_slot + ); + return Err(RpcError::NodeUnhealthy); + } + + let non_circulating_accounts = if config.exclude_non_circulating_accounts_list { + Vec::new() + } else { + accounts_list.clone() + }; + + Ok(RpcResponse { + context: RpcResponseContext { + slot: row.slot, + api_version: None, + }, + value: RpcSupply { + total: row.total, + circulating: row.total.saturating_sub(non_circulating), + non_circulating, + non_circulating_accounts, + }, + }) +} diff --git a/crates/api/src/methods/mod.rs b/crates/api/src/methods/mod.rs index d51e07b..13a0e08 100644 --- a/crates/api/src/methods/mod.rs +++ b/crates/api/src/methods/mod.rs @@ -14,6 +14,7 @@ pub mod genesis; pub mod get_account_info; pub mod get_balance; pub mod get_multiple_accounts; +pub mod get_supply; pub mod get_token_account_balance; pub mod mint; pub mod mint_accounts; diff --git a/crates/api/src/modules/mod.rs b/crates/api/src/modules/mod.rs index b901489..7827530 100644 --- a/crates/api/src/modules/mod.rs +++ b/crates/api/src/modules/mod.rs @@ -4,4 +4,5 @@ */ pub mod cache; +pub mod supply_cache; pub mod vote_accounts_cache; diff --git a/crates/api/src/modules/supply_cache.rs b/crates/api/src/modules/supply_cache.rs new file mode 100644 index 0000000..d4e7e84 --- /dev/null +++ b/crates/api/src/modules/supply_cache.rs @@ -0,0 +1,111 @@ +use std::{ + sync::{Arc, RwLock}, + time::Duration, +}; + +use cloudbreak_core::modules::supply_tracker::SUPPLY_RING_SLOTS; +use rust_decimal::{Decimal, prelude::ToPrimitive}; +use sea_orm::{ConnectionTrait, DatabaseBackend, DatabaseConnection, Statement}; +use solana_pubkey::Pubkey; +use tokio::task::JoinHandle; + +#[derive(Debug, Clone, Default)] +pub struct SupplySnapshot { + pub rows: Vec, + pub non_circulating_accounts: Option>, +} + +#[derive(Debug, Clone, Copy)] +pub struct SupplyRow { + pub slot: u64, + pub total: u64, + pub non_circulating: Option, +} + +pub type SharedSupplySnapshot = Arc>>; + +const SUPPLY_POLL_INTERVAL: Duration = Duration::from_secs(5); + +pub async fn load_latest_supply( + db: &DatabaseConnection, +) -> Result, anyhow::Error> { + let supply_rows = db + .query_all(Statement::from_sql_and_values( + DatabaseBackend::Postgres, + "SELECT slot, total, non_circulating_lamports FROM supply ORDER BY slot DESC LIMIT $1", + [(SUPPLY_RING_SLOTS as i64).into()], + )) + .await?; + if supply_rows.is_empty() { + return Ok(None); + } + let mut rows = Vec::with_capacity(supply_rows.len()); + for row in supply_rows { + let slot: i64 = row.try_get("", "slot")?; + let total: Decimal = row.try_get("", "total")?; + let total = total + .to_u64() + .ok_or_else(|| anyhow::anyhow!("supply.total {} does not fit in u64", total))?; + let non_circulating: Option = row.try_get("", "non_circulating_lamports")?; + let non_circulating = non_circulating + .map(|lamports| { + lamports.to_u64().ok_or_else(|| { + anyhow::anyhow!( + "supply.non_circulating_lamports {} does not fit in u64", + lamports + ) + }) + }) + .transpose()?; + rows.push(SupplyRow { + slot: slot as u64, + total, + non_circulating, + }); + } + rows.reverse(); + + let nc_row = db + .query_one(Statement::from_string( + DatabaseBackend::Postgres, + "SELECT accounts FROM non_circulating_accounts WHERE id = 1".to_string(), + )) + .await?; + let non_circulating_accounts = match nc_row { + Some(row) => { + let accounts: Vec> = row.try_get("", "accounts")?; + Some( + accounts + .into_iter() + .filter_map(|bytes| Pubkey::try_from(bytes.as_slice()).ok()) + .map(|p| p.to_string()) + .collect(), + ) + } + None => None, + }; + + Ok(Some(SupplySnapshot { + rows, + non_circulating_accounts, + })) +} + +pub fn spawn_poll_task(db: DatabaseConnection, cache: SharedSupplySnapshot) -> JoinHandle<()> { + tokio::spawn(async move { + loop { + tokio::time::sleep(SUPPLY_POLL_INTERVAL).await; + match load_latest_supply(&db).await { + Ok(Some(snapshot)) => { + *cache.write().unwrap() = Arc::new(snapshot); + } + Ok(None) => { + tracing::debug!(target: "supply_cache", "supply table is empty; will retry"); + } + Err(e) => { + tracing::error!(target: "supply_cache", "failed to load supply: {:?}", e); + } + } + } + }) +} diff --git a/crates/command/src/main.rs b/crates/command/src/main.rs index 8842eb5..68ae22b 100644 --- a/crates/command/src/main.rs +++ b/crates/command/src/main.rs @@ -6,7 +6,8 @@ use clap::{Parser, Subcommand}; use cloudbreak_api::run as run_api; use cloudbreak_core::{ - Result, SnapshotConfig, TryLoadConfig, modules::account_owner_map::AccountOwnerMap, + Result, SnapshotConfig, TryLoadConfig, + modules::{account_owner_map::AccountOwnerMap, supply_tracker::SupplyTracker}, }; use cloudbreak_index::indexer::run as run_index; use cloudbreak_query_tracker::run as run_query_tracker; @@ -63,6 +64,7 @@ async fn main() -> Result<()> { None, None, AccountOwnerMap::default(), + SupplyTracker::default(), ) .await } diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index 8a9e60d..5c64a15 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -376,6 +376,9 @@ pub struct IndexConfig { #[serde(default)] #[serde(rename = "accounts-owner-map-enabled")] pub accounts_owner_map_enabled: bool, + #[serde(default)] + #[serde(rename = "supply-tracker-enabled")] + pub supply_tracker_enabled: bool, } #[derive(Deserialize, Debug, Clone)] diff --git a/crates/core/src/modules/mod.rs b/crates/core/src/modules/mod.rs index a06a873..a47028f 100644 --- a/crates/core/src/modules/mod.rs +++ b/crates/core/src/modules/mod.rs @@ -5,3 +5,4 @@ pub mod account_owner_map; pub mod rpc_filter_type; +pub mod supply_tracker; diff --git a/crates/core/src/modules/supply_tracker.rs b/crates/core/src/modules/supply_tracker.rs new file mode 100644 index 0000000..23b64c0 --- /dev/null +++ b/crates/core/src/modules/supply_tracker.rs @@ -0,0 +1,205 @@ +use solana_pubkey::Pubkey; +use std::{ + collections::HashMap, + sync::{Arc, Mutex, RwLock}, +}; + +pub const SUPPLY_RING_SLOTS: u64 = 128; + +#[derive(Clone, Copy, Debug)] +struct TrackedLamports { + slot: u64, + lamports: u64, +} + +#[derive(Clone, Default)] +pub struct SupplyTracker(Option>); + +struct Inner { + state: Mutex, + accounts: RwLock>, +} + +#[derive(Clone, Copy, Default, PartialEq)] +enum SupplyStatus { + #[default] + Bootstrapping, + Live, + Stale, + ReanchorPending, +} + +#[derive(Default)] +struct SupplyState { + status: SupplyStatus, + total: u64, + slot: u64, + anchor_slot: u64, + pending_delta: i128, + non_circulating_accounts: Option>, +} + +#[derive(Debug, Clone)] +pub struct SupplyCommit { + pub slot: u64, + pub total: u64, + pub non_circulating: Option, +} + +impl SupplyTracker { + pub fn new() -> Self { + Self(Some(Arc::new(Inner { + state: Mutex::new(SupplyState::default()), + accounts: RwLock::new(HashMap::new()), + }))) + } + + pub fn is_enabled(&self) -> bool { + self.0.is_some() + } + + pub fn observe_account(&self, pubkey: &[u8], slot: u64, lamports: u64) { + let Some(inner) = &self.0 else { return }; + let pubkey = Pubkey::try_from(pubkey).unwrap(); + let mut state = inner.state.lock().expect("Failed to lock supply state"); + state.slot = state.slot.max(slot); + + let mut map = inner + .accounts + .write() + .expect("Failed to write supply accounts"); + let previous_lamports = match map.get(&pubkey) { + Some(entry) if entry.slot > slot => return, + Some(entry) => entry.lamports, + None => 0, + }; + if lamports == 0 && state.status == SupplyStatus::Live { + map.remove(&pubkey); + } else { + map.insert(pubkey, TrackedLamports { slot, lamports }); + } + drop(map); + + if state.status != SupplyStatus::Live { + return; + } + state.pending_delta += lamports as i128 - previous_lamports as i128; + } + + pub fn observe_snapshot_account(&self, pubkey: &Pubkey, slot: u64, lamports: u64) { + let Some(inner) = &self.0 else { return }; + let mut map = inner + .accounts + .write() + .expect("Failed to write supply accounts"); + match map.get(pubkey) { + Some(entry) if entry.slot >= slot => {} + _ => { + map.insert(*pubkey, TrackedLamports { slot, lamports }); + } + } + } + + pub fn set_non_circulating_accounts(&self, accounts: Vec) { + let Some(inner) = &self.0 else { return }; + inner + .state + .lock() + .expect("Failed to lock supply state") + .non_circulating_accounts = Some(accounts); + } + + pub fn note_anchor(&self, slot: u64) { + let Some(inner) = &self.0 else { return }; + let mut state = inner.state.lock().expect("Failed to lock supply state"); + state.anchor_slot = state.anchor_slot.max(slot); + } + + pub fn finish_bootstrap(&self) -> Option { + let inner = self.0.as_deref()?; + let mut state = inner.state.lock().expect("Failed to lock supply state"); + if state.status != SupplyStatus::Bootstrapping || state.anchor_slot == 0 { + return None; + } + state.total = inner.sweep_and_sum(); + state.slot = state.slot.max(state.anchor_slot); + state.status = SupplyStatus::Live; + state.pending_delta = 0; + Some(inner.commit(&state)) + } + + pub fn mark_stale(&self) -> bool { + let Some(inner) = &self.0 else { + return false; + }; + let mut state = inner.state.lock().expect("Failed to lock supply state"); + if state.status != SupplyStatus::Live { + return false; + } + state.status = SupplyStatus::Stale; + state.pending_delta = 0; + true + } + + pub fn request_reanchor(&self) { + let Some(inner) = &self.0 else { return }; + let mut state = inner.state.lock().expect("Failed to lock supply state"); + if state.status == SupplyStatus::Stale { + state.status = SupplyStatus::ReanchorPending; + } + } + + pub fn commit_block(&self, slot: u64) -> Option { + let inner = self.0.as_deref()?; + let mut state = inner.state.lock().expect("Failed to lock supply state"); + state.slot = state.slot.max(slot); + match state.status { + SupplyStatus::Bootstrapping | SupplyStatus::Stale => return None, + SupplyStatus::ReanchorPending => { + state.total = inner.sweep_and_sum(); + state.status = SupplyStatus::Live; + } + SupplyStatus::Live => { + state.total = (state.total as i128 + state.pending_delta) as u64; + } + } + state.pending_delta = 0; + Some(inner.commit(&state)) + } +} + +impl Inner { + fn commit(&self, state: &SupplyState) -> SupplyCommit { + SupplyCommit { + slot: state.slot, + total: state.total, + non_circulating: self.sum_non_circulating(state), + } + } + + fn sum_non_circulating(&self, state: &SupplyState) -> Option { + let accounts = state.non_circulating_accounts.as_ref()?; + let map = self + .accounts + .read() + .expect("Failed to read supply accounts"); + let lamports: u128 = accounts + .iter() + .map(|pubkey| map.get(pubkey).map_or(0, |entry| entry.lamports as u128)) + .sum(); + Some(lamports as u64) + } + + fn sweep_and_sum(&self) -> u64 { + let mut map = self + .accounts + .write() + .expect("Failed to write supply accounts"); + let mut total: u128 = 0; + map.retain(|_, entry| { + total += entry.lamports as u128; + entry.lamports > 0 + }); + total as u64 + } +} diff --git a/crates/index/src/db_queries.rs b/crates/index/src/db_queries.rs index b438b2a..b4e1b10 100644 --- a/crates/index/src/db_queries.rs +++ b/crates/index/src/db_queries.rs @@ -8,6 +8,8 @@ use std::{ time::Duration, }; +use rust_decimal::Decimal; +use solana_pubkey::Pubkey; use sea_orm::{ ActiveValue::{NotSet, Set}, ColumnTrait, Condition, ConnectionTrait, DatabaseBackend, DatabaseConnection, EntityTrait, @@ -20,7 +22,13 @@ use tokio::{ time::{Instant, timeout}, }; use yellowstone_grpc_proto::{geyser::CommitmentLevel, prelude::UnixTimestamp}; -use cloudbreak_core::{IndexConfig, modules::account_owner_map::AccountOwnerMap}; +use cloudbreak_core::{ + IndexConfig, + modules::{ + account_owner_map::AccountOwnerMap, + supply_tracker::{SUPPLY_RING_SLOTS, SupplyCommit}, + }, +}; use cloudbreak_entity::{accounts, service_health, slots}; use crate::metrics; @@ -410,6 +418,71 @@ pub async fn insert_recent_blockhash( } } +pub async fn upsert_supply_row(db: &DatabaseConnection, commit: &SupplyCommit, config: &IndexConfig) { + let query_timeout = Duration::from_secs(config.database.save_block_queries_timeout); + let supply = Statement::from_sql_and_values( + DatabaseBackend::Postgres, + "WITH upsert AS ( \ + INSERT INTO supply (slot, total, non_circulating_lamports) VALUES ($1, $2, $3) \ + ON CONFLICT (slot) DO UPDATE SET \ + total = EXCLUDED.total, \ + non_circulating_lamports = EXCLUDED.non_circulating_lamports, \ + updated_at = now() \ + ) \ + DELETE FROM supply WHERE slot < $4", + [ + Value::from(commit.slot as i64), + Value::from(Decimal::from(commit.total)), + Value::from(commit.non_circulating.map(Decimal::from)), + Value::from(commit.slot.saturating_sub(SUPPLY_RING_SLOTS) as i64), + ], + ); + let result = timeout(query_timeout, db.execute(supply)) + .await + .unwrap_or_else(|elapsed| { + tracing::error!("upsert_supply_row timeout ERROR: {}", elapsed); + Err(sea_orm::DbErr::RecordNotInserted) + }); + + if let Err(e) = result { + tracing::error!("upsert_supply_row failed for slot {}: {}", commit.slot, e); + metrics::increment_db_errors(); + } +} + +pub async fn upsert_non_circulating_accounts( + db: &DatabaseConnection, + slot: u64, + accounts: &[Pubkey], +) { + let accounts: Vec = accounts + .iter() + .map(|p| Value::Bytes(Some(Box::new(p.to_bytes().to_vec())))) + .collect(); + let result = db + .execute(Statement::from_sql_and_values( + DatabaseBackend::Postgres, + "INSERT INTO non_circulating_accounts (id, slot, accounts, updated_at) \ + VALUES (1, $1, $2, now()) \ + ON CONFLICT (id) DO UPDATE SET \ + slot = EXCLUDED.slot, \ + accounts = EXCLUDED.accounts, \ + updated_at = now()", + [ + Value::from(slot as i64), + Value::Array( + sea_orm::sea_query::ArrayType::Bytes, + Some(Box::new(accounts)), + ), + ], + )) + .await; + if let Err(e) = result { + tracing::error!("upsert_non_circulating_accounts failed for slot {}: {}", slot, e); + metrics::increment_db_errors(); + } +} + /// The latest persisted slot for each commitment level, plus the finalized→confirmed lag. /// /// The `slots` table holds exactly one row per commitment (its primary key), updated to the diff --git a/crates/index/src/indexer.rs b/crates/index/src/indexer.rs index 27ddb84..6f76d57 100644 --- a/crates/index/src/indexer.rs +++ b/crates/index/src/indexer.rs @@ -5,7 +5,7 @@ use cloudbreak_core::{ EnvironmentInfo, IndexConfig, Result as CloudbreakResult, TryLoadConfig, - modules::account_owner_map::AccountOwnerMap, + modules::{account_owner_map::AccountOwnerMap, supply_tracker::SupplyTracker}, }; use sea_orm::{ConnectOptions, Database, DatabaseConnection}; use std::{ @@ -49,6 +49,7 @@ pub struct IndexerState { pub finalize_slot_buffer_size: Arc>, /// Used to track the accounts owner pub accounts_owner_map: AccountOwnerMap, + pub supply_tracker: SupplyTracker, } pub async fn run(config: &str) -> CloudbreakResult<()> { @@ -90,6 +91,15 @@ pub async fn run(config: &str) -> CloudbreakResult<()> { AccountOwnerMap::default() }; + let supply_tracker = if config.supply_tracker_enabled { + if !config.programs.supports_simulation() { + panic!("supply-tracker-enabled requires an empty [programs] filter"); + } + SupplyTracker::new() + } else { + SupplyTracker::default() + }; + // Service health is tracked as a set of reasons (Startup is set until the startup snapshot is // processed; GapFill is set while a gap fill is in progress). let health = ServiceHealth::new(db.clone()); @@ -107,11 +117,16 @@ pub async fn run(config: &str) -> CloudbreakResult<()> { let indexer_state = IndexerState { buffer_channel_rx_len: Arc::new(Mutex::new(buffer_channel_rx.len())), snapshot_processing_state: snapshot_processing_state.clone(), - self_healing_state: SelfHealingState::new(&config, slot_finalizer.clone()), + self_healing_state: SelfHealingState::new( + &config, + slot_finalizer.clone(), + supply_tracker.clone(), + ), slot_finalizer, updated_accounts_during_startup, finalize_slot_buffer_size: finalize_slot_buffer_size.clone(), accounts_owner_map, + supply_tracker, }; // Used for the hash-checker to signal the main loop to stop @@ -151,6 +166,11 @@ pub async fn run(config: &str) -> CloudbreakResult<()> { let _epoch_stakes_handle = modules::epoch_stakes::spawn_epoch_stakes_recomputer(db.clone(), config.clone()); + let _non_circulating_handle = modules::non_circulating::spawn_non_circulating_recomputer( + db.clone(), + indexer_state.supply_tracker.clone(), + ); + operational_endpoints::self_healing::SELF_HEALING .set(indexer_state.self_healing_state.clone()) .ok() diff --git a/crates/index/src/metrics.rs b/crates/index/src/metrics.rs index 8aa0c64..baea68f 100644 --- a/crates/index/src/metrics.rs +++ b/crates/index/src/metrics.rs @@ -278,6 +278,21 @@ lazy_static::lazy_static! { "cloudbreak_finalize_slot_handler_queue_size", "Size of the finalize slot handler queue" ) .expect("Failed to create finalize slot handler queue size gauge"); + + pub static ref SUPPLY_TOTAL_LAMPORTS: IntGauge = IntGauge::new( + "cloudbreak_supply_total_lamports", "Running total supply in lamports" + ) + .expect("Failed to create supply total lamports gauge"); + + pub static ref SUPPLY_SLOT: IntGauge = IntGauge::new( + "cloudbreak_supply_slot", "Slot of the last committed supply total" + ) + .expect("Failed to create supply slot gauge"); + + pub static ref SUPPLY_STALE: IntGauge = IntGauge::new( + "cloudbreak_supply_stale", "Whether the supply tracker is stale (1) or fresh (0)" + ) + .expect("Failed to create supply stale gauge"); } /// We use a guard to increment the current tokio tasks metric when a task is created and @@ -414,5 +429,8 @@ pub fn register_collectors() { register!(GRPC_TOTAL_UPDATES_RECEIVED); register!(GRPC_BUFFER_CHANNEL_SIZE_SENDER); register!(FINALIZE_SLOT_DELETED_ACCOUNTS); + register!(SUPPLY_TOTAL_LAMPORTS); + register!(SUPPLY_SLOT); + register!(SUPPLY_STALE); }); } diff --git a/crates/index/src/modules/epoch_stakes.rs b/crates/index/src/modules/epoch_stakes.rs index e320def..decf394 100644 --- a/crates/index/src/modules/epoch_stakes.rs +++ b/crates/index/src/modules/epoch_stakes.rs @@ -42,7 +42,7 @@ const EPOCH_REWARDS_SYSVAR_ID: Pubkey = Pubkey::from_str_const("SysvarEpochRewards1111111111111111111111111"); /// Latest live state per account for a given owner, across the live and snapshot tables. -const LATEST_BY_OWNER_SQL: &str = r#" +pub(crate) const LATEST_BY_OWNER_SQL: &str = r#" WITH latest AS ( SELECT DISTINCT ON (pubkey) pubkey, data, lamports FROM ( @@ -173,7 +173,7 @@ pub fn spawn_epoch_stakes_recomputer( }) } -async fn is_healthy(db: &DatabaseConnection) -> bool { +pub(crate) async fn is_healthy(db: &DatabaseConnection) -> bool { db.query_one(Statement::from_string( DatabaseBackend::Postgres, "SELECT healthy FROM service_health WHERE id = 1".to_string(), diff --git a/crates/index/src/modules/mod.rs b/crates/index/src/modules/mod.rs index 16ba93a..b55469a 100644 --- a/crates/index/src/modules/mod.rs +++ b/crates/index/src/modules/mod.rs @@ -9,6 +9,8 @@ pub mod grpc; pub mod hash_checker; pub mod health; pub mod lt_hash; +pub mod non_circulating; +pub mod non_circulating_lists; pub mod panic_handler; pub mod save_block; pub mod self_healing; diff --git a/crates/index/src/modules/non_circulating.rs b/crates/index/src/modules/non_circulating.rs new file mode 100644 index 0000000..585f3fc --- /dev/null +++ b/crates/index/src/modules/non_circulating.rs @@ -0,0 +1,158 @@ +use std::collections::HashSet; +use std::time::Duration; + +use cloudbreak_core::STAKE_PROGRAM_ID; +use cloudbreak_core::modules::supply_tracker::SupplyTracker; +use futures::TryStreamExt; +use sea_orm::{ConnectionTrait, DatabaseBackend, DatabaseConnection, Statement, StreamTrait}; +use solana_program::clock::Clock; +use solana_pubkey::Pubkey; +use solana_stake_interface::state::StakeStateV2; +use tokio::task::JoinHandle; +use tokio::time::Instant; + +use crate::metrics; +use crate::modules::epoch_stakes::{LATEST_BY_OWNER_SQL, is_healthy}; +use crate::modules::non_circulating_lists::{NON_CIRCULATING_ACCOUNTS, WITHDRAW_AUTHORITY}; + +const POLL_INTERVAL: Duration = Duration::from_secs(60); +const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(600); + +const SYSVAR_OWNER_ID: Pubkey = + Pubkey::from_str_const("Sysvar1111111111111111111111111111111111111"); +const CLOCK_SYSVAR_ID: Pubkey = + Pubkey::from_str_const("SysvarC1ock11111111111111111111111111111111"); + +pub fn spawn_non_circulating_recomputer( + db: DatabaseConnection, + supply_tracker: SupplyTracker, +) -> JoinHandle<()> { + tokio::spawn(async move { + let _guard = metrics::TokioTaskCounterGuard::new("non_circulating_recomputer"); + + if !supply_tracker.is_enabled() { + return; + } + + let mut last_recompute: Option = None; + let mut next_lockup_expiry: Option = None; + loop { + tokio::time::sleep(POLL_INTERVAL).await; + + if !is_healthy(&db).await { + continue; + } + + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64; + let expiry_due = next_lockup_expiry.is_some_and(|ts| now >= ts); + + if !expiry_due && last_recompute.is_some_and(|last| last.elapsed() < HEARTBEAT_INTERVAL) + { + continue; + } + + let (slot, accounts, next_expiry) = match recompute(&db).await { + Ok(result) => result, + Err(e) => { + tracing::error!( + target: "non_circulating_recomputer", + "failed to recompute non-circulating membership: {:?}", + e + ); + continue; + } + }; + last_recompute = Some(Instant::now()); + next_lockup_expiry = next_expiry; + crate::db_queries::upsert_non_circulating_accounts(&db, slot, &accounts).await; + supply_tracker.set_non_circulating_accounts(accounts); + } + }) +} + +async fn recompute( + db: &DatabaseConnection, +) -> Result<(u64, Vec, Option), anyhow::Error> { + let start_time = Instant::now(); + let clock = read_clock(db) + .await + .ok_or_else(|| anyhow::anyhow!("Clock sysvar not found in index"))?; + + let withdraw_authorities: HashSet = WITHDRAW_AUTHORITY.iter().copied().collect(); + let mut set: HashSet = NON_CIRCULATING_ACCOUNTS.iter().copied().collect(); + let mut next_expiry: Option = None; + + let mut stream = db + .stream(Statement::from_sql_and_values( + DatabaseBackend::Postgres, + LATEST_BY_OWNER_SQL, + [STAKE_PROGRAM_ID.to_bytes().to_vec().into()], + )) + .await?; + while let Some(row) = stream.try_next().await? { + let pubkey_bytes: Vec = row.try_get("", "pubkey")?; + let data: Vec = row.try_get("", "data")?; + let Ok(pubkey) = Pubkey::try_from(pubkey_bytes.as_slice()) else { + continue; + }; + let Ok(state) = bincode::deserialize::(&data) else { + continue; + }; + let meta = match state { + StakeStateV2::Initialized(meta) => meta, + StakeStateV2::Stake(meta, _stake, _flags) => meta, + _ => continue, + }; + let in_force = meta.lockup.is_in_force(&clock, None); + if in_force + && meta.lockup.epoch <= clock.epoch + && meta.lockup.unix_timestamp > clock.unix_timestamp + { + let ts = meta.lockup.unix_timestamp; + next_expiry = Some(next_expiry.map_or(ts, |current| current.min(ts))); + } + if in_force || withdraw_authorities.contains(&meta.authorized.withdrawer) { + set.insert(pubkey); + } + } + + tracing::debug!( + target: "non_circulating_recomputer", + "recomputed membership ({} accounts) in {:.3}s", + set.len(), + start_time.elapsed().as_secs_f64() + ); + Ok((clock.slot, set.into_iter().collect(), next_expiry)) +} + +async fn read_clock(db: &DatabaseConnection) -> Option { + let row = db + .query_one(Statement::from_sql_and_values( + DatabaseBackend::Postgres, + r#" + SELECT data FROM ( + SELECT slot, data, lamports FROM accounts + WHERE owner = $1 AND pubkey = $2 + UNION ALL + SELECT slot, data, lamports FROM snapshot_accounts + WHERE owner = $1 AND pubkey = $2 + ) AS u + WHERE lamports > 0 + ORDER BY slot DESC + LIMIT 1 + "#, + [ + SYSVAR_OWNER_ID.to_bytes().to_vec().into(), + CLOCK_SYSVAR_ID.to_bytes().to_vec().into(), + ], + )) + .await + .ok() + .flatten()?; + + let data: Vec = row.try_get("", "data").ok()?; + bincode::deserialize::(&data).ok() +} diff --git a/crates/index/src/modules/non_circulating_lists.rs b/crates/index/src/modules/non_circulating_lists.rs new file mode 100644 index 0000000..43bb08c --- /dev/null +++ b/crates/index/src/modules/non_circulating_lists.rs @@ -0,0 +1,349 @@ +use solana_pubkey::Pubkey; + +pub const AGAVE_SOURCE_COMMIT: &str = "e8320a0b376d77a90396b04ee07aaba4e0918f15"; +pub const AGAVE_SOURCE_DATE: &str = "2026-07-16"; + +pub static NON_CIRCULATING_ACCOUNTS: &[Pubkey] = &[ + Pubkey::from_str_const("9huDUZfxoJ7wGMTffUE7vh1xePqef7gyrLJu9NApncqA"), + Pubkey::from_str_const("GK2zqSsXLA2rwVZk347RYhh6jJpRsCA69FjLW93ZGi3B"), + Pubkey::from_str_const("CWeRmXme7LmbaUWTZWFLt6FMnpzLCHaQLuR2TdgFn4Lq"), + Pubkey::from_str_const("HCV5dGFJXRrJ3jhDYA4DCeb9TEDTwGGYXtT3wHksu2Zr"), + Pubkey::from_str_const("14FUT96s9swbmH7ZjpDvfEDywnAYy9zaNhv4xvezySGu"), + Pubkey::from_str_const("HbZ5FfmKWNHC7uwk6TF1hVi6TCs7dtYfdjEcuPGgzFAg"), + Pubkey::from_str_const("C7C8odR8oashR5Feyrq2tJKaXL18id1dSj2zbkDGL2C2"), + Pubkey::from_str_const("Eyr9P5XsjK2NUKNCnfu39eqpGoiLFgVAv1LSQgMZCwiQ"), + Pubkey::from_str_const("DE1bawNcRJB9rVm3buyMVfr8mBEoyyu73NBovf2oXJsJ"), + Pubkey::from_str_const("CakcnaRDHka2gXyfbEd2d3xsvkJkqsLw2akB3zsN1D2S"), + Pubkey::from_str_const("7Np41oeYqPefeNQEHSv1UDhYrehxin3NStELsSKCT4K2"), + Pubkey::from_str_const("GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ"), + Pubkey::from_str_const("Mc5XB47H3DKJHym5RLa9mPzWv5snERsF3KNv5AauXK8"), + Pubkey::from_str_const("7cvkjYAkUYs4W8XcXsca7cBrEGFeSUjeZmKoNBvEwyri"), + Pubkey::from_str_const("AG3m2bAibcY8raMt4oXEGqRHwX4FWKPPJVjZxn1LySDX"), + Pubkey::from_str_const("5XdtyEDREHJXXW1CTtCsVjJRjBapAwK78ZquzvnNVRrV"), + Pubkey::from_str_const("6yKHERk8rsbmJxvMpPuwPs1ct3hRiP7xaJF2tvnGU6nK"), + Pubkey::from_str_const("CHmdL15akDcJgBkY6BP3hzs98Dqr6wbdDC5p8odvtSbq"), + Pubkey::from_str_const("FR84wZQy3Y3j2gWz6pgETUiUoJtreMEuWfbg6573UCj9"), + Pubkey::from_str_const("5q54XjQ7vDx4y6KphPeE97LUNiYGtP55spjvXAWPGBuf"), + Pubkey::from_str_const("3o6xgkJ9sTmDeQWyfj3sxwon18fXJB9PV5LDc8sfgR4a"), + Pubkey::from_str_const("GumSE5HsMV5HCwBTv2D2D81yy9x17aDkvobkqAfTRgmo"), + Pubkey::from_str_const("AzVV9ZZDxTgW4wWfJmsG6ytaHpQGSe1yz76Nyy84VbQF"), + Pubkey::from_str_const("8CUUMKYNGxdgYio5CLHRHyzMEhhVRMcqefgE6dLqnVRK"), + Pubkey::from_str_const("CQDYc4ET2mbFhVpgj41gXahL6Exn5ZoPcGAzSHuYxwmE"), + Pubkey::from_str_const("5PLJZLJiRR9vf7d1JCCg7UuWjtyN9nkab9uok6TqSyuP"), + Pubkey::from_str_const("7xJ9CLtEAcEShw9kW2gSoZkRWL566Dg12cvgzANJwbTr"), + Pubkey::from_str_const("BuCEvc9ze8UoAQwwsQLy8d447C8sA4zeVtVpc6m5wQeS"), + Pubkey::from_str_const("8ndGYFjav6NDXvzYcxs449Aub3AxYv4vYpk89zRDwgj7"), + Pubkey::from_str_const("8W58E8JVJjH1jCy5CeHJQgvwFXTyAVyesuXRZGbcSUGG"), + Pubkey::from_str_const("GNiz4Mq886bTNDT3pijGsu2gbw6it7sqrwncro45USeB"), + Pubkey::from_str_const("GhsotwFMH6XUrRLJCxcx62h7748N2Uq8mf87hUGkmPhg"), + Pubkey::from_str_const("Fgyh8EeYGZtbW8sS33YmNQnzx54WXPrJ5KWNPkCfWPot"), + Pubkey::from_str_const("8UVjvYyoqP6sqcctTso3xpCdCfgTMiv3VRh7vraC2eJk"), + Pubkey::from_str_const("BhvLngiqqKeZ8rpxch2uGjeCiC88zzewoWPRuoxpp1aS"), + Pubkey::from_str_const("63DtkW7zuARcd185EmHAkfF44bDcC2SiTSEj2spLP3iA"), + Pubkey::from_str_const("GvpCiTgq9dmEeojCDBivoLoZqc4AkbUDACpqPMwYLWKh"), + Pubkey::from_str_const("7Y8smnoUrYKGGuDq2uaFKVxJYhojgg7DVixHyAtGTYEV"), + Pubkey::from_str_const("DUS1KxwUhUyDKB4A81E8vdnTe3hSahd92Abtn9CXsEcj"), + Pubkey::from_str_const("F9MWFw8cnYVwsRq8Am1PGfFL3cQUZV37mbGoxZftzLjN"), + Pubkey::from_str_const("8vqrX3H2BYLaXVintse3gorPEM4TgTwTFZNN1Fm9TdYs"), + Pubkey::from_str_const("CUageMFi49kzoDqtdU8NvQ4Bq3sbtJygjKDAXJ45nmAi"), + Pubkey::from_str_const("5smrYwb1Hr2T8XMnvsqccTgXxuqQs14iuE8RbHFYf2Cf"), + Pubkey::from_str_const("xQadXQiUTCCFhfHjvQx1hyJK6KVWr1w2fD6DT3cdwj7"), + Pubkey::from_str_const("8DE8fqPfv1fp9DHyGyDFFaMjpopMgDeXspzoi9jpBJjC"), + Pubkey::from_str_const("3itU5ME8L6FDqtMiRoUiT1F7PwbkTtHBbW51YWD5jtjm"), + Pubkey::from_str_const("AsrYX4FeLXnZcrjcZmrASY2Eq1jvEeQfwxtNTxS5zojA"), + Pubkey::from_str_const("8rT45mqpuDBR1vcnDc9kwP9DrZAXDR4ZeuKWw3u1gTGa"), + Pubkey::from_str_const("nGME7HgBT6tAJN1f6YuCCngpqT5cvSTndZUVLjQ4jwA"), + Pubkey::from_str_const("CzAHrrrHKx9Lxf6wdCMrsZkLvk74c7J2vGv8VYPUmY6v"), + Pubkey::from_str_const("AzHQ8Bia1grVVbcGyci7wzueSWkgvu7YZVZ4B9rkL5P6"), + Pubkey::from_str_const("FiWYY85b58zEEcPtxe3PuqzWPjqBJXqdwgZeqSBmT9Cn"), + Pubkey::from_str_const("GpxpMVhrBBBEYbEJxdR62w3daWz444V7m6dxYDZKH77D"), + Pubkey::from_str_const("3bTGcGB9F98XxnrBNftmmm48JGfPgi5sYxDEKiCjQYk3"), + Pubkey::from_str_const("8pNBEppa1VcFAsx4Hzq9CpdXUXZjUXbvQwLX2K7QsCwb"), + Pubkey::from_str_const("HKJgYGTTYYR2ZkfJKHbn58w676fKueQXmvbtpyvrSM3N"), + Pubkey::from_str_const("3jnknRabs7G2V9dKhxd2KP85pNWXKXiedYnYxtySnQMs"), + Pubkey::from_str_const("4sxwau4mdqZ8zEJsfryXq4QFYnMJSCp3HWuZQod8WU5k"), + Pubkey::from_str_const("Fg12tB1tz8w6zJSQ4ZAGotWoCztdMJF9hqK8R11pakog"), + Pubkey::from_str_const("GEWSkfWgHkpiLbeKaAnwvqnECGdRNf49at5nFccVey7c"), + Pubkey::from_str_const("CND6ZjRTzaCFVdX7pSSWgjTfHZuhxqFDoUBqWBJguNoA"), + Pubkey::from_str_const("2WWb1gRzuXDd5viZLQF7pNRR6Y7UiyeaPpaL35X6j3ve"), + Pubkey::from_str_const("BUnRE27mYXN9p8H1Ay24GXhJC88q2CuwLoNU2v2CrW4W"), + Pubkey::from_str_const("CsUqV42gVQLJwQsKyjWHqGkfHarxn9hcY4YeSjgaaeTd"), + Pubkey::from_str_const("5khMKAcvmsFaAhoKkdg3u5abvKsmjUQNmhTNP624WB1F"), + Pubkey::from_str_const("GpYnVDgB7dzvwSgsjQFeHznjG6Kt1DLBFYrKxjGU1LuD"), + Pubkey::from_str_const("DQQGPtj7pphPHCLzzBuEyDDQByUcKGrsJdsH7SP3hAug"), + Pubkey::from_str_const("FwfaykN7ACnsEUDHANzGHqTGQZMcGnUSsahAHUqbdPrz"), + Pubkey::from_str_const("JCwT5Ygmq3VeBEbDjL8s8E82Ra2rP9bq45QfZE7Xyaq7"), + Pubkey::from_str_const("H3Ni7vG1CsmJZdTvxF7RkAf9UM5qk4RsohJsmPvtZNnu"), + Pubkey::from_str_const("CVgyXrbEd1ctEuvq11QdpnCQVnPit8NLdhyqXQHLprM2"), + Pubkey::from_str_const("EAJJD6nDqtXcZ4DnQb19F9XEz8y8bRDHxbWbahatZNbL"), + Pubkey::from_str_const("6o5v1HC7WhBnLfRHp8mQTtCP2khdXXjhuyGyYEoy2Suy"), + Pubkey::from_str_const("3ZrsTmNM6AkMcqFfv3ryfhQ2jMfqP64RQbqVyAaxqhrQ"), + Pubkey::from_str_const("6zw7em7uQdmMpuS9fGz8Nq9TLHa5YQhEKKwPjo5PwDK4"), + Pubkey::from_str_const("CuatS6njAcfkFHnvai7zXCs7syA9bykXWsDCJEWfhjHG"), + Pubkey::from_str_const("Hz9nydgN1k15wnwffKX7CSmZp4VFTnTwLXAEdomFGNXy"), + Pubkey::from_str_const("Ep5Y58PaSyALPrdFxDVAdfKtVdP55vApvsWjb3jSmXsG"), + Pubkey::from_str_const("EziVYi3Sv5kJWxmU77PnbrT8jmkVuqwdiFLLzZpLVEn7"), + Pubkey::from_str_const("H1rt8KvXkNhQExTRfkY8r9wjZbZ8yCih6J4wQ5Fz9HGP"), + Pubkey::from_str_const("6nN69B4uZuESZYxr9nrLDjmKRtjDZQXrehwkfQTKw62U"), + Pubkey::from_str_const("Hm9JW7of5i9dnrboS8pCUCSeoQUPh7JsP1rkbJnW7An4"), + Pubkey::from_str_const("5D5NxsNVTgXHyVziwV7mDFwVDS6voaBsyyGxUbhQrhNW"), + Pubkey::from_str_const("EMAY24PrS6rWfvpqffFCsTsFJypeeYYmtUc26wdh3Wup"), + Pubkey::from_str_const("Br3aeVGapRb2xTq17RU2pYZCoJpWA7bq6TKBCcYtMSmt"), + Pubkey::from_str_const("BUjkdqUuH5Lz9XzcMcR4DdEMnFG6r8QzUMBm16Rfau96"), + Pubkey::from_str_const("Es13uD2p64UVPFpEWfDtd6SERdoNR2XVgqBQBZcZSLqW"), + Pubkey::from_str_const("AVYpwVou2BhdLivAwLxKPALZQsY7aZNkNmGbP2fZw7RU"), + Pubkey::from_str_const("DrKzW5koKSZp4mg4BdHLwr72MMXscd2kTiWgckCvvPXz"), + Pubkey::from_str_const("9hknftBZAQL4f48tWfk3bUEV5YSLcYYtDRqNmpNnhCWG"), + Pubkey::from_str_const("GLUmCeJpXB8veNcchPwibkRYwCwvQbKodex5mEjrgToi"), + Pubkey::from_str_const("9S2M3UYPpnPZTBtbcUvehYmiWFK3kBhwfzV2iWuwvaVy"), + Pubkey::from_str_const("HUAkU5psJXZuw54Lrg1ksbXzHv2fzczQ9sNbmisVMeJU"), + Pubkey::from_str_const("GK8R4uUmrawcREZ5xJy5dAzVV5V7aFvYg77id37pVTK"), + Pubkey::from_str_const("4vuWt1oHRqLMhf8Nv1zyEXZsYaeK7dipwrfKLoYU9Riq"), + Pubkey::from_str_const("EMhn1U3TMimW3bvWYbPUvN2eZnCfsuBN4LGWhzzYhiWR"), + Pubkey::from_str_const("BsKsunvENxAraBrL77UfAn1Gi7unVEmQAdCbhsjUN6tU"), + Pubkey::from_str_const("CTvhdUVf8KNyMbyEdnvRrBCHJjBKtQwkbj6zwoqcEssG"), + Pubkey::from_str_const("3fV2GaDKa3pZxyDcpMh5Vrh2FVAMUiWUKbYmnBFv8As3"), + Pubkey::from_str_const("4pV47TiPzZ7SSBPHmgUvSLmH9mMSe8tjyPhQZGbi1zPC"), + Pubkey::from_str_const("P8aKfWQPeRnsZtpBrwWTYzyAoRk74KMz56xc6NEpC4J"), + Pubkey::from_str_const("HuqDWJodFhAEWh6aWdsDVUqsjRket5DYXMYyDYtD8hdN"), + Pubkey::from_str_const("Ab1UcdsFXZVnkSt1Z3vcYU65GQk5MvCbs54SviaiaqHb"), + Pubkey::from_str_const("Dc2oHxFXQaC2QfLStuU7txtD3U5HZ82MrCSGDooWjbsv"), + Pubkey::from_str_const("3iPvAS4xdhYr6SkhVDHCLr7tJjMAFK4wvvHWJxFQVg15"), + Pubkey::from_str_const("GmyW1nqYcrw7P7JqrcyP9ivU9hYNbrgZ1r5SYJJH41Fs"), + Pubkey::from_str_const("E8jcgWvrvV7rwYHJThwfiBeQ8VAH4FgNEEMG9aAuCMAq"), + Pubkey::from_str_const("CY7X5o3Wi2eQhTocLmUS6JSWyx1NinBfW7AXRrkRCpi8"), + Pubkey::from_str_const("HQJtLqvEGGxgNYfRXUurfxV8E1swvCnsbC3456ik27HY"), + Pubkey::from_str_const("9xbcBZoGYFnfJZe81EDuDYKUm8xGkjzW8z4EgnVhNvsv"), + Pubkey::from_str_const("4sNBQyPbJCQyUimBueZkGWnLVqds4rWkm7eXyi9WskGU"), + Pubkey::from_str_const("AZWdNvnZxJnbcT8ZzonpN19AZJadxPdUxSiCEDTJzu8L"), + Pubkey::from_str_const("5kFTzLuM2VgFdb6x16smnY3JWoVdPxNZVFAqeVgjSTUP"), + Pubkey::from_str_const("Gbz6wkNFus8SNEkWGNNENLv9NFwVvF1pWVDpaVKUWcMh"), + Pubkey::from_str_const("AksPzoA9DKCipgdhHjhUzQJe4iEniCvBoEfvayuFA3BN"), + Pubkey::from_str_const("DVhs8YHWrvhhGxoefDNY9KotqtEEnjnSAK8MYGL2Q7X"), + Pubkey::from_str_const("2bvGnYAPSV8pa1H3vRYr5tPAXktP4DkFACHfAgqyyfhd"), + Pubkey::from_str_const("2xP8YQ3sVmfNPtGM17tZi7Lr4vsPUiN6mHLx42roazG5"), + Pubkey::from_str_const("3p9ZxnrSFTkXVrT3KnYg2tT6asnysDApEFB5DRkdeAhB"), + Pubkey::from_str_const("wRVP5MYuqP8HJ1Q8RCJ5NzUraL3DxCKPGMKSBd5iQH1"), + Pubkey::from_str_const("513qFSVgmAQFBDsnyFCM1MrVKBrWiDgb4nXrdGsqa7Z4"), + Pubkey::from_str_const("619qLS85ieR4qh7MGNLLZyLefN7hMi2FDVb5cmX1nisb"), + Pubkey::from_str_const("768NcPfBJpFBtjDAbYZLFkej6ca1W5jeArsW5MtdF8S9"), + Pubkey::from_str_const("EWAjC8a9VPbALSM3D6tGsbRfgDV48kRuHZPu8qtYSNDv"), + Pubkey::from_str_const("H8wurbnaaXsgtrjqkNH1HhncUPUhTLAmKUHkwMeyqmfN"), + Pubkey::from_str_const("HBfi37TwD4kMa1WrAWwXp3ZaFbZQ1g3XxWwNZs8QsCpY"), + Pubkey::from_str_const("6B7mXMM6BixHvDpPAPLSKweLyCXcbtprkfsw3HfMUSjZ"), + Pubkey::from_str_const("5TuV9WpmESXNfTNqasXVehoXpQy65WUHBbzgKPXPLwWx"), + Pubkey::from_str_const("FB6VmiYFnVGp1uKXA3WbNsqg9neGYVpwYYiB9q8bFRrQ"), + Pubkey::from_str_const("7Anoa4ZRiq8qaaiEnhmdpXyTEmBjZASXGfnQqVDynNie"), + Pubkey::from_str_const("6dXeE5hS8bQKeqZsc18ewCyqHimnhCBAvVJBusnRqa2F"), + Pubkey::from_str_const("Ds69ZQPb3D3aVPXdN5REyzALBrzJLdruJZ1cwyfyoEEx"), + Pubkey::from_str_const("5HA8QV7tp59iNpfjs8f84LGUGX4imaynMFSKWHUcTrMT"), + Pubkey::from_str_const("EPfiDzgbdgXdyqqwbYFMqU6Qvfx2J9Zf2J9noXBYYbbx"), + Pubkey::from_str_const("DEGyTHFXmYuyANRDYRoEcShBXLonWNdBRpNzbFZBuzhY"), + Pubkey::from_str_const("9xz1vZSWgY6TFPLZgPM2WJjDM1KiPXcALnjYFHRTkYiK"), + Pubkey::from_str_const("6n1mSmsdGFCkEyyHe4wtgEigbwhiwYRszrerMW9YRyYF"), + Pubkey::from_str_const("8rp9vcJG1Uwk25g9XfQNHysfGBYcHizVEJvavqiffB6P"), + Pubkey::from_str_const("BVsJuJbhXmxPB9XPZrYMXs1SfuZuo998u1jQ9emVp8YD"), + Pubkey::from_str_const("Fas9FrnngyVjdMZ6r8p8SsfyFDtHLMmrw1dyBxRWWVTF"), + Pubkey::from_str_const("6Lfk4e6yxXGcFNMHue5u8KQM9hWYKjbdMFZ2n7SHHd6L"), + Pubkey::from_str_const("6WuU6DWoM7ZpgUVL5TR97Fgose7vLgrv4Qvg1Yhqv8Xw"), + Pubkey::from_str_const("6mXiGJbQbX726ZhrMSmbLCF81E8rkSfo1pvBWyn1asG5"), + Pubkey::from_str_const("2UdDthRdtZKMjEx9oQYnzuLdWy3sa4XKeaqihdXkXFza"), + Pubkey::from_str_const("33XwSCq2Ft5tTCsGK7kTyMCJ4hqufDZT2REFAWXmsia8"), + Pubkey::from_str_const("B5tRZWRCTCLANkY5zLnxEXAn8S2exJD9EkH3cacEYqmE"), + Pubkey::from_str_const("CfCvwXZk1sLSqzuoFx1K7KG1puMofgwHA3tSpcArVysX"), + Pubkey::from_str_const("C4fATCN93YL5iYS3c2MrHeDmkMD6waHuBXLqsri5q5AF"), + Pubkey::from_str_const("HbJUXNHXLzpucDr9wgjuoqAwK6XNKEcqi5h29MkkuSBQ"), + Pubkey::from_str_const("G8EBx1Qo5W8731nmaBSGYyvU96onpEP5adrVSjt4vnLF"), + Pubkey::from_str_const("4dc5ty7TNod2EQfx4bPnDofPbQk5cELcn3it47qnLizL"), + Pubkey::from_str_const("4VCET97mE8ixxAPRYabM7S7dvkiAFZ8ZzEoH6kbmfYus"), + Pubkey::from_str_const("DwqBsPgtAHqQUmRN3WF5YTuLKhK7dqzrb9eRJ4UVRNLf"), + Pubkey::from_str_const("26FqcAbTLvr6cVXk2ktPu9hpeMYWukY7uUwNCtTwu7Wb"), + Pubkey::from_str_const("B22zqCsZL6PDsi4fftmcjj5k7hFjhH1BM8Dcz4fXfFXK"), + Pubkey::from_str_const("9ahLw5LFpeTuBykK3A5aiFZ1CidTY7z4YxG2kmQdSGon"), + Pubkey::from_str_const("8M731ZthMdeVoMFGRaPnUBzR7Ze6qY4zEcQ7WALEAn5i"), + Pubkey::from_str_const("A3aaVsuX7BfNCUQbuepQSaLs9KNB7c6djG6u4UuJzwwP"), + Pubkey::from_str_const("DeXE7LTUqsC3B9kSACfFdW5TPG1eT9n1Z6gVB9dJNxqz"), + Pubkey::from_str_const("BtvSeDfZNfA3VEH6baFprfx6JcS6JX8jdt6NffCxoJGV"), + Pubkey::from_str_const("8o8trB6XdG8o4qo29De2mcooY5E3pW2rvMMkeBwJHkUD"), + Pubkey::from_str_const("CSSxN8vQYqSjg5GV6ZUGSM65dXqhytLzANLpMrBZjLKP"), + Pubkey::from_str_const("5A2yoxdGdC3B3QC39pkXpjx2ZPH4WGgie9exHQnhNz4J"), + Pubkey::from_str_const("DFaHYDcuLmxx3vrJGCgw8sPehzBNjvbvZTjz7PgvYLcb"), + Pubkey::from_str_const("43Cwv866ActL11JqVQXoFPiytUYA3tFXUaNg1zPsarn9"), + Pubkey::from_str_const("3wzw3L4wd3B7DHocbQxHosQXEfqEz5AnkQHKBisGKLzY"), + Pubkey::from_str_const("EnX9MvycxMGKFpMPJHgNo6kD2proVaorKQcSaMmguMEZ"), + Pubkey::from_str_const("GLz4zxemfHJy3BnK7PjZ4uUgJoHtycpT3JrmTDYduVpg"), + Pubkey::from_str_const("362rDzQwChHJ9ToZgLdv8wzFHTTsxsxzFLTZPwKY3CYg"), + Pubkey::from_str_const("3FKwgoQWCz4i43Pbo3jdfsjzkgrkjnk44rDWh7A9J9Ee"), + Pubkey::from_str_const("Cj53e3n1WX9dAUNX5PZpsc9xG9e7hXGnuJkKLJkNaf6v"), + Pubkey::from_str_const("Amn9KQZY5kJj932b49pxwqZuuKfNWewrmNQtdMrRfdvf"), + Pubkey::from_str_const("6spvvQJzFCcw5fVVdF1zKxCeq8Gj2yRFGgFuG16fYSMo"), + Pubkey::from_str_const("H5PutDz8EgAoznaX7Bc8qSB6sPxD2Xho145H6MH6AcbV"), + Pubkey::from_str_const("HNCooAiRiVeqGK9VSjxsvRsfr4weTcx14XhkvM7zDMgx"), + Pubkey::from_str_const("AUqLkd6bR54NtXY9Zt4J2zZ7CJbhVHuDR95YFk5qahaX"), + Pubkey::from_str_const("H35YRuMGE6WDPBLu3fDYLxV7tEcL2yJ7C97jDkw6NrBf"), + Pubkey::from_str_const("EhN8bv8LCXnNAVju9ERGsCccmdBNmNTAESAHXE8PyBLR"), + Pubkey::from_str_const("Ht5roME7Fjzt3dcEyf7UxYpPPAAdyTrnJ6rgEwQrNAFg"), + Pubkey::from_str_const("5zHRch6mowPiobRQHYXZ1siRcthw4dYkFPuaNDQkK3EE"), + Pubkey::from_str_const("AZkdkkHzSEbaZXpr7xBmt71AHj9c8iWE2oWYELm3nns5"), + Pubkey::from_str_const("FThHNjNQYi7FLvP4KjeYf3LXjcN2LhyYHmza35ucqtDT"), + Pubkey::from_str_const("6vHscAGjgbTfi1ooRVWGjSiUhHcUnLBceJyuKjpKrCYy"), + Pubkey::from_str_const("AnbszMCtjEVZTquG3FQ68x1f1WK5B1Z6E7gtN2J6vD5a"), + Pubkey::from_str_const("5aUknZidNKmAPxLMHoUeWsEdmXX8sGKvj8s71pHTDC7H"), + Pubkey::from_str_const("7Xgby7EgwoRkhbQSdNJL9sXwpo3jddUhqB4zv3WuJB4m"), + Pubkey::from_str_const("43KdfhYK6LUXX4LmyxfZ1KLaQrfpRe8vNwopNS7wXsUC"), + Pubkey::from_str_const("FG6UGgT4VpCQxjwQsVy6woHeemwb5jP5u9kR5ry3twcK"), + Pubkey::from_str_const("BDvLyqobZFubd7ESEJde6KqjbNoChkdj5aEiBdCqcN5r"), + Pubkey::from_str_const("BtF4msVoQYJPrQUMwNtEXw58LVjux18ddVhC4XjbsdwX"), + Pubkey::from_str_const("GbPK6cgUKso2ZGNV6GRwbp3hkdUzTVjsybYQMGDBvMFa"), + Pubkey::from_str_const("4bWjzNek2m6P3Q8rKkq11C47TaoAwpj3ykk5yDCYLWr1"), + Pubkey::from_str_const("FPzPT7YV5BHSTjYjqycXbwBsD4E5WsrZNPcCgcHhPkUx"), + Pubkey::from_str_const("EbitXxcwjC1dzrGJ7sa3Y6RBzSM3wxvrxMaBncNBKEf2"), + Pubkey::from_str_const("HMmHHmCmC3oo4G4QCjKvNxctSBQzRFDp2WQHZoXyrW6Q"), + Pubkey::from_str_const("145FLxWrDG9gKifyJKSnkKNAUPhG6hxUMEiZWw6XeSMW"), + Pubkey::from_str_const("FnXgwJZT7RMhpYEJFCXGLHp63bhVLfVn5d4aSVQDe19n"), + Pubkey::from_str_const("5tDptzMGG79DLp9XV4fdcmiRNPQGCkfBJeo8EVCSYKw7"), + Pubkey::from_str_const("9oBNYSARUMuH2swnxzbxCi827YfUnYWyWSAkrHz93h5y"), + Pubkey::from_str_const("DWfXRxmUXKdSTji4S1nXncviTmrhWQwxHAQCcvjRLeWm"), + Pubkey::from_str_const("EHUv6Hc9XGAb57C8iNCUuoE9xnJnRTFr9LaP7zbu8LNB"), + Pubkey::from_str_const("CTqnGTKHrcg8AVK5pSwVzpA3mmSxz7Js9y7rEqCbjryH"), + Pubkey::from_str_const("7PzsZ3ApLcfQosSr2B1C9YV1GNNgriZVFyqzDpeHoAD"), + Pubkey::from_str_const("AKXpgmVhedTT49sekGbh6MXfx2kJKBcaFQLpc83NCp6N"), + Pubkey::from_str_const("3MP45RTcFdLX3HpPD18asXCCxFjsgrZM9V6tEeQxFXd4"), + Pubkey::from_str_const("88YC29WwpugANLcN7DYkuL3benW79WsdxkQ3KX8zER9s"), + Pubkey::from_str_const("98j2yMfxrLzQ5YQJU1FMW3CTkoYgZStJirpm4HLAkA3o"), + Pubkey::from_str_const("56AidDLicyQaLMqbMQeZ7itUmJyd7TAvehVDi8MXDYbx"), + Pubkey::from_str_const("3yeCpRbBU5gzBaweVRqUXFcZUp7gSXBeknKcbGmjwmjJ"), + Pubkey::from_str_const("5McU7LcsRw3RHyrjj98qHefx6ehy7MdjF1uKKi6Kp8hV"), + Pubkey::from_str_const("3CYxk8uTXtANvbU1HW3NHNhKcBEfYesi2BdBFizy3f6s"), + Pubkey::from_str_const("4bAowZuPRG8jzNwMceqjYjZnrGXQNR5ZCgQJM8s4hF4i"), + Pubkey::from_str_const("ATWFW8xBGPe6xB8JnjyjZqG5FuEXnfyuhEocmqAqfeJG"), + Pubkey::from_str_const("73Aa7yCd6AMPqsaLisfgrpjESVFtDrLnHEjfDsXBK3HV"), +]; + +pub static WITHDRAW_AUTHORITY: &[Pubkey] = &[ + Pubkey::from_str_const("8CUUMKYNGxdgYio5CLHRHyzMEhhVRMcqefgE6dLqnVRK"), + Pubkey::from_str_const("3FFaheyqtyAXZSYxDzsr5CVKvJuvZD1WE1VEsBtDbRqB"), + Pubkey::from_str_const("FdGYQdiRky8NZzN9wZtczTBcWLYYRXrJ3LMDhqDPn5rM"), + Pubkey::from_str_const("4e6KwQpyzGQPfgVr5Jn3g5jLjbXB4pKPa2jRLohEb1QA"), + Pubkey::from_str_const("FjiEiVKyMGzSLpqoB27QypukUfyWHrwzPcGNtopzZVdh"), + Pubkey::from_str_const("DwbVjia1mYeSGoJipzhaf4L5hfer2DJ1Ys681VzQm5YY"), + Pubkey::from_str_const("GeMGyvsTEsANVvcT5cme65Xq5MVU8fVVzMQ13KAZFNS2"), + Pubkey::from_str_const("Bj3aQ2oFnZYfNR1njzRjmWizzuhvfcYLckh76cqsbuBM"), + Pubkey::from_str_const("4ZJhPQAgUseCsWhKvJLTmmRRUV74fdoTpQLNfKoekbPY"), + Pubkey::from_str_const("HXdYQ5gixrY2H6Y9gqsD8kPM2JQKSaRiohDQtLbZkRWE"), + Pubkey::from_str_const("4sNBQyPbJCQyUimBueZkGWnLVqds4rWkm7eXyi9WskGU"), + Pubkey::from_str_const("AZWdNvnZxJnbcT8ZzonpN19AZJadxPdUxSiCEDTJzu8L"), + Pubkey::from_str_const("5kFTzLuM2VgFdb6x16smnY3JWoVdPxNZVFAqeVgjSTUP"), + Pubkey::from_str_const("Gbz6wkNFus8SNEkWGNNENLv9NFwVvF1pWVDpaVKUWcMh"), + Pubkey::from_str_const("AksPzoA9DKCipgdhHjhUzQJe4iEniCvBoEfvayuFA3BN"), + Pubkey::from_str_const("DVhs8YHWrvhhGxoefDNY9KotqtEEnjnSAK8MYGL2Q7X"), + Pubkey::from_str_const("2bvGnYAPSV8pa1H3vRYr5tPAXktP4DkFACHfAgqyyfhd"), + Pubkey::from_str_const("2xP8YQ3sVmfNPtGM17tZi7Lr4vsPUiN6mHLx42roazG5"), + Pubkey::from_str_const("3p9ZxnrSFTkXVrT3KnYg2tT6asnysDApEFB5DRkdeAhB"), + Pubkey::from_str_const("wRVP5MYuqP8HJ1Q8RCJ5NzUraL3DxCKPGMKSBd5iQH1"), + Pubkey::from_str_const("513qFSVgmAQFBDsnyFCM1MrVKBrWiDgb4nXrdGsqa7Z4"), + Pubkey::from_str_const("619qLS85ieR4qh7MGNLLZyLefN7hMi2FDVb5cmX1nisb"), + Pubkey::from_str_const("768NcPfBJpFBtjDAbYZLFkej6ca1W5jeArsW5MtdF8S9"), + Pubkey::from_str_const("EWAjC8a9VPbALSM3D6tGsbRfgDV48kRuHZPu8qtYSNDv"), + Pubkey::from_str_const("H8wurbnaaXsgtrjqkNH1HhncUPUhTLAmKUHkwMeyqmfN"), + Pubkey::from_str_const("HBfi37TwD4kMa1WrAWwXp3ZaFbZQ1g3XxWwNZs8QsCpY"), + Pubkey::from_str_const("6B7mXMM6BixHvDpPAPLSKweLyCXcbtprkfsw3HfMUSjZ"), + Pubkey::from_str_const("5TuV9WpmESXNfTNqasXVehoXpQy65WUHBbzgKPXPLwWx"), + Pubkey::from_str_const("FB6VmiYFnVGp1uKXA3WbNsqg9neGYVpwYYiB9q8bFRrQ"), + Pubkey::from_str_const("7Anoa4ZRiq8qaaiEnhmdpXyTEmBjZASXGfnQqVDynNie"), + Pubkey::from_str_const("6dXeE5hS8bQKeqZsc18ewCyqHimnhCBAvVJBusnRqa2F"), + Pubkey::from_str_const("Ds69ZQPb3D3aVPXdN5REyzALBrzJLdruJZ1cwyfyoEEx"), + Pubkey::from_str_const("5HA8QV7tp59iNpfjs8f84LGUGX4imaynMFSKWHUcTrMT"), + Pubkey::from_str_const("EPfiDzgbdgXdyqqwbYFMqU6Qvfx2J9Zf2J9noXBYYbbx"), + Pubkey::from_str_const("DEGyTHFXmYuyANRDYRoEcShBXLonWNdBRpNzbFZBuzhY"), + Pubkey::from_str_const("9xz1vZSWgY6TFPLZgPM2WJjDM1KiPXcALnjYFHRTkYiK"), + Pubkey::from_str_const("6n1mSmsdGFCkEyyHe4wtgEigbwhiwYRszrerMW9YRyYF"), + Pubkey::from_str_const("8rp9vcJG1Uwk25g9XfQNHysfGBYcHizVEJvavqiffB6P"), + Pubkey::from_str_const("BVsJuJbhXmxPB9XPZrYMXs1SfuZuo998u1jQ9emVp8YD"), + Pubkey::from_str_const("Fas9FrnngyVjdMZ6r8p8SsfyFDtHLMmrw1dyBxRWWVTF"), + Pubkey::from_str_const("6Lfk4e6yxXGcFNMHue5u8KQM9hWYKjbdMFZ2n7SHHd6L"), + Pubkey::from_str_const("6WuU6DWoM7ZpgUVL5TR97Fgose7vLgrv4Qvg1Yhqv8Xw"), + Pubkey::from_str_const("6mXiGJbQbX726ZhrMSmbLCF81E8rkSfo1pvBWyn1asG5"), + Pubkey::from_str_const("2UdDthRdtZKMjEx9oQYnzuLdWy3sa4XKeaqihdXkXFza"), + Pubkey::from_str_const("33XwSCq2Ft5tTCsGK7kTyMCJ4hqufDZT2REFAWXmsia8"), + Pubkey::from_str_const("B5tRZWRCTCLANkY5zLnxEXAn8S2exJD9EkH3cacEYqmE"), + Pubkey::from_str_const("CfCvwXZk1sLSqzuoFx1K7KG1puMofgwHA3tSpcArVysX"), + Pubkey::from_str_const("C4fATCN93YL5iYS3c2MrHeDmkMD6waHuBXLqsri5q5AF"), + Pubkey::from_str_const("HbJUXNHXLzpucDr9wgjuoqAwK6XNKEcqi5h29MkkuSBQ"), + Pubkey::from_str_const("G8EBx1Qo5W8731nmaBSGYyvU96onpEP5adrVSjt4vnLF"), + Pubkey::from_str_const("4dc5ty7TNod2EQfx4bPnDofPbQk5cELcn3it47qnLizL"), + Pubkey::from_str_const("4VCET97mE8ixxAPRYabM7S7dvkiAFZ8ZzEoH6kbmfYus"), + Pubkey::from_str_const("DwqBsPgtAHqQUmRN3WF5YTuLKhK7dqzrb9eRJ4UVRNLf"), + Pubkey::from_str_const("26FqcAbTLvr6cVXk2ktPu9hpeMYWukY7uUwNCtTwu7Wb"), + Pubkey::from_str_const("B22zqCsZL6PDsi4fftmcjj5k7hFjhH1BM8Dcz4fXfFXK"), + Pubkey::from_str_const("9ahLw5LFpeTuBykK3A5aiFZ1CidTY7z4YxG2kmQdSGon"), + Pubkey::from_str_const("8M731ZthMdeVoMFGRaPnUBzR7Ze6qY4zEcQ7WALEAn5i"), + Pubkey::from_str_const("A3aaVsuX7BfNCUQbuepQSaLs9KNB7c6djG6u4UuJzwwP"), + Pubkey::from_str_const("DeXE7LTUqsC3B9kSACfFdW5TPG1eT9n1Z6gVB9dJNxqz"), + Pubkey::from_str_const("BtvSeDfZNfA3VEH6baFprfx6JcS6JX8jdt6NffCxoJGV"), + Pubkey::from_str_const("8o8trB6XdG8o4qo29De2mcooY5E3pW2rvMMkeBwJHkUD"), + Pubkey::from_str_const("CSSxN8vQYqSjg5GV6ZUGSM65dXqhytLzANLpMrBZjLKP"), + Pubkey::from_str_const("5A2yoxdGdC3B3QC39pkXpjx2ZPH4WGgie9exHQnhNz4J"), + Pubkey::from_str_const("DFaHYDcuLmxx3vrJGCgw8sPehzBNjvbvZTjz7PgvYLcb"), + Pubkey::from_str_const("43Cwv866ActL11JqVQXoFPiytUYA3tFXUaNg1zPsarn9"), + Pubkey::from_str_const("3wzw3L4wd3B7DHocbQxHosQXEfqEz5AnkQHKBisGKLzY"), + Pubkey::from_str_const("EnX9MvycxMGKFpMPJHgNo6kD2proVaorKQcSaMmguMEZ"), + Pubkey::from_str_const("GLz4zxemfHJy3BnK7PjZ4uUgJoHtycpT3JrmTDYduVpg"), + Pubkey::from_str_const("362rDzQwChHJ9ToZgLdv8wzFHTTsxsxzFLTZPwKY3CYg"), + Pubkey::from_str_const("3FKwgoQWCz4i43Pbo3jdfsjzkgrkjnk44rDWh7A9J9Ee"), + Pubkey::from_str_const("Cj53e3n1WX9dAUNX5PZpsc9xG9e7hXGnuJkKLJkNaf6v"), + Pubkey::from_str_const("Amn9KQZY5kJj932b49pxwqZuuKfNWewrmNQtdMrRfdvf"), + Pubkey::from_str_const("6spvvQJzFCcw5fVVdF1zKxCeq8Gj2yRFGgFuG16fYSMo"), + Pubkey::from_str_const("H5PutDz8EgAoznaX7Bc8qSB6sPxD2Xho145H6MH6AcbV"), + Pubkey::from_str_const("HNCooAiRiVeqGK9VSjxsvRsfr4weTcx14XhkvM7zDMgx"), + Pubkey::from_str_const("AUqLkd6bR54NtXY9Zt4J2zZ7CJbhVHuDR95YFk5qahaX"), + Pubkey::from_str_const("H35YRuMGE6WDPBLu3fDYLxV7tEcL2yJ7C97jDkw6NrBf"), + Pubkey::from_str_const("EhN8bv8LCXnNAVju9ERGsCccmdBNmNTAESAHXE8PyBLR"), + Pubkey::from_str_const("Ht5roME7Fjzt3dcEyf7UxYpPPAAdyTrnJ6rgEwQrNAFg"), + Pubkey::from_str_const("5zHRch6mowPiobRQHYXZ1siRcthw4dYkFPuaNDQkK3EE"), + Pubkey::from_str_const("AZkdkkHzSEbaZXpr7xBmt71AHj9c8iWE2oWYELm3nns5"), + Pubkey::from_str_const("FThHNjNQYi7FLvP4KjeYf3LXjcN2LhyYHmza35ucqtDT"), + Pubkey::from_str_const("6vHscAGjgbTfi1ooRVWGjSiUhHcUnLBceJyuKjpKrCYy"), + Pubkey::from_str_const("AnbszMCtjEVZTquG3FQ68x1f1WK5B1Z6E7gtN2J6vD5a"), + Pubkey::from_str_const("5aUknZidNKmAPxLMHoUeWsEdmXX8sGKvj8s71pHTDC7H"), + Pubkey::from_str_const("7Xgby7EgwoRkhbQSdNJL9sXwpo3jddUhqB4zv3WuJB4m"), + Pubkey::from_str_const("43KdfhYK6LUXX4LmyxfZ1KLaQrfpRe8vNwopNS7wXsUC"), + Pubkey::from_str_const("FG6UGgT4VpCQxjwQsVy6woHeemwb5jP5u9kR5ry3twcK"), + Pubkey::from_str_const("BDvLyqobZFubd7ESEJde6KqjbNoChkdj5aEiBdCqcN5r"), + Pubkey::from_str_const("BtF4msVoQYJPrQUMwNtEXw58LVjux18ddVhC4XjbsdwX"), + Pubkey::from_str_const("GbPK6cgUKso2ZGNV6GRwbp3hkdUzTVjsybYQMGDBvMFa"), + Pubkey::from_str_const("4bWjzNek2m6P3Q8rKkq11C47TaoAwpj3ykk5yDCYLWr1"), + Pubkey::from_str_const("FPzPT7YV5BHSTjYjqycXbwBsD4E5WsrZNPcCgcHhPkUx"), + Pubkey::from_str_const("EbitXxcwjC1dzrGJ7sa3Y6RBzSM3wxvrxMaBncNBKEf2"), + Pubkey::from_str_const("HMmHHmCmC3oo4G4QCjKvNxctSBQzRFDp2WQHZoXyrW6Q"), + Pubkey::from_str_const("145FLxWrDG9gKifyJKSnkKNAUPhG6hxUMEiZWw6XeSMW"), + Pubkey::from_str_const("FnXgwJZT7RMhpYEJFCXGLHp63bhVLfVn5d4aSVQDe19n"), + Pubkey::from_str_const("5tDptzMGG79DLp9XV4fdcmiRNPQGCkfBJeo8EVCSYKw7"), + Pubkey::from_str_const("9oBNYSARUMuH2swnxzbxCi827YfUnYWyWSAkrHz93h5y"), + Pubkey::from_str_const("DWfXRxmUXKdSTji4S1nXncviTmrhWQwxHAQCcvjRLeWm"), + Pubkey::from_str_const("EHUv6Hc9XGAb57C8iNCUuoE9xnJnRTFr9LaP7zbu8LNB"), + Pubkey::from_str_const("CTqnGTKHrcg8AVK5pSwVzpA3mmSxz7Js9y7rEqCbjryH"), + Pubkey::from_str_const("7PzsZ3ApLcfQosSr2B1C9YV1GNNgriZVFyqzDpeHoAD"), + Pubkey::from_str_const("AKXpgmVhedTT49sekGbh6MXfx2kJKBcaFQLpc83NCp6N"), + Pubkey::from_str_const("3MP45RTcFdLX3HpPD18asXCCxFjsgrZM9V6tEeQxFXd4"), + Pubkey::from_str_const("88YC29WwpugANLcN7DYkuL3benW79WsdxkQ3KX8zER9s"), + Pubkey::from_str_const("98j2yMfxrLzQ5YQJU1FMW3CTkoYgZStJirpm4HLAkA3o"), + Pubkey::from_str_const("56AidDLicyQaLMqbMQeZ7itUmJyd7TAvehVDi8MXDYbx"), + Pubkey::from_str_const("3yeCpRbBU5gzBaweVRqUXFcZUp7gSXBeknKcbGmjwmjJ"), + Pubkey::from_str_const("5McU7LcsRw3RHyrjj98qHefx6ehy7MdjF1uKKi6Kp8hV"), + Pubkey::from_str_const("3CYxk8uTXtANvbU1HW3NHNhKcBEfYesi2BdBFizy3f6s"), + Pubkey::from_str_const("4bAowZuPRG8jzNwMceqjYjZnrGXQNR5ZCgQJM8s4hF4i"), + Pubkey::from_str_const("ATWFW8xBGPe6xB8JnjyjZqG5FuEXnfyuhEocmqAqfeJG"), + Pubkey::from_str_const("73Aa7yCd6AMPqsaLisfgrpjESVFtDrLnHEjfDsXBK3HV"), +]; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pinned_list_sizes() { + assert_eq!(NON_CIRCULATING_ACCOUNTS.len(), 214); + assert_eq!(WITHDRAW_AUTHORITY.len(), 114); + } +} diff --git a/crates/index/src/modules/save_block.rs b/crates/index/src/modules/save_block.rs index 491c6fa..f00f795 100644 --- a/crates/index/src/modules/save_block.rs +++ b/crates/index/src/modules/save_block.rs @@ -37,6 +37,7 @@ pub async fn save_block( buffer_channel_rx_len: _, finalize_slot_buffer_size, accounts_owner_map, + supply_tracker, } = indexer_state; let start_time = Instant::now(); @@ -51,6 +52,7 @@ pub async fn save_block( snapshot_processing_state.clone(), finalize_slot_buffer_size.clone(), accounts_owner_map.clone(), + supply_tracker.clone(), ) .await; @@ -80,6 +82,8 @@ pub async fn save_block( // Create the chunks for updating the "accounts" table let system_program_id = [0u8; 32].to_vec(); for account in block.accounts { + supply_tracker.observe_account(&account.pubkey, slot, account.lamports); + // If the account is being closed we still add it to the hashmap for cleanup // but we don't add it to the "accounts" table in a normal fashion, instead we added using [`db_queries::insert_closed_accounts`] if account.lamports == 0 { @@ -246,6 +250,13 @@ pub async fn save_block( ) .await; + if let Some(commit) = supply_tracker.commit_block(slot) { + db_queries::upsert_supply_row(db, &commit, &config).await; + metrics::SUPPLY_TOTAL_LAMPORTS.set(commit.total as i64); + metrics::SUPPLY_SLOT.set(commit.slot as i64); + metrics::SUPPLY_STALE.set(0); + } + let elapsed = start_time.elapsed().as_secs_f64(); metrics::record_block_processing(elapsed, "block"); } diff --git a/crates/index/src/modules/self_healing.rs b/crates/index/src/modules/self_healing.rs index 62839bf..bcf7472 100644 --- a/crates/index/src/modules/self_healing.rs +++ b/crates/index/src/modules/self_healing.rs @@ -3,7 +3,7 @@ * Copyright 2025-2026 Triton One Limited. All rights reserved. */ -use cloudbreak_core::{IndexConfig, SnapshotConfig}; +use cloudbreak_core::{IndexConfig, SnapshotConfig, modules::supply_tracker::SupplyTracker}; use cloudbreak_snapshot::sidecar::{SnapshotPair, SnapshotType}; use sea_orm::DatabaseConnection; use std::{ @@ -38,15 +38,21 @@ pub struct SelfHealingState { /// [`SlotFinalizer::enqueue_gap_boundary`]). pub gap_boundaries: Arc>>, pub finalizer: SlotFinalizer, + pub supply_tracker: SupplyTracker, } impl SelfHealingState { - pub fn new(_config: &IndexConfig, finalizer: SlotFinalizer) -> Self { + pub fn new( + _config: &IndexConfig, + finalizer: SlotFinalizer, + supply_tracker: SupplyTracker, + ) -> Self { Self { last_slot_received: Arc::new(Mutex::new(0)), gaps_list: Arc::new(Mutex::new(Vec::new())), gap_boundaries: Arc::new(Mutex::new(BTreeSet::new())), finalizer, + supply_tracker, } } @@ -130,6 +136,10 @@ impl SelfHealingState { // startup pauses the very worker that completes startup; that is intentionally // unsupported and `fill_gaps` fails fast in that case. self.finalizer.pause().await; + + if self.supply_tracker.mark_stale() { + metrics::SUPPLY_STALE.set(1); + } } } @@ -286,6 +296,7 @@ impl SelfHealingState { snapshot_pair, snapshot_config, covered_gaps_list.clone(), + self.supply_tracker.clone(), ) { Ok((handle, rx)) => (handle, rx), Err(e) => { @@ -376,6 +387,7 @@ impl SelfHealingState { .expect("Failed to lock gaps_list") .is_empty(); if !gaps_remaining { + self.supply_tracker.request_reanchor(); self.finalizer.resume().await; } } @@ -451,6 +463,7 @@ fn download_and_process_snapshot_for_gap_filling( snapshot_pair: SnapshotPair, config: SnapshotConfig, gaps_list: Vec, + supply_tracker: SupplyTracker, ) -> Result< ( JoinHandle>, @@ -486,6 +499,7 @@ fn download_and_process_snapshot_for_gap_filling( config, gaps_list, tx, + supply_tracker, ) .await?; diff --git a/crates/index/src/modules/snapshot.rs b/crates/index/src/modules/snapshot.rs index 2bfa01c..9897591 100644 --- a/crates/index/src/modules/snapshot.rs +++ b/crates/index/src/modules/snapshot.rs @@ -5,7 +5,8 @@ use std::sync::{Arc, Mutex}; use cloudbreak_core::{ - IndexConfig, SnapshotConfig, modules::account_owner_map::AccountOwnerMap, + IndexConfig, SnapshotConfig, + modules::{account_owner_map::AccountOwnerMap, supply_tracker::SupplyTracker}, }; use crate::metrics; @@ -29,6 +30,7 @@ pub async fn process_snapshot_if_needed( snapshot_processing_state: Arc>, finalize_slot_buffer_size: Arc>, accounts_owner_map: AccountOwnerMap, + supply_tracker: SupplyTracker, ) { let snapshot_config = match config.snapshot { Some(snapshot_config) => snapshot_config, @@ -68,6 +70,7 @@ pub async fn process_snapshot_if_needed( Some(metrics::METRICS_REGISTRY.clone()), Some(finalize_slot_buffer_size.clone()), accounts_owner_map, + supply_tracker, ) .await; diff --git a/crates/integration_tests/src/benchmark.rs b/crates/integration_tests/src/benchmark.rs index 1f38c3d..39c3825 100644 --- a/crates/integration_tests/src/benchmark.rs +++ b/crates/integration_tests/src/benchmark.rs @@ -33,6 +33,7 @@ pub enum RequestType { GetBalance, GetTokenAccountBalance, SimulateTransaction, + GetSupply, } pub async fn run(args: &BenchmarkArgs) -> Result<()> { diff --git a/crates/integration_tests/src/response_comparison.rs b/crates/integration_tests/src/response_comparison.rs index b7b1c56..613a7a9 100644 --- a/crates/integration_tests/src/response_comparison.rs +++ b/crates/integration_tests/src/response_comparison.rs @@ -6,7 +6,7 @@ use anyhow::Result; use base64::Engine as _; use serde_json::Value as JsonValue; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::time::{Duration, SystemTime}; use crate::benchmark::RequestType; @@ -182,6 +182,7 @@ pub fn compare_responses( compare_value_direct(response1, response2) } RequestType::SimulateTransaction => compare_simulate_responses(response1, response2), + RequestType::GetSupply => compare_supply_responses(response1, response2), }; CompareResponsesResult::new_with_matching_context(matches, compare_context_result) @@ -197,6 +198,35 @@ fn compare_value_direct(response1: &JsonValue, response2: &JsonValue) -> bool { static NULL_VALUE: JsonValue = JsonValue::Null; +fn compare_supply_responses(response1: &JsonValue, response2: &JsonValue) -> bool { + let v1 = response1.get("result").and_then(|r| r.get("value")); + let v2 = response2.get("result").and_then(|r| r.get("value")); + + let (v1, v2) = match (v1, v2) { + (Some(v1), Some(v2)) => (v1, v2), + _ => return v1 == v2, + }; + + if ["total", "circulating", "nonCirculating"] + .iter() + .any(|key| field(v1, key) != field(v2, key)) + { + return false; + } + + let accounts_set = |value: &JsonValue| -> Option> { + field(value, "nonCirculatingAccounts") + .as_array() + .map(|array| { + array + .iter() + .filter_map(|a| a.as_str().map(String::from)) + .collect() + }) + }; + accounts_set(v1) == accounts_set(v2) +} + fn field<'a>(value: &'a JsonValue, key: &str) -> &'a JsonValue { value.get(key).unwrap_or(&NULL_VALUE) } diff --git a/crates/integration_tests/src/sources/victoria_logs.rs b/crates/integration_tests/src/sources/victoria_logs.rs index f60c23b..6b7adf9 100644 --- a/crates/integration_tests/src/sources/victoria_logs.rs +++ b/crates/integration_tests/src/sources/victoria_logs.rs @@ -65,6 +65,9 @@ pub fn get_body_query( RequestType::SimulateTransaction => format!( "query={time_filter}rpc_call:=\"simulateTransaction\" AND pool_dedicated:~\"foundation\" AND body:* | limit {limit}" ), + RequestType::GetSupply => format!( + "query={time_filter}rpc_call:=\"getSupply\" AND body:* | limit {limit}" + ), } } diff --git a/crates/integration_tests/src/utils.rs b/crates/integration_tests/src/utils.rs index 433d006..7d6a691 100644 --- a/crates/integration_tests/src/utils.rs +++ b/crates/integration_tests/src/utils.rs @@ -100,6 +100,7 @@ pub fn extract_commitment_from_request(request: &JsonValue, request_type: Reques | RequestType::GetTokenAccountBalance | RequestType::SimulateTransaction => 1, RequestType::Gtabo | RequestType::Gtabd => 2, + RequestType::GetSupply => 0, }; request .get("params") @@ -129,7 +130,7 @@ pub fn extract_encoding_from_request(request: &JsonValue, request_type: RequestT // getBalance / getTokenAccountBalance have no encoding concept. if matches!( request_type, - RequestType::GetBalance | RequestType::GetTokenAccountBalance + RequestType::GetBalance | RequestType::GetTokenAccountBalance | RequestType::GetSupply ) { return "none".to_string(); } @@ -142,7 +143,9 @@ pub fn extract_encoding_from_request(request: &JsonValue, request_type: RequestT | RequestType::GetMultipleAccounts | RequestType::SimulateTransaction => 1, RequestType::Gtabo | RequestType::Gtabd => 2, - RequestType::GetBalance | RequestType::GetTokenAccountBalance => unreachable!(), + RequestType::GetBalance | RequestType::GetTokenAccountBalance | RequestType::GetSupply => { + unreachable!() + } }; let encoding = request .get("params") @@ -161,7 +164,9 @@ pub fn extract_encoding_from_request(request: &JsonValue, request_type: RequestT RequestType::GetAccountInfo => "binary".to_string(), RequestType::GetMultipleAccounts => "base64".to_string(), RequestType::SimulateTransaction => "base58".to_string(), - RequestType::GetBalance | RequestType::GetTokenAccountBalance => unreachable!(), + RequestType::GetBalance + | RequestType::GetTokenAccountBalance + | RequestType::GetSupply => unreachable!(), }, } } diff --git a/crates/migration/src/lib.rs b/crates/migration/src/lib.rs index fad4174..6b48c52 100644 --- a/crates/migration/src/lib.rs +++ b/crates/migration/src/lib.rs @@ -21,6 +21,7 @@ mod m20260528_000000_create_epoch_stakes_table; mod m20260618_000000_create_auto_index_usage_table; mod m20260703_000000_create_recent_blockhashes_table; mod m20260709_000000_add_block_height_to_recent_blockhashes; +mod m20260717_000000_create_supply_tables; pub struct Migrator; @@ -42,6 +43,7 @@ impl MigratorTrait for Migrator { Box::new(m20260618_000000_create_auto_index_usage_table::Migration), Box::new(m20260703_000000_create_recent_blockhashes_table::Migration), Box::new(m20260709_000000_add_block_height_to_recent_blockhashes::Migration), + Box::new(m20260717_000000_create_supply_tables::Migration), ] } } diff --git a/crates/migration/src/m20260717_000000_create_supply_tables.rs b/crates/migration/src/m20260717_000000_create_supply_tables.rs new file mode 100644 index 0000000..bb81e07 --- /dev/null +++ b/crates/migration/src/m20260717_000000_create_supply_tables.rs @@ -0,0 +1,43 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let connection = manager.get_connection(); + + connection + .execute_unprepared( + r#" + CREATE TABLE IF NOT EXISTS supply ( + slot BIGINT PRIMARY KEY, + total NUMERIC(20, 0) NOT NULL, + non_circulating_lamports NUMERIC(20, 0), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + CREATE TABLE IF NOT EXISTS non_circulating_accounts ( + id BIGINT PRIMARY KEY DEFAULT 1, + slot BIGINT NOT NULL, + accounts BYTEA[] NOT NULL, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() + ); + "#, + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .get_connection() + .execute_unprepared( + "DROP TABLE IF EXISTS supply; DROP TABLE IF EXISTS non_circulating_accounts;", + ) + .await?; + + Ok(()) + } +} diff --git a/crates/snapshot/src/db_queries.rs b/crates/snapshot/src/db_queries.rs index fdd6e1c..7a90fc7 100644 --- a/crates/snapshot/src/db_queries.rs +++ b/crates/snapshot/src/db_queries.rs @@ -16,7 +16,10 @@ use sea_orm::{ }; use tokio::time::Instant; use yellowstone_grpc_proto::geyser::SubscribeUpdateAccount; -use cloudbreak_core::SnapshotPgIndexesConfig; +use cloudbreak_core::{ + SnapshotPgIndexesConfig, + modules::supply_tracker::{SUPPLY_RING_SLOTS, SupplyCommit}, +}; use cloudbreak_entity::snapshot_accounts::{self}; use crate::metrics; @@ -554,3 +557,52 @@ pub async fn persist_epoch_stakes( Ok(()) } +pub async fn persist_supply_seed( + db: &DatabaseConnection, + bank_info: &crate::sidecar::SnapshotBankInfo, +) -> Result<(), anyhow::Error> { + db.execute(Statement::from_sql_and_values( + sea_orm::DatabaseBackend::Postgres, + "WITH upsert AS ( \ + INSERT INTO supply (slot, total) VALUES ($1, $2) \ + ON CONFLICT (slot) DO UPDATE SET total = EXCLUDED.total, updated_at = now() \ + ) \ + DELETE FROM supply WHERE slot < $3", + [ + Value::from(bank_info.slot as i64), + Value::from(Decimal::from(bank_info.capitalization)), + Value::from(bank_info.slot.saturating_sub(SUPPLY_RING_SLOTS) as i64), + ], + )) + .await?; + + tracing::info!( + target: "persist_supply_seed", + "seeded supply from snapshot: slot {} capitalization {}", + bank_info.slot, + bank_info.capitalization + ); + + Ok(()) +} + +pub async fn persist_supply_total( + db: &DatabaseConnection, + commit: &SupplyCommit, +) -> Result<(), anyhow::Error> { + db.execute(Statement::from_sql_and_values( + sea_orm::DatabaseBackend::Postgres, + "INSERT INTO supply (slot, total, non_circulating_lamports) VALUES ($1, $2, $3) \ + ON CONFLICT (slot) DO UPDATE SET \ + total = EXCLUDED.total, \ + non_circulating_lamports = EXCLUDED.non_circulating_lamports, \ + updated_at = now()", + [ + Value::from(commit.slot as i64), + Value::from(Decimal::from(commit.total)), + Value::from(commit.non_circulating.map(Decimal::from)), + ], + )) + .await?; + Ok(()) +} diff --git a/crates/snapshot/src/lib.rs b/crates/snapshot/src/lib.rs index 1274813..d312119 100644 --- a/crates/snapshot/src/lib.rs +++ b/crates/snapshot/src/lib.rs @@ -15,7 +15,8 @@ use yellowstone_grpc_proto::geyser::{ SubscribeUpdateAccount, SubscribeUpdateAccountInfo, SubscribeUpdateBlock, }; use cloudbreak_core::{ - Result, SnapshotConfig, modules::account_owner_map::AccountOwnerMap, + Result, SnapshotConfig, + modules::{account_owner_map::AccountOwnerMap, supply_tracker::SupplyTracker}, }; use crate::{ @@ -45,6 +46,7 @@ pub async fn run( metrics_registry: Option, buffer_size: Option>>, accounts_owner_map: AccountOwnerMap, + supply_tracker: SupplyTracker, ) -> Result<()> { let start_time = Instant::now(); @@ -72,6 +74,7 @@ pub async fn run( &database, config.clone(), accounts_owner_map.clone(), + supply_tracker.clone(), ); // Process incremental snapshot only if needed @@ -83,6 +86,7 @@ pub async fn run( &database, config.clone(), accounts_owner_map.clone(), + supply_tracker.clone(), ) .await??; @@ -93,6 +97,12 @@ pub async fn run( tracing::info!(target: "full_snapshot_completed", "Full snapshot completed successfully in {} secs", start_time.elapsed().as_secs_f64()); + if let Some(commit) = supply_tracker.finish_bootstrap() + && let Err(e) = db_queries::persist_supply_total(&database, &commit).await + { + tracing::error!("Failed to persist bootstrapped supply total: {:?}", e); + } + if let Some(buffer_size) = buffer_size { db_queries::cluster_snapshot_accounts_table( &database, @@ -121,6 +131,7 @@ fn download_and_process_snapshot( database: &DatabaseConnection, config: SnapshotConfig, accounts_owner_map: AccountOwnerMap, + supply_tracker: SupplyTracker, ) -> JoinHandle> { let db_clone = database.clone(); @@ -137,7 +148,14 @@ fn download_and_process_snapshot( tracing::error!("Failed to download snapshot: {:?} ({:?})", e, snapshot_type); })?; - process_downloaded_snapshot(&db_clone, snapshot_data, config, accounts_owner_map).await?; + process_downloaded_snapshot( + &db_clone, + snapshot_data, + config, + accounts_owner_map, + supply_tracker, + ) + .await?; Ok(()) }) @@ -149,6 +167,7 @@ async fn process_downloaded_snapshot( snapshot_data: SnapshotData, config: SnapshotConfig, accounts_owner_map: AccountOwnerMap, + supply_tracker: SupplyTracker, ) -> Result<()> { let start_time = Instant::now(); let total_accounts_files_opening_time_micros = Arc::new(Mutex::new(0)); @@ -158,12 +177,20 @@ async fn process_downloaded_snapshot( let sidecar::UnpackedSnapshot { account_files: solana_snapshot, stake_data, + bank_info, } = sidecar::unpack_compressed_snapshot(path, &base_dir, snapshot_data.slot)?; if let Err(e) = db_queries::persist_epoch_stakes(database, &stake_data).await { tracing::error!("Failed to persist epoch stakes from snapshot: {:?}", e); } + if supply_tracker.is_enabled() { + supply_tracker.note_anchor(bank_info.slot); + if let Err(e) = db_queries::persist_supply_seed(database, &bank_info).await { + tracing::error!("Failed to persist supply seed from snapshot: {:?}", e); + } + } + let mut account_file_workers: JoinSet> = JoinSet::new(); let accounts_file_concurency = config.accounts_file_concurency.unwrap_or(32); let programs_include = config @@ -229,6 +256,7 @@ async fn process_downloaded_snapshot( insert_into_temp_snapshot_account_versions_tx.clone(); let accounts_owner_map = accounts_owner_map.clone(); + let supply_tracker = supply_tracker.clone(); account_file_workers.spawn(async move { let start_time = Instant::now(); @@ -278,6 +306,11 @@ async fn process_downloaded_snapshot( // Add non closed accounts to the accounts owner map (if enabled) if account.lamports > 0 { accounts_owner_map.upsert_account(&pubkey, &owner, account_file_slot); + supply_tracker.observe_snapshot_account( + account.pubkey, + account_file_slot, + account.lamports, + ); } let account_update = SubscribeUpdateAccount { @@ -428,13 +461,14 @@ pub async fn process_downloaded_snapshot_with_gap_filling( config: SnapshotConfig, gaps_list: Vec, block_sender: Sender, + supply_tracker: SupplyTracker, ) -> Result<()> { let start_time = Instant::now(); let path = base_dir.join(&incremental_snapshot_file_name); let sidecar::UnpackedSnapshot { account_files: solana_snapshot, - stake_data: _, + .. } = sidecar::unpack_compressed_snapshot(path, &base_dir, snapshot_slot)?; let mut account_file_workers: JoinSet> = JoinSet::new(); let accounts_file_concurency = config.accounts_file_concurency.unwrap_or(32); @@ -493,6 +527,7 @@ pub async fn process_downloaded_snapshot_with_gap_filling( } let block_sender = block_sender.clone(); + let supply_tracker = supply_tracker.clone(); account_file_workers.spawn(async move { let accounts = AccountsFile::new_for_startup( path, @@ -520,6 +555,14 @@ pub async fn process_downloaded_snapshot_with_gap_filling( return; } + if account.lamports > 0 { + supply_tracker.observe_snapshot_account( + account.pubkey, + account_file_slot, + account.lamports, + ); + } + let account_update = SubscribeUpdateAccountInfo { pubkey: account.pubkey.to_bytes().to_vec(), lamports: account.lamports, diff --git a/crates/snapshot/src/sidecar.rs b/crates/snapshot/src/sidecar.rs index 9feb0d2..368098d 100644 --- a/crates/snapshot/src/sidecar.rs +++ b/crates/snapshot/src/sidecar.rs @@ -374,9 +374,16 @@ pub async fn download_snapshot_file( Ok(()) } +#[derive(Debug, Clone, Copy)] +pub struct SnapshotBankInfo { + pub capitalization: u64, + pub slot: u64, +} + pub struct UnpackedSnapshot { pub account_files: Vec, pub stake_data: crate::stake_data::SnapshotStakeData, + pub bank_info: SnapshotBankInfo, } pub fn unpack_compressed_snapshot>( @@ -436,6 +443,10 @@ pub fn unpack_compressed_snapshot>( let stake_data = crate::stake_data::extract_stake_data(&bank_fields, &extra_fields.versioned_epoch_stakes); + let bank_info = SnapshotBankInfo { + capitalization: bank_fields.capitalization, + slot: bank_fields.slot, + }; tracing::info!( target: "unpack_compressed_snapshot", "Extracted stake data: epoch={}, voters={}, in_epoch_set={}", @@ -522,6 +533,7 @@ pub fn unpack_compressed_snapshot>( Ok(UnpackedSnapshot { account_files: account_file_data, stake_data, + bank_info, }) } diff --git a/example.cloudbreak.index.toml b/example.cloudbreak.index.toml index 56e8de9..f807257 100644 --- a/example.cloudbreak.index.toml +++ b/example.cloudbreak.index.toml @@ -1,6 +1,7 @@ # The buffer size for queuing finalize slot events finalize-slot-buffer-size = 1000 accounts-owner-map-enabled = false +supply-tracker-enabled = false [snapshot] accounts-file-concurency = 32 From ede109ac866596ceca662e751361216af681fcbe Mon Sep 17 00:00:00 2001 From: ArthurPaivaT Date: Wed, 22 Jul 2026 13:06:34 +0000 Subject: [PATCH 07/13] Implement getSupply supply tracker --- README.md | 4 +- crates/core/src/modules/account_owner_map.rs | 6 + crates/core/src/modules/supply_tracker.rs | 301 +++++++++++++------ crates/index/src/db_queries.rs | 164 ++++++++-- crates/index/src/indexer.rs | 4 + crates/index/src/metrics.rs | 15 +- crates/index/src/modules/non_circulating.rs | 25 +- crates/index/src/modules/save_block.rs | 155 +++++++++- crates/index/src/modules/self_healing.rs | 7 +- crates/snapshot/src/db_queries.rs | 148 ++++++++- crates/snapshot/src/lib.rs | 148 +++++++-- 11 files changed, 827 insertions(+), 150 deletions(-) diff --git a/README.md b/README.md index 2b0a808..9f9c85e 100644 --- a/README.md +++ b/README.md @@ -643,9 +643,9 @@ The transaction is executed read-only against the indexed account state at the r ### Supply (`getSupply`) -`getSupply` is optional and only served on a **full, unfiltered index** (empty `[programs]` include and exclude lists), with `supply-tracker-enabled = true` set on the indexer. The indexer anchors the total supply to the snapshot bank's `capitalization` on every (re)start, keeps an in-memory pubkey → lamports map seeded during the same snapshot ingest pass, and advances the total per confirmed block by summing per-account lamport deltas from the block's account updates. One `supply` row carrying the total and the non-circulating lamports is persisted per confirmed block (pruned to the last 128 slots), and a background recomputer derives the non-circulating set from Agave's pinned non-circulating account and withdraw-authority lists plus locked stake accounts, refreshing on the same converge-then-heartbeat cadence as the epoch-stakes recomputer. +`getSupply` is optional and only served on a **full, unfiltered index** (empty `[programs]` include and exclude lists), with `supply-tracker-enabled = true` set on the indexer. The indexer seeds the total supply from the snapshot bank's `capitalization` on every (re)start. While the snapshot is ingesting, accounts touched by live blocks are recorded in a small in-memory touched-account map; once the snapshot pass completes, those touches are reconciled against the startup-slot balances read back from the database and the tracker goes live. From then on it advances the total per confirmed block by reading each updated account's previous lamports from the database (owner-routed via the account owner map) and summing the resulting deltas — no full account→lamports map is kept in memory. One `supply` row carrying the total and the non-circulating lamports is persisted per confirmed block (pruned to the last 128 slots), and a background recomputer derives the non-circulating set from Agave's pinned non-circulating account and withdraw-authority lists plus locked stake accounts, refreshing on the same converge-then-heartbeat cadence as the epoch-stakes recomputer. -The API serves from a polled cache. Requests return a node-unhealthy error until the bootstrap pass has completed and the non-circulating set has been computed. On a detected slot gap the tracker marks itself stale (requests fail as node-unhealthy) and re-anchors from the in-memory map once the gap is repaired. For `finalized` commitment the response picks the newest persisted total at or below the finalized slot; `confirmed` returns the newest total. `context.slot` is the slot of the served row; the non-circulating account list may lag it by up to the recomputer heartbeat. +The API serves from a polled cache. Requests return a node-unhealthy error until the bootstrap pass has completed and the non-circulating set has been computed. On a detected slot gap the tracker keeps applying block deltas but stops publishing totals until every gap slot has been repaired (requests fail as node-unhealthy in the meantime); a failed account write or a failed delta query marks it stale, and it stays fail-closed until the indexer restarts. For `finalized` commitment the response picks the newest persisted total at or below the finalized slot; `confirmed` returns the newest total. `context.slot` is the slot of the served row; the non-circulating account list may lag it by up to the recomputer heartbeat. ### Snapshot on Indexer Startup diff --git a/crates/core/src/modules/account_owner_map.rs b/crates/core/src/modules/account_owner_map.rs index 5e555f5..9e09027 100644 --- a/crates/core/src/modules/account_owner_map.rs +++ b/crates/core/src/modules/account_owner_map.rs @@ -211,6 +211,12 @@ impl AccountOwnerMap { }) } + pub fn get_owner(&self, pubkey: &Pubkey) -> Option { + let accounts = self.accounts.as_ref()?; + let guard = accounts.read().expect("Failed to read accounts"); + guard.get(pubkey).map(|item| item.owner) + } + /// Checks if (in case this was a tracked account) the owner has changed since the last time /// it was seen pub fn check_updated_account_owner(&self, pubkey: Pubkey, owner: Pubkey, slot: u64) -> bool { diff --git a/crates/core/src/modules/supply_tracker.rs b/crates/core/src/modules/supply_tracker.rs index 23b64c0..425434b 100644 --- a/crates/core/src/modules/supply_tracker.rs +++ b/crates/core/src/modules/supply_tracker.rs @@ -1,23 +1,38 @@ use solana_pubkey::Pubkey; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, sync::{Arc, Mutex, RwLock}, }; pub const SUPPLY_RING_SLOTS: u64 = 128; #[derive(Clone, Copy, Debug)] -struct TrackedLamports { +struct MemberBalance { slot: u64, lamports: u64, } +#[derive(Clone, Copy, Debug)] +struct TouchedAccount { + slot: u64, + write_version: u64, + lamports: u64, +} + +#[derive(Clone, Debug)] +pub struct NonCirculatingBalance { + pub pubkey: Pubkey, + pub slot: u64, + pub lamports: u64, +} + #[derive(Clone, Default)] pub struct SupplyTracker(Option>); struct Inner { state: Mutex, - accounts: RwLock>, + non_circulating: RwLock, + block_writes: tokio::sync::Mutex<()>, } #[derive(Clone, Copy, Default, PartialEq)] @@ -25,18 +40,24 @@ enum SupplyStatus { #[default] Bootstrapping, Live, + GapFilling, Stale, - ReanchorPending, } #[derive(Default)] struct SupplyState { status: SupplyStatus, + bootstrap_failed: bool, total: u64, slot: u64, - anchor_slot: u64, - pending_delta: i128, - non_circulating_accounts: Option>, + startup_slot: u64, + startup_touched: HashMap, +} + +#[derive(Default)] +struct NonCirculatingState { + members: Option>, + balances: HashMap, } #[derive(Debug, Clone)] @@ -50,7 +71,8 @@ impl SupplyTracker { pub fn new() -> Self { Self(Some(Arc::new(Inner { state: Mutex::new(SupplyState::default()), - accounts: RwLock::new(HashMap::new()), + non_circulating: RwLock::new(NonCirculatingState::default()), + block_writes: tokio::sync::Mutex::new(()), }))) } @@ -58,77 +80,187 @@ impl SupplyTracker { self.0.is_some() } + pub fn is_tracking_deltas(&self) -> bool { + let Some(inner) = &self.0 else { return false }; + let state = inner.state.lock().expect("Failed to lock supply state"); + matches!(state.status, SupplyStatus::Live | SupplyStatus::GapFilling) + } + + pub async fn lock_block_writes(&self) -> Option> { + let inner = self.0.as_deref()?; + Some(inner.block_writes.lock().await) + } + pub fn observe_account(&self, pubkey: &[u8], slot: u64, lamports: u64) { let Some(inner) = &self.0 else { return }; let pubkey = Pubkey::try_from(pubkey).unwrap(); - let mut state = inner.state.lock().expect("Failed to lock supply state"); - state.slot = state.slot.max(slot); - - let mut map = inner - .accounts - .write() - .expect("Failed to write supply accounts"); - let previous_lamports = match map.get(&pubkey) { - Some(entry) if entry.slot > slot => return, - Some(entry) => entry.lamports, - None => 0, - }; - if lamports == 0 && state.status == SupplyStatus::Live { - map.remove(&pubkey); - } else { - map.insert(pubkey, TrackedLamports { slot, lamports }); + { + let non_circulating = inner + .non_circulating + .read() + .expect("Failed to read non-circulating state"); + let is_member = non_circulating + .members + .as_ref() + .is_some_and(|members| members.contains(&pubkey)); + if !is_member { + return; + } } - drop(map); - if state.status != SupplyStatus::Live { + let mut non_circulating = inner + .non_circulating + .write() + .expect("Failed to write non-circulating state"); + let is_member = non_circulating + .members + .as_ref() + .is_some_and(|members| members.contains(&pubkey)); + if !is_member { return; } - state.pending_delta += lamports as i128 - previous_lamports as i128; + match non_circulating.balances.get(&pubkey) { + Some(entry) if entry.slot > slot => {} + _ => { + non_circulating + .balances + .insert(pubkey, MemberBalance { slot, lamports }); + } + } } - pub fn observe_snapshot_account(&self, pubkey: &Pubkey, slot: u64, lamports: u64) { + pub fn set_non_circulating_accounts( + &self, + accounts: Vec, + balances: Vec, + ) { let Some(inner) = &self.0 else { return }; - let mut map = inner - .accounts + let members: HashSet = accounts.into_iter().collect(); + let mut non_circulating = inner + .non_circulating .write() - .expect("Failed to write supply accounts"); - match map.get(pubkey) { - Some(entry) if entry.slot >= slot => {} - _ => { - map.insert(*pubkey, TrackedLamports { slot, lamports }); + .expect("Failed to write non-circulating state"); + non_circulating + .balances + .retain(|pubkey, _| members.contains(pubkey)); + for balance in balances { + if !members.contains(&balance.pubkey) { + continue; + } + match non_circulating.balances.get(&balance.pubkey) { + Some(entry) if entry.slot >= balance.slot => {} + _ => { + non_circulating.balances.insert( + balance.pubkey, + MemberBalance { + slot: balance.slot, + lamports: balance.lamports, + }, + ); + } } } + non_circulating.members = Some(members); } - pub fn set_non_circulating_accounts(&self, accounts: Vec) { + pub fn set_startup_total(&self, slot: u64, capitalization: u64) { let Some(inner) = &self.0 else { return }; - inner - .state - .lock() - .expect("Failed to lock supply state") - .non_circulating_accounts = Some(accounts); + let mut state = inner.state.lock().expect("Failed to lock supply state"); + if state.status == SupplyStatus::Bootstrapping && slot > state.startup_slot { + state.startup_slot = slot; + state.total = capitalization; + } } - pub fn note_anchor(&self, slot: u64) { + pub fn startup_slot(&self) -> Option { + let inner = self.0.as_deref()?; + let state = inner.state.lock().expect("Failed to lock supply state"); + (state.status == SupplyStatus::Bootstrapping && state.startup_slot > 0) + .then_some(state.startup_slot) + } + + pub fn record_startup_touches( + &self, + slot: u64, + touches: impl IntoIterator, + ) { let Some(inner) = &self.0 else { return }; let mut state = inner.state.lock().expect("Failed to lock supply state"); - state.anchor_slot = state.anchor_slot.max(slot); + if state.status != SupplyStatus::Bootstrapping { + return; + } + for (pubkey, lamports, write_version) in touches { + let account = TouchedAccount { + slot, + write_version, + lamports, + }; + let entry = state.startup_touched.entry(pubkey).or_insert(account); + if (slot, write_version) > (entry.slot, entry.write_version) { + *entry = account; + } + } } - pub fn finish_bootstrap(&self) -> Option { + pub fn startup_touched_pubkeys(&self) -> Vec { + let Some(inner) = &self.0 else { + return Vec::new(); + }; + let state = inner.state.lock().expect("Failed to lock supply state"); + state.startup_touched.keys().copied().collect() + } + + pub fn mark_bootstrap_failed(&self) -> bool { + let Some(inner) = &self.0 else { + return false; + }; + let mut state = inner.state.lock().expect("Failed to lock supply state"); + if state.status != SupplyStatus::Bootstrapping || state.bootstrap_failed { + return false; + } + state.bootstrap_failed = true; + true + } + + pub fn bootstrap_failed(&self) -> bool { + let Some(inner) = &self.0 else { + return false; + }; + let state = inner.state.lock().expect("Failed to lock supply state"); + state.bootstrap_failed + } + + pub fn finish_bootstrap( + &self, + startup_balances: &HashMap, + ) -> Option { let inner = self.0.as_deref()?; let mut state = inner.state.lock().expect("Failed to lock supply state"); - if state.status != SupplyStatus::Bootstrapping || state.anchor_slot == 0 { + if state.status != SupplyStatus::Bootstrapping + || state.startup_slot == 0 + || state.bootstrap_failed + { return None; } - state.total = inner.sweep_and_sum(); - state.slot = state.slot.max(state.anchor_slot); + let startup_slot = state.startup_slot; + let mut window_delta: i128 = 0; + let mut max_slot = startup_slot; + for (pubkey, account) in &state.startup_touched { + if account.slot <= startup_slot { + continue; + } + let balance = *startup_balances.get(pubkey)?; + window_delta += account.lamports as i128 - balance as i128; + max_slot = max_slot.max(account.slot); + } + state.total = (state.total as i128 + window_delta) as u64; + state.slot = state.slot.max(max_slot); + state.startup_touched = HashMap::new(); state.status = SupplyStatus::Live; - state.pending_delta = 0; Some(inner.commit(&state)) } - pub fn mark_stale(&self) -> bool { + pub fn mark_gap(&self) -> bool { let Some(inner) = &self.0 else { return false; }; @@ -136,35 +268,48 @@ impl SupplyTracker { if state.status != SupplyStatus::Live { return false; } - state.status = SupplyStatus::Stale; - state.pending_delta = 0; + state.status = SupplyStatus::GapFilling; true } - pub fn request_reanchor(&self) { + pub fn finish_gap(&self) { let Some(inner) = &self.0 else { return }; let mut state = inner.state.lock().expect("Failed to lock supply state"); - if state.status == SupplyStatus::Stale { - state.status = SupplyStatus::ReanchorPending; + if state.status == SupplyStatus::GapFilling { + state.status = SupplyStatus::Live; } } - pub fn commit_block(&self, slot: u64) -> Option { + pub fn mark_stale(&self) -> bool { + let Some(inner) = &self.0 else { + return false; + }; + let mut state = inner.state.lock().expect("Failed to lock supply state"); + if !matches!(state.status, SupplyStatus::Live | SupplyStatus::GapFilling) { + return false; + } + state.status = SupplyStatus::Stale; + true + } + + pub fn commit_block(&self, slot: u64, block_delta: i128) -> Option { let inner = self.0.as_deref()?; let mut state = inner.state.lock().expect("Failed to lock supply state"); state.slot = state.slot.max(slot); + if slot <= state.startup_slot { + return None; + } match state.status { - SupplyStatus::Bootstrapping | SupplyStatus::Stale => return None, - SupplyStatus::ReanchorPending => { - state.total = inner.sweep_and_sum(); - state.status = SupplyStatus::Live; + SupplyStatus::Bootstrapping | SupplyStatus::Stale => None, + SupplyStatus::GapFilling => { + state.total = (state.total as i128 + block_delta) as u64; + None } SupplyStatus::Live => { - state.total = (state.total as i128 + state.pending_delta) as u64; + state.total = (state.total as i128 + block_delta) as u64; + Some(inner.commit(&state)) } } - state.pending_delta = 0; - Some(inner.commit(&state)) } } @@ -173,33 +318,21 @@ impl Inner { SupplyCommit { slot: state.slot, total: state.total, - non_circulating: self.sum_non_circulating(state), + non_circulating: self.sum_non_circulating(), } } - fn sum_non_circulating(&self, state: &SupplyState) -> Option { - let accounts = state.non_circulating_accounts.as_ref()?; - let map = self - .accounts + fn sum_non_circulating(&self) -> Option { + let non_circulating = self + .non_circulating .read() - .expect("Failed to read supply accounts"); - let lamports: u128 = accounts - .iter() - .map(|pubkey| map.get(pubkey).map_or(0, |entry| entry.lamports as u128)) + .expect("Failed to read non-circulating state"); + non_circulating.members.as_ref()?; + let lamports: u128 = non_circulating + .balances + .values() + .map(|balance| balance.lamports as u128) .sum(); Some(lamports as u64) } - - fn sweep_and_sum(&self) -> u64 { - let mut map = self - .accounts - .write() - .expect("Failed to write supply accounts"); - let mut total: u128 = 0; - map.retain(|_, entry| { - total += entry.lamports as u128; - entry.lamports > 0 - }); - total as u64 - } } diff --git a/crates/index/src/db_queries.rs b/crates/index/src/db_queries.rs index b4e1b10..442e9fa 100644 --- a/crates/index/src/db_queries.rs +++ b/crates/index/src/db_queries.rs @@ -8,14 +8,14 @@ use std::{ time::Duration, }; -use rust_decimal::Decimal; +use rust_decimal::{Decimal, prelude::ToPrimitive}; use solana_pubkey::Pubkey; use sea_orm::{ ActiveValue::{NotSet, Set}, ColumnTrait, Condition, ConnectionTrait, DatabaseBackend, DatabaseConnection, EntityTrait, QueryFilter, Statement, Value, prelude::Expr, - sea_query::{Alias, OnConflict}, + sea_query::{Alias, ArrayType, OnConflict}, }; use tokio::{ task::JoinHandle, @@ -26,10 +26,11 @@ use cloudbreak_core::{ IndexConfig, modules::{ account_owner_map::AccountOwnerMap, - supply_tracker::{SUPPLY_RING_SLOTS, SupplyCommit}, + supply_tracker::{NonCirculatingBalance, SUPPLY_RING_SLOTS, SupplyCommit}, }, }; use cloudbreak_entity::{accounts, service_health, slots}; +use cloudbreak_snapshot::{bytea_array, owner_pubkey_arrays, parse_pubkey}; use crate::metrics; @@ -89,13 +90,14 @@ pub fn insert_closed_accounts( slot: u64, config: &IndexConfig, accounts_owner_map: AccountOwnerMap, -) -> Option> { +) -> Option> { let query_timeout = Duration::from_secs(config.database.save_block_queries_timeout); let handle = tokio::spawn(async move { let _guard = metrics::TokioTaskCounterGuard::new("insert_closed_accounts"); let start_time = Instant::now(); + let mut inserted = true; if accounts_owner_map.is_enabled() { let result = accounts_owner_map.save_closed_accounts(pubkeys, slot).await; @@ -106,6 +108,7 @@ pub fn insert_closed_accounts( Err(e) => { tracing::error!(target: "save_closed_accounts_with_map", "failed to save closed accounts with map: {}", e); metrics::increment_db_errors(); + inserted = false; } } } else { @@ -157,6 +160,7 @@ pub fn insert_closed_accounts( e ); metrics::increment_db_errors(); + inserted = false; } } } @@ -164,6 +168,8 @@ pub fn insert_closed_accounts( metrics::INSERT_CLOSED_ACCOUNTS_PER_SLOT_HISTOGRAM .observe(start_time.elapsed().as_micros() as f64 / 1000.0); + + inserted }); Some(handle) @@ -446,7 +452,7 @@ pub async fn upsert_supply_row(db: &DatabaseConnection, commit: &SupplyCommit, c if let Err(e) = result { tracing::error!("upsert_supply_row failed for slot {}: {}", commit.slot, e); - metrics::increment_db_errors(); + metrics::SUPPLY_QUERY_ERRORS.inc(); } } @@ -455,10 +461,6 @@ pub async fn upsert_non_circulating_accounts( slot: u64, accounts: &[Pubkey], ) { - let accounts: Vec = accounts - .iter() - .map(|p| Value::Bytes(Some(Box::new(p.to_bytes().to_vec())))) - .collect(); let result = db .execute(Statement::from_sql_and_values( DatabaseBackend::Postgres, @@ -470,17 +472,137 @@ pub async fn upsert_non_circulating_accounts( updated_at = now()", [ Value::from(slot as i64), - Value::Array( - sea_orm::sea_query::ArrayType::Bytes, - Some(Box::new(accounts)), - ), + bytea_array(accounts.iter().map(|p| p.to_bytes().to_vec()).collect()), ], )) .await; if let Err(e) = result { tracing::error!("upsert_non_circulating_accounts failed for slot {}: {}", slot, e); - metrics::increment_db_errors(); + metrics::SUPPLY_QUERY_ERRORS.inc(); + } +} + +#[derive(Debug, Clone, Copy, Default)] +pub struct BlockSupplyDelta { + pub block_delta: i128, + pub routed_misses: u64, +} + +fn numeric_array(items: Vec) -> Value { + Value::Array( + ArrayType::Decimal, + Some(Box::new( + items + .into_iter() + .map(|value| Value::Decimal(Some(Box::new(Decimal::from(value))))) + .collect(), + )), + ) +} + +pub async fn fetch_block_supply_delta( + db: &DatabaseConnection, + owners: Vec>, + pubkeys: Vec>, + new_lamports: Vec, + slot: u64, + config: &IndexConfig, +) -> Result { + let query_timeout = Duration::from_secs(config.database.save_block_queries_timeout); + + let query = db.query_one(Statement::from_sql_and_values( + DatabaseBackend::Postgres, + r#" + SELECT + COALESCE(SUM(v.new_lamports - COALESCE(prev.lamports, 0)) + FILTER (WHERE prev.slot IS NULL OR prev.slot < $4), 0) AS block_delta, + COUNT(*) FILTER (WHERE prev.slot IS NULL) AS routed_misses + FROM unnest($1::bytea[], $2::bytea[], $3::numeric[]) AS v(owner, pubkey, new_lamports) + LEFT JOIN LATERAL ( + SELECT lamports, slot FROM ( + SELECT lamports, slot FROM accounts + WHERE owner = v.owner AND pubkey = v.pubkey + UNION ALL + SELECT lamports, slot FROM snapshot_accounts + WHERE owner = v.owner AND pubkey = v.pubkey + ) u + ORDER BY slot DESC + LIMIT 1 + ) prev ON true + "#, + [ + bytea_array(owners), + bytea_array(pubkeys), + numeric_array(new_lamports), + Value::BigInt(Some(slot as i64)), + ], + )); + + let row = timeout(query_timeout, query) + .await + .map_err(|elapsed| { + sea_orm::DbErr::Custom(format!("fetch_block_supply_delta timeout: {}", elapsed)) + })?? + .ok_or_else(|| { + sea_orm::DbErr::Custom("fetch_block_supply_delta returned no row".to_string()) + })?; + + let block_delta: Decimal = row.try_get("", "block_delta")?; + let routed_misses: i64 = row.try_get("", "routed_misses")?; + let block_delta = block_delta.to_i128().ok_or_else(|| { + sea_orm::DbErr::Custom(format!("block_delta {} does not fit in i128", block_delta)) + })?; + + Ok(BlockSupplyDelta { + block_delta, + routed_misses: routed_misses as u64, + }) +} + +pub async fn fetch_non_circulating_balances( + db: &DatabaseConnection, + members: &[(Pubkey, Pubkey)], +) -> Result, sea_orm::DbErr> { + if members.is_empty() { + return Ok(Vec::new()); } + + let (owners, pubkeys) = owner_pubkey_arrays(members); + + let rows = db + .query_all(Statement::from_sql_and_values( + DatabaseBackend::Postgres, + r#" + SELECT v.pubkey, latest.lamports, latest.slot + FROM unnest($1::bytea[], $2::bytea[]) AS v(owner, pubkey) + JOIN LATERAL ( + SELECT lamports, slot FROM ( + SELECT lamports, slot FROM accounts + WHERE owner = v.owner AND pubkey = v.pubkey + UNION ALL + SELECT lamports, slot FROM snapshot_accounts + WHERE owner = v.owner AND pubkey = v.pubkey + ) u + ORDER BY slot DESC + LIMIT 1 + ) latest ON true + "#, + [owners, pubkeys], + )) + .await?; + + rows.into_iter() + .map(|row| { + let pubkey = parse_pubkey(row.try_get("", "pubkey")?)?; + let lamports: i64 = row.try_get("", "lamports")?; + let slot: i64 = row.try_get("", "slot")?; + Ok(NonCirculatingBalance { + pubkey, + slot: slot as u64, + lamports: lamports as u64, + }) + }) + .collect() } /// The latest persisted slot for each commitment level, plus the finalized→confirmed lag. @@ -533,7 +655,7 @@ pub async fn insert_accounts_chunk( chunk: Vec, byte_size: usize, config: &IndexConfig, -) { +) -> bool { let query_timeout = Duration::from_secs(config.database.save_block_queries_timeout); let start_time = Instant::now(); @@ -550,17 +672,23 @@ pub async fn insert_accounts_chunk( Err(sea_orm::DbErr::RecordNotInserted) }); - match result { - Ok(res) => tracing::debug!("upsert_accounts_batched: {}", res), + let inserted = match result { + Ok(res) => { + tracing::debug!("upsert_accounts_batched: {}", res); + true + } Err(e) => { tracing::error!("upsert_accounts_batched ERROR: {}", e); metrics::increment_db_errors(); + false } - } + }; let elapsed = start_time.elapsed().as_secs_f64(); if elapsed > 0.250 { tracing::debug!(target: "slow_chunk", "slow chunk: len: {}, size: {}", chunk_len, byte_size); } metrics::record_chunk_processing(elapsed, "block"); + + inserted } diff --git a/crates/index/src/indexer.rs b/crates/index/src/indexer.rs index 6f76d57..ad1f68b 100644 --- a/crates/index/src/indexer.rs +++ b/crates/index/src/indexer.rs @@ -95,6 +95,9 @@ pub async fn run(config: &str) -> CloudbreakResult<()> { if !config.programs.supports_simulation() { panic!("supply-tracker-enabled requires an empty [programs] filter"); } + if !config.accounts_owner_map_enabled { + panic!("supply-tracker-enabled requires accounts-owner-map-enabled"); + } SupplyTracker::new() } else { SupplyTracker::default() @@ -169,6 +172,7 @@ pub async fn run(config: &str) -> CloudbreakResult<()> { let _non_circulating_handle = modules::non_circulating::spawn_non_circulating_recomputer( db.clone(), indexer_state.supply_tracker.clone(), + indexer_state.accounts_owner_map.clone(), ); operational_endpoints::self_healing::SELF_HEALING diff --git a/crates/index/src/metrics.rs b/crates/index/src/metrics.rs index baea68f..3a287d5 100644 --- a/crates/index/src/metrics.rs +++ b/crates/index/src/metrics.rs @@ -7,7 +7,8 @@ use std::sync::{Once, OnceLock}; use cloudbreak_core::IndexConfig; use prometheus::{ - Counter, Histogram, HistogramOpts, HistogramVec, IntGauge, IntGaugeVec, Opts, Registry, + Counter, Histogram, HistogramOpts, HistogramVec, IntCounter, IntGauge, IntGaugeVec, Opts, + Registry, }; use tracing::error; @@ -293,6 +294,16 @@ lazy_static::lazy_static! { "cloudbreak_supply_stale", "Whether the supply tracker is stale (1) or fresh (0)" ) .expect("Failed to create supply stale gauge"); + + pub static ref SUPPLY_ROUTED_MISSES: IntCounter = IntCounter::new( + "cloudbreak_supply_routed_misses", "Owner-routed supply prev-reads that found no previous row" + ) + .expect("Failed to create supply routed misses counter"); + + pub static ref SUPPLY_QUERY_ERRORS: IntCounter = IntCounter::new( + "cloudbreak_supply_query_errors", "Supply prev-read delta queries that failed or timed out" + ) + .expect("Failed to create supply query errors counter"); } /// We use a guard to increment the current tokio tasks metric when a task is created and @@ -432,5 +443,7 @@ pub fn register_collectors() { register!(SUPPLY_TOTAL_LAMPORTS); register!(SUPPLY_SLOT); register!(SUPPLY_STALE); + register!(SUPPLY_ROUTED_MISSES); + register!(SUPPLY_QUERY_ERRORS); }); } diff --git a/crates/index/src/modules/non_circulating.rs b/crates/index/src/modules/non_circulating.rs index 585f3fc..4e2639b 100644 --- a/crates/index/src/modules/non_circulating.rs +++ b/crates/index/src/modules/non_circulating.rs @@ -2,6 +2,7 @@ use std::collections::HashSet; use std::time::Duration; use cloudbreak_core::STAKE_PROGRAM_ID; +use cloudbreak_core::modules::account_owner_map::AccountOwnerMap; use cloudbreak_core::modules::supply_tracker::SupplyTracker; use futures::TryStreamExt; use sea_orm::{ConnectionTrait, DatabaseBackend, DatabaseConnection, Statement, StreamTrait}; @@ -26,6 +27,7 @@ const CLOCK_SYSVAR_ID: Pubkey = pub fn spawn_non_circulating_recomputer( db: DatabaseConnection, supply_tracker: SupplyTracker, + accounts_owner_map: AccountOwnerMap, ) -> JoinHandle<()> { tokio::spawn(async move { let _guard = metrics::TokioTaskCounterGuard::new("non_circulating_recomputer"); @@ -65,10 +67,31 @@ pub fn spawn_non_circulating_recomputer( continue; } }; + let members: Vec<(Pubkey, Pubkey)> = accounts + .iter() + .filter_map(|pubkey| { + accounts_owner_map + .get_owner(pubkey) + .map(|owner| (owner, *pubkey)) + }) + .collect(); + let balances = + match crate::db_queries::fetch_non_circulating_balances(&db, &members).await { + Ok(balances) => balances, + Err(e) => { + tracing::error!( + target: "non_circulating_recomputer", + "failed to fetch non-circulating balances: {:?}", + e + ); + continue; + } + }; + last_recompute = Some(Instant::now()); next_lockup_expiry = next_expiry; crate::db_queries::upsert_non_circulating_accounts(&db, slot, &accounts).await; - supply_tracker.set_non_circulating_accounts(accounts); + supply_tracker.set_non_circulating_accounts(accounts, balances); } }) } diff --git a/crates/index/src/modules/save_block.rs b/crates/index/src/modules/save_block.rs index f00f795..8cf9a57 100644 --- a/crates/index/src/modules/save_block.rs +++ b/crates/index/src/modules/save_block.rs @@ -8,19 +8,26 @@ use sea_orm::{ DatabaseConnection, }; use solana_pubkey::Pubkey; +use std::collections::{HashMap, hash_map::Entry}; use tokio::{ task::{JoinHandle, JoinSet}, time::Instant, }; use yellowstone_grpc_proto::geyser::CommitmentLevel; use yellowstone_grpc_proto::geyser::SubscribeUpdateBlock; -use cloudbreak_core::IndexConfig; +use cloudbreak_core::{IndexConfig, modules::supply_tracker::SupplyTracker}; use cloudbreak_entity::accounts; use crate::indexer::{AccountsReceivedPerBlock, IndexerState}; use crate::modules::snapshot::SnapshotProcessingState; use crate::{db_queries, metrics, modules}; +struct PendingSupplyAccount { + owner: Option, + lamports: u64, + write_version: u64, +} + /// Splits the block into chunks and saves them into the "accounts" table /// Also updates the HashMap with the accounts pubkeys that were updated in the slot pub async fn save_block( @@ -79,11 +86,33 @@ pub async fn save_block( .map(|pubkey| pubkey.0.to_bytes().to_vec()) .collect::>(); + let supply_enabled = supply_tracker.is_enabled(); + let mut pending_supply_accounts: HashMap = HashMap::new(); + // Create the chunks for updating the "accounts" table let system_program_id = [0u8; 32].to_vec(); for account in block.accounts { supply_tracker.observe_account(&account.pubkey, slot, account.lamports); + if supply_enabled { + let pubkey = Pubkey::try_from(account.pubkey.as_slice()).unwrap(); + match pending_supply_accounts.entry(pubkey) { + Entry::Vacant(entry) => { + entry.insert(PendingSupplyAccount { + owner: accounts_owner_map.get_owner(&pubkey), + lamports: account.lamports, + write_version: account.write_version, + }); + } + Entry::Occupied(mut entry) if account.write_version > entry.get().write_version => { + let pending = entry.get_mut(); + pending.lamports = account.lamports; + pending.write_version = account.write_version; + } + Entry::Occupied(_) => {} + } + } + // If the account is being closed we still add it to the hashmap for cleanup // but we don't add it to the "accounts" table in a normal fashion, instead we added using [`db_queries::insert_closed_accounts`] if account.lamports == 0 { @@ -164,6 +193,24 @@ pub async fn save_block( chunks.push((current_chunk, current_chunk_bytes)); } + let supply_write_guard = if supply_enabled { + supply_tracker.lock_block_writes().await + } else { + None + }; + let block_supply_delta = if supply_tracker.is_tracking_deltas() { + compute_block_supply_delta(db, &config, &supply_tracker, pending_supply_accounts, slot) + .await + } else { + supply_tracker.record_startup_touches( + slot, + pending_supply_accounts + .into_iter() + .map(|(pubkey, pending)| (pubkey, pending.lamports, pending.write_version)), + ); + None + }; + let closed_account_for_slot_len = closed_accounts_for_slot.len(); // We delay the closed accounts insertion until the snapshot is processed to avoid reads while @@ -174,7 +221,7 @@ pub async fn save_block( .expect("Failed to lock snapshot_processing_state") }; - let closed_accounts_insert_handle: Option> = if snapshot_processing_state + let closed_accounts_insert_handle: Option> = if snapshot_processing_state == SnapshotProcessingState::Finished || snapshot_processing_state == SnapshotProcessingState::FinishedAndCleanedUp { @@ -219,18 +266,32 @@ pub async fn save_block( tasks.spawn(async move { let _guard = metrics::TokioTaskCounterGuard::new("insert_accounts_chunk"); - db_queries::insert_accounts_chunk(&db, chunk, byte_size, &config_clone).await; + db_queries::insert_accounts_chunk(&db, chunk, byte_size, &config_clone).await }); } - tasks.join_all().await; + let mut block_writes_ok = tasks.join_all().await.into_iter().all(|inserted| inserted); - if let Some(handle) = closed_accounts_insert_handle - && let Err(e) = handle.await - { - tracing::error!(target: "save_block_closed_accounts_insert", "failed to insert closed accounts: {:?}", e); + if let Some(handle) = closed_accounts_insert_handle { + match handle.await { + Ok(inserted) => block_writes_ok &= inserted, + Err(e) => { + tracing::error!(target: "save_block_closed_accounts_insert", "failed to insert closed accounts: {:?}", e); + block_writes_ok = false; + } + } } + if block_supply_delta.is_none() && !block_writes_ok && supply_tracker.mark_bootstrap_failed() { + tracing::error!( + target: "supply_tracker", + "account writes failed for slot {} during supply bootstrap, marking bootstrap failed", + slot + ); + } + + drop(supply_write_guard); + // Wait until the chunk processing is finished to insert the slot (this ensures that gPA calls can only read from completed slots) db_queries::insert_slot( slot, @@ -250,13 +311,81 @@ pub async fn save_block( ) .await; - if let Some(commit) = supply_tracker.commit_block(slot) { - db_queries::upsert_supply_row(db, &commit, &config).await; - metrics::SUPPLY_TOTAL_LAMPORTS.set(commit.total as i64); - metrics::SUPPLY_SLOT.set(commit.slot as i64); - metrics::SUPPLY_STALE.set(0); + if let Some(block_delta) = block_supply_delta { + if !block_writes_ok { + tracing::error!( + target: "supply_tracker", + "account writes failed for slot {}, marking supply stale", + slot + ); + if supply_tracker.mark_stale() { + metrics::SUPPLY_STALE.set(1); + } + } else if let Some(commit) = supply_tracker.commit_block(slot, block_delta) { + db_queries::upsert_supply_row(db, &commit, &config).await; + metrics::SUPPLY_TOTAL_LAMPORTS.set(commit.total as i64); + metrics::SUPPLY_SLOT.set(commit.slot as i64); + metrics::SUPPLY_STALE.set(0); + } } let elapsed = start_time.elapsed().as_secs_f64(); metrics::record_block_processing(elapsed, "block"); } + +async fn compute_block_supply_delta( + db: &DatabaseConnection, + config: &IndexConfig, + supply_tracker: &SupplyTracker, + pending_accounts: HashMap, + slot: u64, +) -> Option { + let mut block_delta: i128 = 0; + let mut owners = Vec::new(); + let mut pubkeys = Vec::new(); + let mut new_lamports = Vec::new(); + for (pubkey, pending) in pending_accounts { + match pending.owner { + Some(owner) => { + owners.push(owner.to_bytes().to_vec()); + pubkeys.push(pubkey.to_bytes().to_vec()); + new_lamports.push(pending.lamports); + } + None => block_delta += pending.lamports as i128, + } + } + + if pubkeys.is_empty() { + return Some(block_delta); + } + + match db_queries::fetch_block_supply_delta(db, owners, pubkeys, new_lamports, slot, config) + .await + { + Ok(result) => { + if result.routed_misses > 0 { + metrics::SUPPLY_ROUTED_MISSES.inc_by(result.routed_misses); + tracing::warn!( + target: "supply_tracker", + "owner-routed prev-read found no previous row for {} accounts in slot {}", + result.routed_misses, + slot + ); + } + Some(block_delta + result.block_delta) + } + Err(e) => { + metrics::SUPPLY_QUERY_ERRORS.inc(); + tracing::error!( + target: "supply_tracker", + "block supply delta query failed for slot {}, marking supply stale: {}", + slot, + e + ); + if supply_tracker.mark_stale() { + metrics::SUPPLY_STALE.set(1); + } + None + } + } +} diff --git a/crates/index/src/modules/self_healing.rs b/crates/index/src/modules/self_healing.rs index bcf7472..895203a 100644 --- a/crates/index/src/modules/self_healing.rs +++ b/crates/index/src/modules/self_healing.rs @@ -137,7 +137,7 @@ impl SelfHealingState { // unsupported and `fill_gaps` fails fast in that case. self.finalizer.pause().await; - if self.supply_tracker.mark_stale() { + if self.supply_tracker.mark_gap() { metrics::SUPPLY_STALE.set(1); } } @@ -296,7 +296,6 @@ impl SelfHealingState { snapshot_pair, snapshot_config, covered_gaps_list.clone(), - self.supply_tracker.clone(), ) { Ok((handle, rx)) => (handle, rx), Err(e) => { @@ -387,7 +386,7 @@ impl SelfHealingState { .expect("Failed to lock gaps_list") .is_empty(); if !gaps_remaining { - self.supply_tracker.request_reanchor(); + self.supply_tracker.finish_gap(); self.finalizer.resume().await; } } @@ -463,7 +462,6 @@ fn download_and_process_snapshot_for_gap_filling( snapshot_pair: SnapshotPair, config: SnapshotConfig, gaps_list: Vec, - supply_tracker: SupplyTracker, ) -> Result< ( JoinHandle>, @@ -499,7 +497,6 @@ fn download_and_process_snapshot_for_gap_filling( config, gaps_list, tx, - supply_tracker, ) .await?; diff --git a/crates/snapshot/src/db_queries.rs b/crates/snapshot/src/db_queries.rs index 7a90fc7..751c7d3 100644 --- a/crates/snapshot/src/db_queries.rs +++ b/crates/snapshot/src/db_queries.rs @@ -12,9 +12,10 @@ use std::{ use rust_decimal::Decimal; use sea_orm::{ ActiveValue::Set, ConnectionTrait, DatabaseConnection, EntityTrait, Statement, - TransactionTrait, Value, + TransactionTrait, Value, sea_query::ArrayType, }; -use tokio::time::Instant; +use solana_pubkey::Pubkey; +use tokio::time::{Instant, timeout}; use yellowstone_grpc_proto::geyser::SubscribeUpdateAccount; use cloudbreak_core::{ SnapshotPgIndexesConfig, @@ -586,6 +587,149 @@ pub async fn persist_supply_seed( Ok(()) } +const STARTUP_BALANCES_QUERY_TIMEOUT: Duration = Duration::from_secs(60); + +pub fn bytea_array(items: Vec>) -> Value { + Value::Array( + ArrayType::Bytes, + Some(Box::new( + items + .into_iter() + .map(|bytes| Value::Bytes(Some(Box::new(bytes)))) + .collect(), + )), + ) +} + +pub fn parse_pubkey(bytes: Vec) -> Result { + Pubkey::try_from(bytes.as_slice()) + .map_err(|_| sea_orm::DbErr::Custom("invalid pubkey bytes in query result".to_string())) +} + +pub fn owner_pubkey_arrays(pairs: &[(Pubkey, Pubkey)]) -> (Value, Value) { + let owners = pairs + .iter() + .map(|(owner, _)| owner.to_bytes().to_vec()) + .collect(); + let pubkeys = pairs + .iter() + .map(|(_, pubkey)| pubkey.to_bytes().to_vec()) + .collect(); + (bytea_array(owners), bytea_array(pubkeys)) +} + +pub struct StartupBalanceProbe { + pub resolved: Vec<(Pubkey, u64)>, + pub misses: Vec, +} + +pub async fn fetch_startup_balances_by_owner( + db: &DatabaseConnection, + entries: &[(Pubkey, Pubkey)], + startup_slot: u64, +) -> Result { + let mut probe = StartupBalanceProbe { + resolved: Vec::new(), + misses: Vec::new(), + }; + if entries.is_empty() { + return Ok(probe); + } + + let (owners, pubkeys) = owner_pubkey_arrays(entries); + + let query = db.query_all(Statement::from_sql_and_values( + sea_orm::DatabaseBackend::Postgres, + r#" + SELECT v.pubkey, prev.lamports + FROM unnest($1::bytea[], $2::bytea[]) AS v(owner, pubkey) + LEFT JOIN LATERAL ( + SELECT lamports FROM snapshot_accounts + WHERE owner = v.owner AND pubkey = v.pubkey AND slot <= $3 + ORDER BY slot DESC + LIMIT 1 + ) prev ON true + "#, + [owners, pubkeys, Value::BigInt(Some(startup_slot as i64))], + )); + + let rows = timeout(STARTUP_BALANCES_QUERY_TIMEOUT, query) + .await + .map_err(|elapsed| { + sea_orm::DbErr::Custom(format!( + "fetch_startup_balances_by_owner timeout: {}", + elapsed + )) + })??; + + for row in rows { + let pubkey = parse_pubkey(row.try_get("", "pubkey")?)?; + let lamports: Option = row.try_get("", "lamports")?; + match lamports { + Some(lamports) => probe.resolved.push((pubkey, lamports as u64)), + None => probe.misses.push(pubkey), + } + } + + Ok(probe) +} + +pub async fn fetch_startup_balances_from_versions( + db: &DatabaseConnection, + pubkeys: &[Pubkey], + startup_slot: u64, +) -> Result, sea_orm::DbErr> { + if pubkeys.is_empty() { + return Ok(Vec::new()); + } + + let pubkey_values = pubkeys + .iter() + .map(|pubkey| pubkey.to_bytes().to_vec()) + .collect(); + + let query = db.query_all(Statement::from_sql_and_values( + sea_orm::DatabaseBackend::Postgres, + r#" + SELECT v.pubkey, COALESCE(prev.lamports, 0) AS lamports + FROM unnest($1::bytea[]) AS v(pubkey) + LEFT JOIN LATERAL ( + SELECT owner FROM temp_snapshot_account_versions + WHERE pubkey = v.pubkey AND slot <= $2 + ORDER BY slot DESC + LIMIT 1 + ) version ON true + LEFT JOIN LATERAL ( + SELECT lamports FROM snapshot_accounts + WHERE owner = version.owner AND pubkey = v.pubkey AND slot <= $2 + ORDER BY slot DESC + LIMIT 1 + ) prev ON true + "#, + [ + bytea_array(pubkey_values), + Value::BigInt(Some(startup_slot as i64)), + ], + )); + + let rows = timeout(STARTUP_BALANCES_QUERY_TIMEOUT, query) + .await + .map_err(|elapsed| { + sea_orm::DbErr::Custom(format!( + "fetch_startup_balances_from_versions timeout: {}", + elapsed + )) + })??; + + rows.into_iter() + .map(|row| { + let pubkey = parse_pubkey(row.try_get("", "pubkey")?)?; + let lamports: i64 = row.try_get("", "lamports")?; + Ok((pubkey, lamports as u64)) + }) + .collect() +} + pub async fn persist_supply_total( db: &DatabaseConnection, commit: &SupplyCommit, diff --git a/crates/snapshot/src/lib.rs b/crates/snapshot/src/lib.rs index d312119..024d82e 100644 --- a/crates/snapshot/src/lib.rs +++ b/crates/snapshot/src/lib.rs @@ -5,7 +5,9 @@ use sea_orm::{ConnectOptions, Database, DatabaseConnection}; use solana_accounts_db::accounts_file::AccountsFile; +use solana_pubkey::Pubkey; use std::{ + collections::HashMap, path::PathBuf, sync::{Arc, Mutex}, }; @@ -31,7 +33,7 @@ pub mod metrics; pub mod sidecar; pub mod stake_data; -pub use db_queries::persist_epoch_stakes; +pub use db_queries::{bytea_array, owner_pubkey_arrays, parse_pubkey, persist_epoch_stakes}; const DB_ACCOUNTS_BATCH_SIZE: usize = 200; @@ -97,12 +99,6 @@ pub async fn run( tracing::info!(target: "full_snapshot_completed", "Full snapshot completed successfully in {} secs", start_time.elapsed().as_secs_f64()); - if let Some(commit) = supply_tracker.finish_bootstrap() - && let Err(e) = db_queries::persist_supply_total(&database, &commit).await - { - tracing::error!("Failed to persist bootstrapped supply total: {:?}", e); - } - if let Some(buffer_size) = buffer_size { db_queries::cluster_snapshot_accounts_table( &database, @@ -116,6 +112,8 @@ pub async fn run( db_queries::clean_up_closed_accounts(&database).await?; db_queries::create_database_indexes(&database, &config.pg_indexes).await?; + finish_supply_bootstrap(&database, &accounts_owner_map, &supply_tracker).await; + tracing::info!( "Total snapshot processing time after cleanup: {} secs", start_time.elapsed().as_secs_f64() @@ -124,6 +122,124 @@ pub async fn run( Ok(()) } +const STARTUP_BALANCES_CHUNK_SIZE: usize = 5_000; + +async fn finish_supply_bootstrap( + database: &DatabaseConnection, + accounts_owner_map: &AccountOwnerMap, + supply_tracker: &SupplyTracker, +) { + let Some(startup_slot) = supply_tracker.startup_slot() else { + return; + }; + + if supply_tracker.bootstrap_failed() { + tracing::error!( + "Supply bootstrap poisoned by failed startup account writes, supply stays bootstrapping" + ); + return; + } + + let start_time = Instant::now(); + + let touched = supply_tracker.startup_touched_pubkeys(); + let touched_count = touched.len(); + let mut balances = match resolve_startup_balances( + database, + accounts_owner_map, + touched, + startup_slot, + ) + .await + { + Ok(balances) => balances, + Err(e) => { + tracing::error!( + "Failed to resolve startup balances for supply bootstrap, supply stays bootstrapping: {:?}", + e + ); + return; + } + }; + + let _write_guard = supply_tracker.lock_block_writes().await; + + let late_touches: Vec = supply_tracker + .startup_touched_pubkeys() + .into_iter() + .filter(|pubkey| !balances.contains_key(pubkey)) + .collect(); + match resolve_startup_balances(database, accounts_owner_map, late_touches, startup_slot).await { + Ok(resolved) => balances.extend(resolved), + Err(e) => { + tracing::error!( + "Failed to resolve startup balances for supply bootstrap, supply stays bootstrapping: {:?}", + e + ); + return; + } + } + + let Some(commit) = supply_tracker.finish_bootstrap(&balances) else { + if supply_tracker.bootstrap_failed() { + tracing::error!( + "Supply bootstrap poisoned by failed startup account writes, supply stays bootstrapping" + ); + } else { + tracing::error!( + "Supply bootstrap reconciliation left unresolved accounts, supply stays bootstrapping" + ); + } + return; + }; + + tracing::info!( + target: "supply_bootstrap", + "Supply bootstrap reconciled {} touched accounts against startup slot {} in {} secs - slot: {}, total: {}", + touched_count, + startup_slot, + start_time.elapsed().as_secs_f64(), + commit.slot, + commit.total + ); + + if let Err(e) = db_queries::persist_supply_total(database, &commit).await { + tracing::error!("Failed to persist bootstrapped supply total: {:?}", e); + } +} + +async fn resolve_startup_balances( + database: &DatabaseConnection, + accounts_owner_map: &AccountOwnerMap, + pubkeys: Vec, + startup_slot: u64, +) -> Result> { + let mut routed = Vec::new(); + let mut unrouted = Vec::new(); + for pubkey in pubkeys { + match accounts_owner_map.get_owner(&pubkey) { + Some(owner) => routed.push((owner, pubkey)), + None => unrouted.push(pubkey), + } + } + + let mut balances = HashMap::new(); + for chunk in routed.chunks(STARTUP_BALANCES_CHUNK_SIZE) { + let probe = + db_queries::fetch_startup_balances_by_owner(database, chunk, startup_slot).await?; + balances.extend(probe.resolved); + unrouted.extend(probe.misses); + } + + for chunk in unrouted.chunks(STARTUP_BALANCES_CHUNK_SIZE) { + let resolved = + db_queries::fetch_startup_balances_from_versions(database, chunk, startup_slot).await?; + balances.extend(resolved); + } + + Ok(balances) +} + fn download_and_process_snapshot( sidecar_endpoint: String, snapshot_data: SnapshotData, @@ -185,7 +301,7 @@ async fn process_downloaded_snapshot( } if supply_tracker.is_enabled() { - supply_tracker.note_anchor(bank_info.slot); + supply_tracker.set_startup_total(bank_info.slot, bank_info.capitalization); if let Err(e) = db_queries::persist_supply_seed(database, &bank_info).await { tracing::error!("Failed to persist supply seed from snapshot: {:?}", e); } @@ -256,7 +372,6 @@ async fn process_downloaded_snapshot( insert_into_temp_snapshot_account_versions_tx.clone(); let accounts_owner_map = accounts_owner_map.clone(); - let supply_tracker = supply_tracker.clone(); account_file_workers.spawn(async move { let start_time = Instant::now(); @@ -306,11 +421,6 @@ async fn process_downloaded_snapshot( // Add non closed accounts to the accounts owner map (if enabled) if account.lamports > 0 { accounts_owner_map.upsert_account(&pubkey, &owner, account_file_slot); - supply_tracker.observe_snapshot_account( - account.pubkey, - account_file_slot, - account.lamports, - ); } let account_update = SubscribeUpdateAccount { @@ -461,7 +571,6 @@ pub async fn process_downloaded_snapshot_with_gap_filling( config: SnapshotConfig, gaps_list: Vec, block_sender: Sender, - supply_tracker: SupplyTracker, ) -> Result<()> { let start_time = Instant::now(); @@ -527,7 +636,6 @@ pub async fn process_downloaded_snapshot_with_gap_filling( } let block_sender = block_sender.clone(); - let supply_tracker = supply_tracker.clone(); account_file_workers.spawn(async move { let accounts = AccountsFile::new_for_startup( path, @@ -555,14 +663,6 @@ pub async fn process_downloaded_snapshot_with_gap_filling( return; } - if account.lamports > 0 { - supply_tracker.observe_snapshot_account( - account.pubkey, - account_file_slot, - account.lamports, - ); - } - let account_update = SubscribeUpdateAccountInfo { pubkey: account.pubkey.to_bytes().to_vec(), lamports: account.lamports, From 59f950e3d8a3943915d5066bc61bc4cff4a79fda Mon Sep 17 00:00:00 2001 From: ArthurPaivaT Date: Wed, 22 Jul 2026 18:01:35 +0000 Subject: [PATCH 08/13] cleanup getSupply code --- crates/api/src/modules/supply_cache.rs | 20 ++- crates/core/src/modules/supply_tracker.rs | 137 ++++++++++---------- crates/index/src/db_queries.rs | 61 +++++---- crates/index/src/modules/non_circulating.rs | 27 ++-- crates/index/src/modules/save_block.rs | 32 ++--- crates/snapshot/src/db_queries.rs | 43 +++--- crates/snapshot/src/lib.rs | 51 ++++---- 7 files changed, 175 insertions(+), 196 deletions(-) diff --git a/crates/api/src/modules/supply_cache.rs b/crates/api/src/modules/supply_cache.rs index d4e7e84..73589e3 100644 --- a/crates/api/src/modules/supply_cache.rs +++ b/crates/api/src/modules/supply_cache.rs @@ -26,6 +26,12 @@ pub type SharedSupplySnapshot = Arc>>; const SUPPLY_POLL_INTERVAL: Duration = Duration::from_secs(5); +fn decimal_to_u64(value: Decimal, column: &str) -> Result { + value + .to_u64() + .ok_or_else(|| anyhow::anyhow!("supply.{} {} does not fit in u64", column, value)) +} + pub async fn load_latest_supply( db: &DatabaseConnection, ) -> Result, anyhow::Error> { @@ -42,20 +48,10 @@ pub async fn load_latest_supply( let mut rows = Vec::with_capacity(supply_rows.len()); for row in supply_rows { let slot: i64 = row.try_get("", "slot")?; - let total: Decimal = row.try_get("", "total")?; - let total = total - .to_u64() - .ok_or_else(|| anyhow::anyhow!("supply.total {} does not fit in u64", total))?; + let total = decimal_to_u64(row.try_get("", "total")?, "total")?; let non_circulating: Option = row.try_get("", "non_circulating_lamports")?; let non_circulating = non_circulating - .map(|lamports| { - lamports.to_u64().ok_or_else(|| { - anyhow::anyhow!( - "supply.non_circulating_lamports {} does not fit in u64", - lamports - ) - }) - }) + .map(|lamports| decimal_to_u64(lamports, "non_circulating_lamports")) .transpose()?; rows.push(SupplyRow { slot: slot as u64, diff --git a/crates/core/src/modules/supply_tracker.rs b/crates/core/src/modules/supply_tracker.rs index 425434b..c2179a6 100644 --- a/crates/core/src/modules/supply_tracker.rs +++ b/crates/core/src/modules/supply_tracker.rs @@ -1,7 +1,7 @@ use solana_pubkey::Pubkey; use std::{ collections::{HashMap, HashSet}, - sync::{Arc, Mutex, RwLock}, + sync::{Arc, Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}, }; pub const SUPPLY_RING_SLOTS: u64 = 128; @@ -60,6 +60,14 @@ struct NonCirculatingState { balances: HashMap, } +impl NonCirculatingState { + fn is_member(&self, pubkey: &Pubkey) -> bool { + self.members + .as_ref() + .is_some_and(|members| members.contains(pubkey)) + } +} + #[derive(Debug, Clone)] pub struct SupplyCommit { pub slot: u64, @@ -82,8 +90,10 @@ impl SupplyTracker { pub fn is_tracking_deltas(&self) -> bool { let Some(inner) = &self.0 else { return false }; - let state = inner.state.lock().expect("Failed to lock supply state"); - matches!(state.status, SupplyStatus::Live | SupplyStatus::GapFilling) + matches!( + inner.state().status, + SupplyStatus::Live | SupplyStatus::GapFilling + ) } pub async fn lock_block_writes(&self) -> Option> { @@ -94,38 +104,22 @@ impl SupplyTracker { pub fn observe_account(&self, pubkey: &[u8], slot: u64, lamports: u64) { let Some(inner) = &self.0 else { return }; let pubkey = Pubkey::try_from(pubkey).unwrap(); - { - let non_circulating = inner - .non_circulating - .read() - .expect("Failed to read non-circulating state"); - let is_member = non_circulating - .members - .as_ref() - .is_some_and(|members| members.contains(&pubkey)); - if !is_member { - return; - } + if !inner.non_circulating_read().is_member(&pubkey) { + return; } - let mut non_circulating = inner - .non_circulating - .write() - .expect("Failed to write non-circulating state"); - let is_member = non_circulating - .members - .as_ref() - .is_some_and(|members| members.contains(&pubkey)); - if !is_member { + let mut non_circulating = inner.non_circulating_write(); + if !non_circulating.is_member(&pubkey) { return; } - match non_circulating.balances.get(&pubkey) { - Some(entry) if entry.slot > slot => {} - _ => { - non_circulating - .balances - .insert(pubkey, MemberBalance { slot, lamports }); - } + if non_circulating + .balances + .get(&pubkey) + .is_none_or(|entry| entry.slot <= slot) + { + non_circulating + .balances + .insert(pubkey, MemberBalance { slot, lamports }); } } @@ -136,10 +130,7 @@ impl SupplyTracker { ) { let Some(inner) = &self.0 else { return }; let members: HashSet = accounts.into_iter().collect(); - let mut non_circulating = inner - .non_circulating - .write() - .expect("Failed to write non-circulating state"); + let mut non_circulating = inner.non_circulating_write(); non_circulating .balances .retain(|pubkey, _| members.contains(pubkey)); @@ -147,17 +138,18 @@ impl SupplyTracker { if !members.contains(&balance.pubkey) { continue; } - match non_circulating.balances.get(&balance.pubkey) { - Some(entry) if entry.slot >= balance.slot => {} - _ => { - non_circulating.balances.insert( - balance.pubkey, - MemberBalance { - slot: balance.slot, - lamports: balance.lamports, - }, - ); - } + if non_circulating + .balances + .get(&balance.pubkey) + .is_none_or(|entry| entry.slot < balance.slot) + { + non_circulating.balances.insert( + balance.pubkey, + MemberBalance { + slot: balance.slot, + lamports: balance.lamports, + }, + ); } } non_circulating.members = Some(members); @@ -165,7 +157,7 @@ impl SupplyTracker { pub fn set_startup_total(&self, slot: u64, capitalization: u64) { let Some(inner) = &self.0 else { return }; - let mut state = inner.state.lock().expect("Failed to lock supply state"); + let mut state = inner.state(); if state.status == SupplyStatus::Bootstrapping && slot > state.startup_slot { state.startup_slot = slot; state.total = capitalization; @@ -174,7 +166,7 @@ impl SupplyTracker { pub fn startup_slot(&self) -> Option { let inner = self.0.as_deref()?; - let state = inner.state.lock().expect("Failed to lock supply state"); + let state = inner.state(); (state.status == SupplyStatus::Bootstrapping && state.startup_slot > 0) .then_some(state.startup_slot) } @@ -185,7 +177,7 @@ impl SupplyTracker { touches: impl IntoIterator, ) { let Some(inner) = &self.0 else { return }; - let mut state = inner.state.lock().expect("Failed to lock supply state"); + let mut state = inner.state(); if state.status != SupplyStatus::Bootstrapping { return; } @@ -206,15 +198,14 @@ impl SupplyTracker { let Some(inner) = &self.0 else { return Vec::new(); }; - let state = inner.state.lock().expect("Failed to lock supply state"); - state.startup_touched.keys().copied().collect() + inner.state().startup_touched.keys().copied().collect() } pub fn mark_bootstrap_failed(&self) -> bool { let Some(inner) = &self.0 else { return false; }; - let mut state = inner.state.lock().expect("Failed to lock supply state"); + let mut state = inner.state(); if state.status != SupplyStatus::Bootstrapping || state.bootstrap_failed { return false; } @@ -226,8 +217,7 @@ impl SupplyTracker { let Some(inner) = &self.0 else { return false; }; - let state = inner.state.lock().expect("Failed to lock supply state"); - state.bootstrap_failed + inner.state().bootstrap_failed } pub fn finish_bootstrap( @@ -235,7 +225,7 @@ impl SupplyTracker { startup_balances: &HashMap, ) -> Option { let inner = self.0.as_deref()?; - let mut state = inner.state.lock().expect("Failed to lock supply state"); + let mut state = inner.state(); if state.status != SupplyStatus::Bootstrapping || state.startup_slot == 0 || state.bootstrap_failed @@ -264,7 +254,7 @@ impl SupplyTracker { let Some(inner) = &self.0 else { return false; }; - let mut state = inner.state.lock().expect("Failed to lock supply state"); + let mut state = inner.state(); if state.status != SupplyStatus::Live { return false; } @@ -274,7 +264,7 @@ impl SupplyTracker { pub fn finish_gap(&self) { let Some(inner) = &self.0 else { return }; - let mut state = inner.state.lock().expect("Failed to lock supply state"); + let mut state = inner.state(); if state.status == SupplyStatus::GapFilling { state.status = SupplyStatus::Live; } @@ -284,7 +274,7 @@ impl SupplyTracker { let Some(inner) = &self.0 else { return false; }; - let mut state = inner.state.lock().expect("Failed to lock supply state"); + let mut state = inner.state(); if !matches!(state.status, SupplyStatus::Live | SupplyStatus::GapFilling) { return false; } @@ -294,26 +284,38 @@ impl SupplyTracker { pub fn commit_block(&self, slot: u64, block_delta: i128) -> Option { let inner = self.0.as_deref()?; - let mut state = inner.state.lock().expect("Failed to lock supply state"); + let mut state = inner.state(); state.slot = state.slot.max(slot); if slot <= state.startup_slot { return None; } match state.status { SupplyStatus::Bootstrapping | SupplyStatus::Stale => None, - SupplyStatus::GapFilling => { - state.total = (state.total as i128 + block_delta) as u64; - None - } - SupplyStatus::Live => { + SupplyStatus::GapFilling | SupplyStatus::Live => { state.total = (state.total as i128 + block_delta) as u64; - Some(inner.commit(&state)) + (state.status == SupplyStatus::Live).then(|| inner.commit(&state)) } } } } impl Inner { + fn state(&self) -> MutexGuard<'_, SupplyState> { + self.state.lock().expect("Failed to lock supply state") + } + + fn non_circulating_read(&self) -> RwLockReadGuard<'_, NonCirculatingState> { + self.non_circulating + .read() + .expect("Failed to read non-circulating state") + } + + fn non_circulating_write(&self) -> RwLockWriteGuard<'_, NonCirculatingState> { + self.non_circulating + .write() + .expect("Failed to write non-circulating state") + } + fn commit(&self, state: &SupplyState) -> SupplyCommit { SupplyCommit { slot: state.slot, @@ -323,10 +325,7 @@ impl Inner { } fn sum_non_circulating(&self) -> Option { - let non_circulating = self - .non_circulating - .read() - .expect("Failed to read non-circulating state"); + let non_circulating = self.non_circulating_read(); non_circulating.members.as_ref()?; let lamports: u128 = non_circulating .balances diff --git a/crates/index/src/db_queries.rs b/crates/index/src/db_queries.rs index 442e9fa..10732c7 100644 --- a/crates/index/src/db_queries.rs +++ b/crates/index/src/db_queries.rs @@ -30,7 +30,7 @@ use cloudbreak_core::{ }, }; use cloudbreak_entity::{accounts, service_health, slots}; -use cloudbreak_snapshot::{bytea_array, owner_pubkey_arrays, parse_pubkey}; +use cloudbreak_snapshot::{bytea_array, owner_pubkey_arrays, parse_pubkey, pubkey_bytea_array}; use crate::metrics; @@ -424,7 +424,11 @@ pub async fn insert_recent_blockhash( } } -pub async fn upsert_supply_row(db: &DatabaseConnection, commit: &SupplyCommit, config: &IndexConfig) { +pub async fn upsert_supply_row( + db: &DatabaseConnection, + commit: &SupplyCommit, + config: &IndexConfig, +) { let query_timeout = Duration::from_secs(config.database.save_block_queries_timeout); let supply = Statement::from_sql_and_values( DatabaseBackend::Postgres, @@ -470,10 +474,7 @@ pub async fn upsert_non_circulating_accounts( slot = EXCLUDED.slot, \ accounts = EXCLUDED.accounts, \ updated_at = now()", - [ - Value::from(slot as i64), - bytea_array(accounts.iter().map(|p| p.to_bytes().to_vec()).collect()), - ], + [Value::from(slot as i64), pubkey_bytea_array(accounts)], )) .await; if let Err(e) = result { @@ -488,6 +489,18 @@ pub struct BlockSupplyDelta { pub routed_misses: u64, } +const LATEST_ACCOUNT_ROW_SQL: &str = r#" + SELECT lamports, slot FROM ( + SELECT lamports, slot FROM accounts + WHERE owner = v.owner AND pubkey = v.pubkey + UNION ALL + SELECT lamports, slot FROM snapshot_accounts + WHERE owner = v.owner AND pubkey = v.pubkey + ) u + ORDER BY slot DESC + LIMIT 1 +"#; + fn numeric_array(items: Vec) -> Value { Value::Array( ArrayType::Decimal, @@ -512,24 +525,16 @@ pub async fn fetch_block_supply_delta( let query = db.query_one(Statement::from_sql_and_values( DatabaseBackend::Postgres, - r#" + format!( + r#" SELECT COALESCE(SUM(v.new_lamports - COALESCE(prev.lamports, 0)) FILTER (WHERE prev.slot IS NULL OR prev.slot < $4), 0) AS block_delta, COUNT(*) FILTER (WHERE prev.slot IS NULL) AS routed_misses FROM unnest($1::bytea[], $2::bytea[], $3::numeric[]) AS v(owner, pubkey, new_lamports) - LEFT JOIN LATERAL ( - SELECT lamports, slot FROM ( - SELECT lamports, slot FROM accounts - WHERE owner = v.owner AND pubkey = v.pubkey - UNION ALL - SELECT lamports, slot FROM snapshot_accounts - WHERE owner = v.owner AND pubkey = v.pubkey - ) u - ORDER BY slot DESC - LIMIT 1 - ) prev ON true - "#, + LEFT JOIN LATERAL ({LATEST_ACCOUNT_ROW_SQL}) prev ON true + "# + ), [ bytea_array(owners), bytea_array(pubkeys), @@ -572,21 +577,13 @@ pub async fn fetch_non_circulating_balances( let rows = db .query_all(Statement::from_sql_and_values( DatabaseBackend::Postgres, - r#" + format!( + r#" SELECT v.pubkey, latest.lamports, latest.slot FROM unnest($1::bytea[], $2::bytea[]) AS v(owner, pubkey) - JOIN LATERAL ( - SELECT lamports, slot FROM ( - SELECT lamports, slot FROM accounts - WHERE owner = v.owner AND pubkey = v.pubkey - UNION ALL - SELECT lamports, slot FROM snapshot_accounts - WHERE owner = v.owner AND pubkey = v.pubkey - ) u - ORDER BY slot DESC - LIMIT 1 - ) latest ON true - "#, + JOIN LATERAL ({LATEST_ACCOUNT_ROW_SQL}) latest ON true + "# + ), [owners, pubkeys], )) .await?; diff --git a/crates/index/src/modules/non_circulating.rs b/crates/index/src/modules/non_circulating.rs index 4e2639b..1c83621 100644 --- a/crates/index/src/modules/non_circulating.rs +++ b/crates/index/src/modules/non_circulating.rs @@ -12,8 +12,8 @@ use solana_stake_interface::state::StakeStateV2; use tokio::task::JoinHandle; use tokio::time::Instant; -use crate::metrics; use crate::modules::epoch_stakes::{LATEST_BY_OWNER_SQL, is_healthy}; +use crate::{db_queries, metrics}; use crate::modules::non_circulating_lists::{NON_CIRCULATING_ACCOUNTS, WITHDRAW_AUTHORITY}; const POLL_INTERVAL: Duration = Duration::from_secs(60); @@ -75,22 +75,21 @@ pub fn spawn_non_circulating_recomputer( .map(|owner| (owner, *pubkey)) }) .collect(); - let balances = - match crate::db_queries::fetch_non_circulating_balances(&db, &members).await { - Ok(balances) => balances, - Err(e) => { - tracing::error!( - target: "non_circulating_recomputer", - "failed to fetch non-circulating balances: {:?}", - e - ); - continue; - } - }; + let balances = match db_queries::fetch_non_circulating_balances(&db, &members).await { + Ok(balances) => balances, + Err(e) => { + tracing::error!( + target: "non_circulating_recomputer", + "failed to fetch non-circulating balances: {:?}", + e + ); + continue; + } + }; last_recompute = Some(Instant::now()); next_lockup_expiry = next_expiry; - crate::db_queries::upsert_non_circulating_accounts(&db, slot, &accounts).await; + db_queries::upsert_non_circulating_accounts(&db, slot, &accounts).await; supply_tracker.set_non_circulating_accounts(accounts, balances); } }) diff --git a/crates/index/src/modules/save_block.rs b/crates/index/src/modules/save_block.rs index 8cf9a57..573a900 100644 --- a/crates/index/src/modules/save_block.rs +++ b/crates/index/src/modules/save_block.rs @@ -8,7 +8,7 @@ use sea_orm::{ DatabaseConnection, }; use solana_pubkey::Pubkey; -use std::collections::{HashMap, hash_map::Entry}; +use std::collections::HashMap; use tokio::{ task::{JoinHandle, JoinSet}, time::Instant, @@ -96,20 +96,16 @@ pub async fn save_block( if supply_enabled { let pubkey = Pubkey::try_from(account.pubkey.as_slice()).unwrap(); - match pending_supply_accounts.entry(pubkey) { - Entry::Vacant(entry) => { - entry.insert(PendingSupplyAccount { - owner: accounts_owner_map.get_owner(&pubkey), - lamports: account.lamports, - write_version: account.write_version, - }); - } - Entry::Occupied(mut entry) if account.write_version > entry.get().write_version => { - let pending = entry.get_mut(); - pending.lamports = account.lamports; - pending.write_version = account.write_version; - } - Entry::Occupied(_) => {} + let pending = pending_supply_accounts + .entry(pubkey) + .or_insert_with(|| PendingSupplyAccount { + owner: accounts_owner_map.get_owner(&pubkey), + lamports: account.lamports, + write_version: account.write_version, + }); + if account.write_version > pending.write_version { + pending.lamports = account.lamports; + pending.write_version = account.write_version; } } @@ -193,11 +189,7 @@ pub async fn save_block( chunks.push((current_chunk, current_chunk_bytes)); } - let supply_write_guard = if supply_enabled { - supply_tracker.lock_block_writes().await - } else { - None - }; + let supply_write_guard = supply_tracker.lock_block_writes().await; let block_supply_delta = if supply_tracker.is_tracking_deltas() { compute_block_supply_delta(db, &config, &supply_tracker, pending_supply_accounts, slot) .await diff --git a/crates/snapshot/src/db_queries.rs b/crates/snapshot/src/db_queries.rs index 751c7d3..160d19b 100644 --- a/crates/snapshot/src/db_queries.rs +++ b/crates/snapshot/src/db_queries.rs @@ -601,11 +601,29 @@ pub fn bytea_array(items: Vec>) -> Value { ) } +pub fn pubkey_bytea_array(pubkeys: &[Pubkey]) -> Value { + bytea_array( + pubkeys + .iter() + .map(|pubkey| pubkey.to_bytes().to_vec()) + .collect(), + ) +} + pub fn parse_pubkey(bytes: Vec) -> Result { Pubkey::try_from(bytes.as_slice()) .map_err(|_| sea_orm::DbErr::Custom("invalid pubkey bytes in query result".to_string())) } +async fn query_all_with_timeout( + name: &str, + query: impl Future, sea_orm::DbErr>>, +) -> Result, sea_orm::DbErr> { + timeout(STARTUP_BALANCES_QUERY_TIMEOUT, query) + .await + .map_err(|elapsed| sea_orm::DbErr::Custom(format!("{name} timeout: {elapsed}")))? +} + pub fn owner_pubkey_arrays(pairs: &[(Pubkey, Pubkey)]) -> (Value, Value) { let owners = pairs .iter() @@ -653,14 +671,7 @@ pub async fn fetch_startup_balances_by_owner( [owners, pubkeys, Value::BigInt(Some(startup_slot as i64))], )); - let rows = timeout(STARTUP_BALANCES_QUERY_TIMEOUT, query) - .await - .map_err(|elapsed| { - sea_orm::DbErr::Custom(format!( - "fetch_startup_balances_by_owner timeout: {}", - elapsed - )) - })??; + let rows = query_all_with_timeout("fetch_startup_balances_by_owner", query).await?; for row in rows { let pubkey = parse_pubkey(row.try_get("", "pubkey")?)?; @@ -683,11 +694,6 @@ pub async fn fetch_startup_balances_from_versions( return Ok(Vec::new()); } - let pubkey_values = pubkeys - .iter() - .map(|pubkey| pubkey.to_bytes().to_vec()) - .collect(); - let query = db.query_all(Statement::from_sql_and_values( sea_orm::DatabaseBackend::Postgres, r#" @@ -707,19 +713,12 @@ pub async fn fetch_startup_balances_from_versions( ) prev ON true "#, [ - bytea_array(pubkey_values), + pubkey_bytea_array(pubkeys), Value::BigInt(Some(startup_slot as i64)), ], )); - let rows = timeout(STARTUP_BALANCES_QUERY_TIMEOUT, query) - .await - .map_err(|elapsed| { - sea_orm::DbErr::Custom(format!( - "fetch_startup_balances_from_versions timeout: {}", - elapsed - )) - })??; + let rows = query_all_with_timeout("fetch_startup_balances_from_versions", query).await?; rows.into_iter() .map(|row| { diff --git a/crates/snapshot/src/lib.rs b/crates/snapshot/src/lib.rs index 024d82e..9edbaa4 100644 --- a/crates/snapshot/src/lib.rs +++ b/crates/snapshot/src/lib.rs @@ -33,7 +33,9 @@ pub mod metrics; pub mod sidecar; pub mod stake_data; -pub use db_queries::{bytea_array, owner_pubkey_arrays, parse_pubkey, persist_epoch_stakes}; +pub use db_queries::{ + bytea_array, owner_pubkey_arrays, parse_pubkey, persist_epoch_stakes, pubkey_bytea_array, +}; const DB_ACCOUNTS_BATCH_SIZE: usize = 200; @@ -144,22 +146,12 @@ async fn finish_supply_bootstrap( let touched = supply_tracker.startup_touched_pubkeys(); let touched_count = touched.len(); - let mut balances = match resolve_startup_balances( - database, - accounts_owner_map, - touched, - startup_slot, - ) - .await - { - Ok(balances) => balances, - Err(e) => { - tracing::error!( - "Failed to resolve startup balances for supply bootstrap, supply stays bootstrapping: {:?}", - e - ); - return; - } + let Ok(mut balances) = + resolve_startup_balances(database, accounts_owner_map, touched, startup_slot) + .await + .map_err(log_startup_balances_error) + else { + return; }; let _write_guard = supply_tracker.lock_block_writes().await; @@ -169,16 +161,14 @@ async fn finish_supply_bootstrap( .into_iter() .filter(|pubkey| !balances.contains_key(pubkey)) .collect(); - match resolve_startup_balances(database, accounts_owner_map, late_touches, startup_slot).await { - Ok(resolved) => balances.extend(resolved), - Err(e) => { - tracing::error!( - "Failed to resolve startup balances for supply bootstrap, supply stays bootstrapping: {:?}", - e - ); - return; - } - } + let Ok(late_balances) = + resolve_startup_balances(database, accounts_owner_map, late_touches, startup_slot) + .await + .map_err(log_startup_balances_error) + else { + return; + }; + balances.extend(late_balances); let Some(commit) = supply_tracker.finish_bootstrap(&balances) else { if supply_tracker.bootstrap_failed() { @@ -208,6 +198,13 @@ async fn finish_supply_bootstrap( } } +fn log_startup_balances_error(e: impl std::fmt::Debug) { + tracing::error!( + "Failed to resolve startup balances for supply bootstrap, supply stays bootstrapping: {:?}", + e + ); +} + async fn resolve_startup_balances( database: &DatabaseConnection, accounts_owner_map: &AccountOwnerMap, From edb2af5d77114553c8dcf3deef5326f833f0611a Mon Sep 17 00:00:00 2001 From: ArthurPaivaT Date: Thu, 23 Jul 2026 11:43:03 +0000 Subject: [PATCH 09/13] add getTokenSupply integration tests --- crates/integration_tests/src/benchmark.rs | 1 + .../integration_tests/src/response_comparison.rs | 11 +++++++---- .../src/sources/victoria_logs.rs | 10 +++++++--- crates/integration_tests/src/utils.rs | 16 ++++++++++++---- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/crates/integration_tests/src/benchmark.rs b/crates/integration_tests/src/benchmark.rs index 39c3825..8321a6e 100644 --- a/crates/integration_tests/src/benchmark.rs +++ b/crates/integration_tests/src/benchmark.rs @@ -32,6 +32,7 @@ pub enum RequestType { GetMultipleAccounts, GetBalance, GetTokenAccountBalance, + GetTokenSupply, SimulateTransaction, GetSupply, } diff --git a/crates/integration_tests/src/response_comparison.rs b/crates/integration_tests/src/response_comparison.rs index 613a7a9..d8b5e32 100644 --- a/crates/integration_tests/src/response_comparison.rs +++ b/crates/integration_tests/src/response_comparison.rs @@ -145,6 +145,8 @@ fn accounts_equal(account1: &JsonValue, account2: &JsonValue, encoding: &str) -> /// - `GetBalance` — `result.value` is a `u64`, compared directly. /// - `GetTokenAccountBalance` — `result.value` is a `UiTokenAmount` object, /// compared directly. +/// - `GetTokenSupply` — `result.value` is a `UiTokenAmount` object, +/// compared directly. /// /// For zstd-compressed encodings, account data is decompressed before /// comparison to handle non-deterministic compression across implementations. @@ -178,9 +180,9 @@ pub fn compare_responses( compare_multiple_accounts_responses(response1, response2, encoding) } RequestType::GetAccountInfo => compare_value_with_account(response1, response2, encoding), - RequestType::GetBalance | RequestType::GetTokenAccountBalance => { - compare_value_direct(response1, response2) - } + RequestType::GetBalance + | RequestType::GetTokenAccountBalance + | RequestType::GetTokenSupply => compare_value_direct(response1, response2), RequestType::SimulateTransaction => compare_simulate_responses(response1, response2), RequestType::GetSupply => compare_supply_responses(response1, response2), }; @@ -189,7 +191,8 @@ pub fn compare_responses( } /// Compares two `result.value: u64 | object` responses by direct JSON equality. -/// Used for `getBalance` (u64) and `getTokenAccountBalance` (UiTokenAmount). +/// Used for `getBalance` (u64) and `getTokenAccountBalance` / `getTokenSupply` +/// (UiTokenAmount). fn compare_value_direct(response1: &JsonValue, response2: &JsonValue) -> bool { let v1 = response1.get("result").and_then(|r| r.get("value")); let v2 = response2.get("result").and_then(|r| r.get("value")); diff --git a/crates/integration_tests/src/sources/victoria_logs.rs b/crates/integration_tests/src/sources/victoria_logs.rs index 6b7adf9..6e9d82c 100644 --- a/crates/integration_tests/src/sources/victoria_logs.rs +++ b/crates/integration_tests/src/sources/victoria_logs.rs @@ -62,6 +62,9 @@ pub fn get_body_query( RequestType::GetTokenAccountBalance => format!( "query={time_filter}rpc_call:=\"getTokenAccountBalance\" AND pool_dedicated:~\"liquid\" | limit {limit}" ), + RequestType::GetTokenSupply => format!( + "query={time_filter}rpc_call:=\"getTokenSupply\" AND pool_dedicated:~\"liquid\" | limit {limit}" + ), RequestType::SimulateTransaction => format!( "query={time_filter}rpc_call:=\"simulateTransaction\" AND pool_dedicated:~\"foundation\" AND body:* | limit {limit}" ), @@ -167,9 +170,10 @@ pub async fn get_requests( // positives when "encoding" appears in a memcmp filter rather than as // the response encoding parameter. Verify using the actual parsed field. // - // Methods without an encoding field (`getBalance`, `getTokenAccountBalance`) - // report `"none"` from `extract_encoding_from_request` — for those the - // encoding filter is treated as a no-op (the request passes through). + // Methods without an encoding field (`getBalance`, `getTokenAccountBalance`, + // `getTokenSupply`) report `"none"` from `extract_encoding_from_request` — + // for those the encoding filter is treated as a no-op (the request passes + // through). if let Some(target_encoding) = encoding { let actual_encoding = utils::extract_encoding_from_request(&body, request_type); if actual_encoding != "none" { diff --git a/crates/integration_tests/src/utils.rs b/crates/integration_tests/src/utils.rs index 7d6a691..7ffae73 100644 --- a/crates/integration_tests/src/utils.rs +++ b/crates/integration_tests/src/utils.rs @@ -98,6 +98,7 @@ pub fn extract_commitment_from_request(request: &JsonValue, request_type: Reques | RequestType::GetMultipleAccounts | RequestType::GetBalance | RequestType::GetTokenAccountBalance + | RequestType::GetTokenSupply | RequestType::SimulateTransaction => 1, RequestType::Gtabo | RequestType::Gtabd => 2, RequestType::GetSupply => 0, @@ -119,7 +120,7 @@ pub fn extract_commitment_from_request(request: &JsonValue, request_type: Reques /// `getMultipleAccounts`): the request's `encoding` if present, otherwise the /// per-method Agave default. /// - For methods that don't carry an encoding at all (`getBalance`, -/// `getTokenAccountBalance`): the sentinel string `"none"`. +/// `getTokenAccountBalance`, `getTokenSupply`): the sentinel string `"none"`. /// /// Agave defaults followed here: /// - `getProgramAccounts` → `base58` @@ -127,10 +128,13 @@ pub fn extract_commitment_from_request(request: &JsonValue, request_type: Reques /// - `getAccountInfo` → `binary` (deprecated base58 plain-string) /// - `getMultipleAccounts` → `base64` pub fn extract_encoding_from_request(request: &JsonValue, request_type: RequestType) -> String { - // getBalance / getTokenAccountBalance have no encoding concept. + // getBalance / getTokenAccountBalance / getTokenSupply have no encoding concept. if matches!( request_type, - RequestType::GetBalance | RequestType::GetTokenAccountBalance | RequestType::GetSupply + RequestType::GetBalance + | RequestType::GetTokenAccountBalance + | RequestType::GetTokenSupply + | RequestType::GetSupply ) { return "none".to_string(); } @@ -143,7 +147,10 @@ pub fn extract_encoding_from_request(request: &JsonValue, request_type: RequestT | RequestType::GetMultipleAccounts | RequestType::SimulateTransaction => 1, RequestType::Gtabo | RequestType::Gtabd => 2, - RequestType::GetBalance | RequestType::GetTokenAccountBalance | RequestType::GetSupply => { + RequestType::GetBalance + | RequestType::GetTokenAccountBalance + | RequestType::GetTokenSupply + | RequestType::GetSupply => { unreachable!() } }; @@ -166,6 +173,7 @@ pub fn extract_encoding_from_request(request: &JsonValue, request_type: RequestT RequestType::SimulateTransaction => "base58".to_string(), RequestType::GetBalance | RequestType::GetTokenAccountBalance + | RequestType::GetTokenSupply | RequestType::GetSupply => unreachable!(), }, } From 09f84f35d670e8935266dc3294ff59973315c65b Mon Sep 17 00:00:00 2001 From: ArthurPaivaT Date: Thu, 23 Jul 2026 11:43:11 +0000 Subject: [PATCH 10/13] Fix getSupply mismatch on startup window --- crates/core/src/modules/supply_tracker.rs | 13 +++++++++++++ crates/index/src/modules/save_block.rs | 5 +++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/crates/core/src/modules/supply_tracker.rs b/crates/core/src/modules/supply_tracker.rs index c2179a6..f646adf 100644 --- a/crates/core/src/modules/supply_tracker.rs +++ b/crates/core/src/modules/supply_tracker.rs @@ -52,6 +52,7 @@ struct SupplyState { slot: u64, startup_slot: u64, startup_touched: HashMap, + startup_zero_prev: HashSet, } #[derive(Default)] @@ -235,7 +236,11 @@ impl SupplyTracker { let startup_slot = state.startup_slot; let mut window_delta: i128 = 0; let mut max_slot = startup_slot; + let mut zero_prev = HashSet::new(); for (pubkey, account) in &state.startup_touched { + if account.lamports == 0 { + zero_prev.insert(*pubkey); + } if account.slot <= startup_slot { continue; } @@ -246,10 +251,18 @@ impl SupplyTracker { state.total = (state.total as i128 + window_delta) as u64; state.slot = state.slot.max(max_slot); state.startup_touched = HashMap::new(); + state.startup_zero_prev = zero_prev; state.status = SupplyStatus::Live; Some(inner.commit(&state)) } + pub fn take_zero_prev(&self, pubkey: &Pubkey) -> bool { + let Some(inner) = &self.0 else { + return false; + }; + inner.state().startup_zero_prev.remove(pubkey) + } + pub fn mark_gap(&self) -> bool { let Some(inner) = &self.0 else { return false; diff --git a/crates/index/src/modules/save_block.rs b/crates/index/src/modules/save_block.rs index 573a900..10431e4 100644 --- a/crates/index/src/modules/save_block.rs +++ b/crates/index/src/modules/save_block.rs @@ -337,13 +337,14 @@ async fn compute_block_supply_delta( let mut pubkeys = Vec::new(); let mut new_lamports = Vec::new(); for (pubkey, pending) in pending_accounts { + let zero_prev = supply_tracker.take_zero_prev(&pubkey); match pending.owner { - Some(owner) => { + Some(owner) if !zero_prev => { owners.push(owner.to_bytes().to_vec()); pubkeys.push(pubkey.to_bytes().to_vec()); new_lamports.push(pending.lamports); } - None => block_delta += pending.lamports as i128, + _ => block_delta += pending.lamports as i128, } } From 83d6bbcd7f118754ec09f53565fa8955f8a24dd5 Mon Sep 17 00:00:00 2001 From: ArthurPaivaT Date: Mon, 27 Jul 2026 11:50:56 +0000 Subject: [PATCH 11/13] fix getSupply over count on gap fixes --- crates/core/src/modules/account_owner_map.rs | 8 ++++- crates/core/src/modules/supply_tracker.rs | 25 ++++++++++++++ crates/index/src/db_queries.rs | 5 ++- crates/index/src/modules/save_block.rs | 34 ++++++++++++++++++-- 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/crates/core/src/modules/account_owner_map.rs b/crates/core/src/modules/account_owner_map.rs index 9e09027..21deaad 100644 --- a/crates/core/src/modules/account_owner_map.rs +++ b/crates/core/src/modules/account_owner_map.rs @@ -134,6 +134,7 @@ impl AccountOwnerMap { &self, closed_accounts: Vec>, slot: u64, + defer_removals: bool, ) -> Result { let accounts = self.accounts.as_ref().expect("AccountOwnerMap not enabled"); @@ -146,7 +147,12 @@ impl AccountOwnerMap { let pubkey = Pubkey::try_from(pubkey_bytes.as_slice()).unwrap(); // Only insert closed accounts that are present in the map - if let Some(item) = map.remove(&pubkey) { + let item = if defer_removals { + map.get(&pubkey).cloned() + } else { + map.remove(&pubkey) + }; + if let Some(item) = item { pubkeys.push(pubkey_bytes.clone()); owners.push(item.owner.to_bytes().to_vec()); } diff --git a/crates/core/src/modules/supply_tracker.rs b/crates/core/src/modules/supply_tracker.rs index f646adf..365b1c2 100644 --- a/crates/core/src/modules/supply_tracker.rs +++ b/crates/core/src/modules/supply_tracker.rs @@ -53,6 +53,7 @@ struct SupplyState { startup_slot: u64, startup_touched: HashMap, startup_zero_prev: HashSet, + gap_closes: HashMap, } #[derive(Default)] @@ -263,6 +264,30 @@ impl SupplyTracker { inner.state().startup_zero_prev.remove(pubkey) } + pub fn is_gap_filling(&self) -> bool { + let Some(inner) = &self.0 else { return false }; + inner.state().status == SupplyStatus::GapFilling + } + + pub fn record_gap_closes(&self, slot: u64, closed_accounts: &[Vec]) { + let Some(inner) = &self.0 else { return }; + let mut state = inner.state(); + if state.status != SupplyStatus::GapFilling { + return; + } + for pubkey in closed_accounts { + let pubkey = Pubkey::try_from(pubkey.as_slice()).unwrap(); + let entry = state.gap_closes.entry(pubkey).or_insert(slot); + *entry = (*entry).max(slot); + } + } + + pub fn gap_close_floor(&self, pubkey: &Pubkey) -> Option { + let inner = self.0.as_deref()?; + let state = inner.state(); + state.gap_closes.get(pubkey).copied() + } + pub fn mark_gap(&self) -> bool { let Some(inner) = &self.0 else { return false; diff --git a/crates/index/src/db_queries.rs b/crates/index/src/db_queries.rs index 10732c7..c36cede 100644 --- a/crates/index/src/db_queries.rs +++ b/crates/index/src/db_queries.rs @@ -90,6 +90,7 @@ pub fn insert_closed_accounts( slot: u64, config: &IndexConfig, accounts_owner_map: AccountOwnerMap, + defer_map_removals: bool, ) -> Option> { let query_timeout = Duration::from_secs(config.database.save_block_queries_timeout); @@ -100,7 +101,9 @@ pub fn insert_closed_accounts( let mut inserted = true; if accounts_owner_map.is_enabled() { - let result = accounts_owner_map.save_closed_accounts(pubkeys, slot).await; + let result = accounts_owner_map + .save_closed_accounts(pubkeys, slot, defer_map_removals) + .await; match result { Ok(res) => { tracing::debug!("saved {} closed accounts", res.rows_affected()); diff --git a/crates/index/src/modules/save_block.rs b/crates/index/src/modules/save_block.rs index 10431e4..3de99b3 100644 --- a/crates/index/src/modules/save_block.rs +++ b/crates/index/src/modules/save_block.rs @@ -52,6 +52,7 @@ pub async fn save_block( let max_chunk_bytes_data = config.grpc.max_chunk_bytes_data; let slot = block.slot; + let is_repaired = block.blockhash.is_empty(); modules::snapshot::process_snapshot_if_needed( config.clone(), @@ -152,7 +153,13 @@ pub async fn save_block( continue; } - accounts_owner_map.upsert_account(&account.pubkey, &account.owner, slot); + let resurrects_gap_closed_account = is_repaired + && supply_tracker + .gap_close_floor(&Pubkey::try_from(account.pubkey.as_slice()).unwrap()) + .is_some_and(|closed_slot| closed_slot >= slot); + if !resurrects_gap_closed_account { + accounts_owner_map.upsert_account(&account.pubkey, &account.owner, slot); + } block_bytes_data += account.data.len(); current_chunk_bytes += account.data.len(); @@ -190,9 +197,20 @@ pub async fn save_block( } let supply_write_guard = supply_tracker.lock_block_writes().await; + if !is_repaired { + supply_tracker.record_gap_closes(slot, &closed_accounts_for_slot); + } + let defer_map_removals = supply_tracker.is_gap_filling(); let block_supply_delta = if supply_tracker.is_tracking_deltas() { - compute_block_supply_delta(db, &config, &supply_tracker, pending_supply_accounts, slot) - .await + compute_block_supply_delta( + db, + &config, + &supply_tracker, + pending_supply_accounts, + slot, + is_repaired, + ) + .await } else { supply_tracker.record_startup_touches( slot, @@ -223,6 +241,7 @@ pub async fn save_block( slot, &config, accounts_owner_map, + defer_map_removals, ) } else { None @@ -331,6 +350,7 @@ async fn compute_block_supply_delta( supply_tracker: &SupplyTracker, pending_accounts: HashMap, slot: u64, + is_repaired: bool, ) -> Option { let mut block_delta: i128 = 0; let mut owners = Vec::new(); @@ -338,6 +358,14 @@ async fn compute_block_supply_delta( let mut new_lamports = Vec::new(); for (pubkey, pending) in pending_accounts { let zero_prev = supply_tracker.take_zero_prev(&pubkey); + if is_repaired + && !zero_prev + && supply_tracker + .gap_close_floor(&pubkey) + .is_some_and(|closed_slot| closed_slot >= slot) + { + continue; + } match pending.owner { Some(owner) if !zero_prev => { owners.push(owner.to_bytes().to_vec()); From 804b3b7eec7d0ee785df10b1405e33effdf750c4 Mon Sep 17 00:00:00 2001 From: ArthurPaivaT Date: Mon, 27 Jul 2026 12:02:56 +0000 Subject: [PATCH 12/13] add integration tests for get token largest accounts --- crates/integration_tests/src/benchmark.rs | 1 + .../integration_tests/src/response_comparison.rs | 15 +++++++++++---- .../src/sources/victoria_logs.rs | 9 ++++++--- crates/integration_tests/src/utils.rs | 10 ++++++++-- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/crates/integration_tests/src/benchmark.rs b/crates/integration_tests/src/benchmark.rs index 2e279bc..759b280 100644 --- a/crates/integration_tests/src/benchmark.rs +++ b/crates/integration_tests/src/benchmark.rs @@ -33,6 +33,7 @@ pub enum RequestType { GetBalance, GetTokenAccountBalance, GetTokenSupply, + GetTokenLargestAccounts, SimulateTransaction, GetSupply, } diff --git a/crates/integration_tests/src/response_comparison.rs b/crates/integration_tests/src/response_comparison.rs index e0b02d2..80ff4d1 100644 --- a/crates/integration_tests/src/response_comparison.rs +++ b/crates/integration_tests/src/response_comparison.rs @@ -147,6 +147,10 @@ fn accounts_equal(account1: &JsonValue, account2: &JsonValue, encoding: &str) -> /// compared directly. /// - `GetTokenSupply` — `result.value` is a `UiTokenAmount` object, /// compared directly. +/// - `GetTokenLargestAccounts` — `result.value` is an array of +/// `RpcTokenAccountBalance` objects ordered deterministically by both +/// implementations (amount descending, pubkey-descending tie-break), +/// compared directly. /// /// For zstd-compressed encodings, account data is decompressed before /// comparison to handle non-deterministic compression across implementations. @@ -182,7 +186,8 @@ pub fn compare_responses( RequestType::GetAccountInfo => compare_value_with_account(response1, response2, encoding), RequestType::GetBalance | RequestType::GetTokenAccountBalance - | RequestType::GetTokenSupply => compare_value_direct(response1, response2), + | RequestType::GetTokenSupply + | RequestType::GetTokenLargestAccounts => compare_value_direct(response1, response2), RequestType::SimulateTransaction => compare_simulate_responses(response1, response2), RequestType::GetSupply => compare_supply_responses(response1, response2), }; @@ -190,9 +195,11 @@ pub fn compare_responses( CompareResponsesResult::new_with_matching_context(matches, compare_context_result) } -/// Compares two `result.value: u64 | object` responses by direct JSON equality. -/// Used for `getBalance` (u64) and `getTokenAccountBalance` / `getTokenSupply` -/// (UiTokenAmount). +/// Compares two `result.value: u64 | object | array` responses by direct JSON +/// equality. Used for `getBalance` (u64), `getTokenAccountBalance` / +/// `getTokenSupply` (UiTokenAmount), and `getTokenLargestAccounts` (array of +/// `RpcTokenAccountBalance`, deterministically ordered by amount descending +/// with a pubkey-descending tie-break in both implementations). fn compare_value_direct(response1: &JsonValue, response2: &JsonValue) -> bool { let v1 = response1.get("result").and_then(|r| r.get("value")); let v2 = response2.get("result").and_then(|r| r.get("value")); diff --git a/crates/integration_tests/src/sources/victoria_logs.rs b/crates/integration_tests/src/sources/victoria_logs.rs index e7407db..23eaca6 100644 --- a/crates/integration_tests/src/sources/victoria_logs.rs +++ b/crates/integration_tests/src/sources/victoria_logs.rs @@ -80,6 +80,9 @@ pub fn get_body_query( RequestType::GetTokenSupply => format!( "query={time_filter}rpc_call:=\"getTokenSupply\" AND pool_dedicated:~\"liquid\" | limit {limit}" ), + RequestType::GetTokenLargestAccounts => format!( + "query={time_filter}rpc_call:=\"getTokenLargestAccounts\" AND pool_dedicated:~\"liquid\" | limit {limit}" + ), RequestType::SimulateTransaction => format!( "query={time_filter}rpc_call:=\"simulateTransaction\"{simulate_pool_filter} AND body:* | limit {limit}" ), @@ -192,9 +195,9 @@ pub async fn get_requests( // the response encoding parameter. Verify using the actual parsed field. // // Methods without an encoding field (`getBalance`, `getTokenAccountBalance`, - // `getTokenSupply`) report `"none"` from `extract_encoding_from_request` — - // for those the encoding filter is treated as a no-op (the request passes - // through). + // `getTokenSupply`, `getTokenLargestAccounts`) report `"none"` from + // `extract_encoding_from_request` — for those the encoding filter is + // treated as a no-op (the request passes through). if let Some(target_encoding) = encoding { let actual_encoding = utils::extract_encoding_from_request(&body, request_type); if actual_encoding != "none" { diff --git a/crates/integration_tests/src/utils.rs b/crates/integration_tests/src/utils.rs index 147f1f3..f270c52 100644 --- a/crates/integration_tests/src/utils.rs +++ b/crates/integration_tests/src/utils.rs @@ -112,6 +112,7 @@ pub fn extract_commitment_from_request(request: &JsonValue, request_type: Reques | RequestType::GetBalance | RequestType::GetTokenAccountBalance | RequestType::GetTokenSupply + | RequestType::GetTokenLargestAccounts | RequestType::SimulateTransaction => 1, RequestType::Gtabo | RequestType::Gtabd => 2, RequestType::GetSupply => 0, @@ -133,7 +134,8 @@ pub fn extract_commitment_from_request(request: &JsonValue, request_type: Reques /// `getMultipleAccounts`): the request's `encoding` if present, otherwise the /// per-method Agave default. /// - For methods that don't carry an encoding at all (`getBalance`, -/// `getTokenAccountBalance`, `getTokenSupply`): the sentinel string `"none"`. +/// `getTokenAccountBalance`, `getTokenSupply`, `getTokenLargestAccounts`): +/// the sentinel string `"none"`. /// /// Agave defaults followed here: /// - `getProgramAccounts` → `base58` @@ -141,12 +143,14 @@ pub fn extract_commitment_from_request(request: &JsonValue, request_type: Reques /// - `getAccountInfo` → `binary` (deprecated base58 plain-string) /// - `getMultipleAccounts` → `base64` pub fn extract_encoding_from_request(request: &JsonValue, request_type: RequestType) -> String { - // getBalance / getTokenAccountBalance / getTokenSupply have no encoding concept. + // getBalance / getTokenAccountBalance / getTokenSupply / getTokenLargestAccounts + // have no encoding concept. if matches!( request_type, RequestType::GetBalance | RequestType::GetTokenAccountBalance | RequestType::GetTokenSupply + | RequestType::GetTokenLargestAccounts | RequestType::GetSupply ) { return "none".to_string(); @@ -163,6 +167,7 @@ pub fn extract_encoding_from_request(request: &JsonValue, request_type: RequestT RequestType::GetBalance | RequestType::GetTokenAccountBalance | RequestType::GetTokenSupply + | RequestType::GetTokenLargestAccounts | RequestType::GetSupply => { unreachable!() } @@ -187,6 +192,7 @@ pub fn extract_encoding_from_request(request: &JsonValue, request_type: RequestT RequestType::GetBalance | RequestType::GetTokenAccountBalance | RequestType::GetTokenSupply + | RequestType::GetTokenLargestAccounts | RequestType::GetSupply => unreachable!(), }, } From fb661d9c8c29b8f6bf4507d28fde218dd7f6d1a8 Mon Sep 17 00:00:00 2001 From: ArthurPaivaT Date: Mon, 27 Jul 2026 12:03:59 +0000 Subject: [PATCH 13/13] add getTokenSupply and getTokenLargestAccounts to the readme --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9f9c85e..4cf2144 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ The API server exposes the following JSON-RPC methods: | `getMultipleAccounts` | Batched `getAccountInfo` for up to `[server].max-multiple-accounts` pubkeys per request (default `100`). Returns `null` per position for missing or indexer-filter-excluded accounts. | | `getBalance` | Returns the lamport balance of an account. Returns `0` for missing or closed accounts (Agave-compatible). | | `getTokenAccountBalance` | Returns the `UiTokenAmount` of an SPL Token / Token-2022 account, including mint-aware decimals and UI amounts. WSOL native mint is recognised explicitly. | +| `getTokenSupply` | Returns the total supply of an SPL Token / Token-2022 mint as a `UiTokenAmount`, with mint-aware decimals and UI amounts. | +| `getTokenLargestAccounts` | Returns the 20 largest accounts holding a given mint, sorted by amount descending, each with its address and `UiTokenAmount`. | | `getSlot` | Returns the current slot at the requested commitment level. | | `getHealth` | Returns the health status of the service. | | `getVersion` | Returns the cluster version, Agave-compatible (`{"solana-core": ""}`). See note below for the composite-string format Cloudbreak uses. | @@ -637,13 +639,13 @@ The recomputer detects drift by comparing the total activated stake across runs. ### Simulate Transaction (`simulateTransaction`) -`simulateTransaction` is optional and only served on a **full, unfiltered index** (empty `[programs]` include and exclude lists). Simulation must be able to load any account a transaction touches program, lookup-table, sysvar, and feature-gate accounts included so a filtered index cannot serve it. Cloudbreak checks this at startup, if the index is filtered, the method returns a `simulateTransaction is not supported on this node` error. +`simulateTransaction` is optional and only served on a **full, unfiltered index** (empty `[programs]` include and exclude lists). Simulation must be able to load any account a transaction touches — including program, lookup-table, sysvar, and feature-gate accounts — so a filtered index cannot serve it. Cloudbreak checks this at startup; if the index is filtered, the method returns a `simulateTransaction is not supported on this node` error. -The transaction is executed read-only against the indexed account state at the requested slot, nothing is committed. Cloudbreak reconstructs the cluster's actually-activated feature set at that slot from the on-chain feature accounts (rather than enabling all features), so compute-unit accounting and execution behaviour match mainnet. `replaceRecentBlockhash` substitutes the latest recorded blockhash before execution and reports it with its `lastValidBlockHeight`, `sigVerify` verifies signatures, `accounts` returns post-simulation state for the requested addresses, `innerInstructions` includes decoded inner instructions. +The transaction is executed read-only against the indexed account state at the requested slot; nothing is committed. Cloudbreak reconstructs the cluster's actually-activated feature set at that slot from the on-chain feature accounts (rather than enabling all features), so compute-unit accounting and execution behaviour match mainnet. `replaceRecentBlockhash` substitutes the latest recorded blockhash before execution and reports it with its `lastValidBlockHeight`; `sigVerify` verifies signatures; `accounts` returns post-simulation state for the requested addresses; `innerInstructions` includes decoded inner instructions. ### Supply (`getSupply`) -`getSupply` is optional and only served on a **full, unfiltered index** (empty `[programs]` include and exclude lists), with `supply-tracker-enabled = true` set on the indexer. The indexer seeds the total supply from the snapshot bank's `capitalization` on every (re)start. While the snapshot is ingesting, accounts touched by live blocks are recorded in a small in-memory touched-account map; once the snapshot pass completes, those touches are reconciled against the startup-slot balances read back from the database and the tracker goes live. From then on it advances the total per confirmed block by reading each updated account's previous lamports from the database (owner-routed via the account owner map) and summing the resulting deltas — no full account→lamports map is kept in memory. One `supply` row carrying the total and the non-circulating lamports is persisted per confirmed block (pruned to the last 128 slots), and a background recomputer derives the non-circulating set from Agave's pinned non-circulating account and withdraw-authority lists plus locked stake accounts, refreshing on the same converge-then-heartbeat cadence as the epoch-stakes recomputer. +`getSupply` is optional and only served on a **full, unfiltered index** (empty `[programs]` include and exclude lists), with `supply-tracker-enabled = true` set on the indexer. The indexer seeds the total supply from the snapshot bank's `capitalization` on every (re)start. While the snapshot is ingesting, accounts touched by live blocks are recorded in a small in-memory touched-account map; once the snapshot pass completes, those touches are reconciled against the startup-slot balances read back from the database and the tracker goes live. From then on it advances the total per confirmed block by reading each updated account's previous lamports from the database (owner-routed via the account owner map) and summing the resulting deltas — no full per-account lamports map is kept in memory. One `supply` row carrying the total and the non-circulating lamports is persisted per confirmed block (pruned to the last 128 slots), and a background recomputer derives the non-circulating set from Agave's pinned non-circulating account and withdraw-authority lists plus locked stake accounts, refreshing on the same converge-then-heartbeat cadence as the epoch-stakes recomputer. The API serves from a polled cache. Requests return a node-unhealthy error until the bootstrap pass has completed and the non-circulating set has been computed. On a detected slot gap the tracker keeps applying block deltas but stops publishing totals until every gap slot has been repaired (requests fail as node-unhealthy in the meantime); a failed account write or a failed delta query marks it stale, and it stays fail-closed until the indexer restarts. For `finalized` commitment the response picks the newest persisted total at or below the finalized slot; `confirmed` returns the newest total. `context.slot` is the slot of the served row; the non-circulating account list may lag it by up to the recomputer heartbeat.