diff --git a/packages/fastlane/Cargo.lock b/packages/fastlane/Cargo.lock index ab4780a..d9ab3c8 100644 --- a/packages/fastlane/Cargo.lock +++ b/packages/fastlane/Cargo.lock @@ -68,6 +68,17 @@ dependencies = [ "solana-svm-feature-set", ] +[[package]] +name = "agave-reserved-account-keys" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "997e580e670daab5b0e26331b292e0dd06771d3dd732d58393cea1f52f8187a1" +dependencies = [ + "agave-feature-set", + "solana-pubkey 3.0.0", + "solana-sdk-ids", +] + [[package]] name = "ahash" version = "0.8.12" @@ -221,6 +232,58 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "autotools" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef941527c41b0fc0dd48511a8154cd5fc7e29200a0ff8b7203c5d777dbc795cf" +dependencies = [ + "cc", +] + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "bytes", + "futures-util", + "http 1.4.0", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -914,6 +977,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "event-listener" version = "5.4.1" @@ -947,6 +1020,12 @@ dependencies = [ "siphasher 1.0.1", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "feature-probe" version = "0.1.1" @@ -1014,6 +1093,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "059c31d7d36c43fe39d89e55711858b4da8be7eb6dabac23c7289b1a19489406" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.5" @@ -1213,6 +1298,25 @@ dependencies = [ "subtle", ] +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hash32" version = "0.3.1" @@ -1234,6 +1338,12 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.5.2" @@ -1305,6 +1415,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.8.1" @@ -1315,9 +1431,11 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http 1.4.0", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -1343,6 +1461,19 @@ dependencies = [ "webpki-roots 1.0.4", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.19" @@ -1646,6 +1777,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "litemap" version = "0.8.1" @@ -1673,6 +1810,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.7.6" @@ -1700,6 +1843,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1727,6 +1876,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "napi" version = "2.16.17" @@ -2044,6 +2199,36 @@ dependencies = [ "num", ] +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2058,7 +2243,7 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pipeit-fastlane" -version = "0.1.2" +version = "0.1.4" dependencies = [ "anyhow", "dashmap 6.1.0", @@ -2073,6 +2258,8 @@ dependencies = [ "solana-tls-utils", "thiserror 1.0.69", "tokio", + "yellowstone-grpc-client", + "yellowstone-grpc-proto", ] [[package]] @@ -2133,6 +2320,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.111", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -2151,6 +2348,89 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "pulldown-cmark", + "pulldown-cmark-to-cmark", + "regex", + "syn 2.0.111", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "prost-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" +dependencies = [ + "prost", +] + +[[package]] +name = "protobuf-src" +version = "1.1.0+21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7ac8852baeb3cc6fb83b93646fb93c0ffe5d14bf138c945ceb4b9948ee0e3c1" +dependencies = [ + "autotools", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "pulldown-cmark-to-cmark" +version = "21.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8246feae3db61428fd0bb94285c690b460e4517d83152377543ca802357785f1" +dependencies = [ + "pulldown-cmark", +] + [[package]] name = "qstring" version = "0.7.2" @@ -2482,12 +2762,26 @@ dependencies = [ "nom", ] +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -3438,6 +3732,20 @@ dependencies = [ "solana-sysvar-id", ] +[[package]] +name = "solana-loader-v2-interface" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4a6f0ad4fd9c30679bfee2ce3ea6a449cac38049f210480b751f65676dfe82" +dependencies = [ + "serde", + "serde_bytes", + "serde_derive", + "solana-instruction", + "solana-pubkey 3.0.0", + "solana-sdk-ids", +] + [[package]] name = "solana-loader-v3-interface" version = "6.1.0" @@ -3450,6 +3758,7 @@ dependencies = [ "solana-instruction", "solana-pubkey 3.0.0", "solana-sdk-ids", + "solana-system-interface", ] [[package]] @@ -4429,6 +4738,49 @@ dependencies = [ "solana-signature", ] +[[package]] +name = "solana-transaction-status" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eb034ed7e5a57e25f82622b006d0d05a248d07ff55701ba51152f4178098dcd" +dependencies = [ + "Inflector", + "agave-reserved-account-keys", + "base64 0.22.1", + "bincode", + "borsh", + "bs58", + "log", + "serde", + "serde_json", + "solana-account-decoder", + "solana-address-lookup-table-interface", + "solana-clock", + "solana-hash 3.1.0", + "solana-instruction", + "solana-loader-v2-interface", + "solana-loader-v3-interface", + "solana-message", + "solana-program-option", + "solana-pubkey 3.0.0", + "solana-reward-info", + "solana-sdk-ids", + "solana-signature", + "solana-stake-interface", + "solana-system-interface", + "solana-transaction", + "solana-transaction-error", + "solana-transaction-status-client-types", + "solana-vote-interface", + "spl-associated-token-account-interface", + "spl-memo-interface", + "spl-token-2022-interface", + "spl-token-group-interface", + "spl-token-interface", + "spl-token-metadata-interface", + "thiserror 2.0.17", +] + [[package]] name = "solana-transaction-status-client-types" version = "3.1.4" @@ -4565,6 +4917,17 @@ dependencies = [ "der", ] +[[package]] +name = "spl-associated-token-account-interface" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6433917b60441d68d99a17e121d9db0ea15a9a69c0e5afa34649cf5ba12612f" +dependencies = [ + "borsh", + "solana-instruction", + "solana-pubkey 3.0.0", +] + [[package]] name = "spl-discriminator" version = "0.5.1" @@ -4611,6 +4974,16 @@ dependencies = [ "solana-pubkey 3.0.0", ] +[[package]] +name = "spl-memo-interface" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d4e2aedd58f858337fa609af5ad7100d4a243fdaf6a40d6eb4c28c5f19505d3" +dependencies = [ + "solana-instruction", + "solana-pubkey 3.0.0", +] + [[package]] name = "spl-pod" version = "0.7.1" @@ -4836,6 +5209,19 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -4979,6 +5365,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -5041,6 +5428,91 @@ dependencies = [ "winnow", ] +[[package]] +name = "tonic" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" +dependencies = [ + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "flate2", + "h2", + "http 1.4.0", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "rustls-native-certs", + "socket2", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", + "zstd", +] + +[[package]] +name = "tonic-build" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c40aaccc9f9eccf2cd82ebc111adc13030d23e887244bc9cfa5d1d636049de3" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "tonic-health" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a82868bf299e0a1d2e8dce0dc33a46c02d6f045b2c1f1d6cc8dc3d0bf1812ef" +dependencies = [ + "prost", + "tokio", + "tokio-stream", + "tonic", + "tonic-prost", +] + +[[package]] +name = "tonic-prost" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a16cba4043dc3ff43fcb3f96b4c5c154c64cbd18ca8dce2ab2c6a451d058a2" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn 2.0.111", + "tempfile", + "tonic-build", +] + [[package]] name = "tower" version = "0.5.2" @@ -5049,11 +5521,15 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", + "indexmap", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -5099,9 +5575,21 @@ checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "tracing-core" version = "0.1.35" @@ -5143,6 +5631,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-ident" version = "1.0.22" @@ -5681,6 +6175,48 @@ dependencies = [ "time", ] +[[package]] +name = "yellowstone-grpc-client" +version = "10.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e3475e3027f8832589b573fb5253a8c304532999b427d3d4c9de6f6912a7162" +dependencies = [ + "bytes", + "futures", + "thiserror 2.0.17", + "tonic", + "tonic-health", + "yellowstone-grpc-proto", +] + +[[package]] +name = "yellowstone-grpc-proto" +version = "10.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b515077c7db0e9e0dcc506bc6e7bbd6906fcdc3d4978d7039a23a415af4390b" +dependencies = [ + "anyhow", + "bincode", + "prost", + "prost-types", + "protobuf-src", + "solana-account", + "solana-account-decoder", + "solana-clock", + "solana-hash 3.1.0", + "solana-message", + "solana-pubkey 3.0.0", + "solana-signature", + "solana-transaction", + "solana-transaction-context", + "solana-transaction-error", + "solana-transaction-status", + "tonic", + "tonic-build", + "tonic-prost", + "tonic-prost-build", +] + [[package]] name = "yoke" version = "0.8.1" diff --git a/packages/fastlane/Cargo.toml b/packages/fastlane/Cargo.toml index d706743..a9edd01 100644 --- a/packages/fastlane/Cargo.toml +++ b/packages/fastlane/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pipeit-fastlane" -version = "0.1.3" +version = "0.1.4" edition = "2021" description = "Native QUIC client for direct Solana TPU transaction submission" license = "MIT" @@ -26,6 +26,8 @@ solana-sdk = "3.0" # Async runtime tokio = { version = "1", features = ["full"] } futures-util = "0.3" +yellowstone-grpc-client = { version = "10.2.0" } +yellowstone-grpc-proto = { version = "10.1.1" } # Data structures dashmap = "6" @@ -37,8 +39,9 @@ thiserror = "1" [build-dependencies] napi-build = "2" +[patch.crates-io] +protobuf-src = { path = "vendor/protobuf-src" } + [profile.release] lto = false opt-level = 3 - - diff --git a/packages/fastlane/package.json b/packages/fastlane/package.json index 0e61838..fccd18e 100644 --- a/packages/fastlane/package.json +++ b/packages/fastlane/package.json @@ -1,6 +1,6 @@ { "name": "@pipeit/fastlane", - "version": "0.1.3", + "version": "0.1.4", "description": "Native QUIC client for direct Solana TPU transaction submission", "main": "index.js", "types": "index.d.ts", diff --git a/packages/fastlane/src/client.rs b/packages/fastlane/src/client.rs index 60d9175..0944829 100644 --- a/packages/fastlane/src/client.rs +++ b/packages/fastlane/src/client.rs @@ -30,6 +30,11 @@ pub struct TpuClientConfig { pub rpc_url: String, /// WebSocket URL for slot update subscriptions. pub ws_url: String, + /// Optional gRPC URL for Yellowstone slot subscriptions. + /// When set, this takes precedence over WebSocket tracking. + pub grpc_url: Option, + /// Optional gRPC x-token for authenticated Yellowstone endpoints. + pub grpc_x_token: Option, /// Number of upcoming leaders to send transactions to (default: 2). pub fanout: Option, /// Whether to pre-warm connections to upcoming leaders (default: true). @@ -145,7 +150,12 @@ impl TpuClient { // Initialize leader tracker let leader_tracker = runtime.block_on(async { - LeaderTracker::new(config.rpc_url.clone(), config.ws_url.clone()) + LeaderTracker::new( + config.rpc_url.clone(), + config.ws_url.clone(), + config.grpc_url.clone(), + config.grpc_x_token.clone(), + ) .await .context("Failed to create leader tracker") }).map_err(anyhow_to_napi)?; @@ -182,11 +192,11 @@ impl TpuClient { let _ = lt_for_slots.run_slot_listener().await; }); - // Start socket updater (every 60 seconds) + // Start socket updater (every 10 seconds for fresher TPU sockets) let lt_for_sockets = lt_clone.clone(); let socket_updater = tokio::spawn(async move { lt_for_sockets - .run_socket_updater(Duration::from_secs(60)) + .run_socket_updater(Duration::from_secs(10)) .await; }); diff --git a/packages/fastlane/src/tracker/leader_tracker.rs b/packages/fastlane/src/tracker/leader_tracker.rs index 5a1532d..3d36fb1 100644 --- a/packages/fastlane/src/tracker/leader_tracker.rs +++ b/packages/fastlane/src/tracker/leader_tracker.rs @@ -30,7 +30,7 @@ pub struct LeaderInfo { /// TPU socket addresses for a validator. /// Stores both normal and forwards ports for flexible routing. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct TpuSockets { /// Standard TPU QUIC socket address. pub tpu_socket: Option, @@ -52,6 +52,10 @@ pub struct LeaderTracker { rpc_url: String, /// WebSocket URL for subscriptions. ws_url: String, + /// Optional Yellowstone gRPC URL for slot subscriptions. + grpc_url: Option, + /// Optional Yellowstone gRPC x-token for authenticated endpoints. + grpc_x_token: Option, /// Real-time slot tracker. pub slots_tracker: RwLock, /// Leader schedule tracker. @@ -69,7 +73,12 @@ impl LeaderTracker { /// /// * `rpc_url` - RPC endpoint URL /// * `ws_url` - WebSocket endpoint URL - pub async fn new(rpc_url: String, ws_url: String) -> Result { + pub async fn new( + rpc_url: String, + ws_url: String, + grpc_url: Option, + grpc_x_token: Option, + ) -> Result { let rpc_client = RpcClient::new(rpc_url.clone()); let schedule_tracker = ScheduleTracker::new(&rpc_client) @@ -79,6 +88,8 @@ impl LeaderTracker { Ok(Self { rpc_url, ws_url, + grpc_url, + grpc_x_token, slots_tracker: RwLock::new(SlotsTracker::new()), schedule_tracker: RwLock::new(schedule_tracker), leader_sockets: RwLock::new(HashMap::new()), @@ -272,7 +283,7 @@ impl LeaderTracker { /// Updates the leader socket addresses from cluster nodes. /// /// Fetches both normal TPU QUIC and TPU forwards QUIC addresses. - /// Should be called periodically (e.g., every 60 seconds) as + /// Should be called periodically (e.g., every 10 seconds) as /// validator IPs can change. pub async fn update_leader_sockets(&self) -> Result<()> { let rpc_client = RpcClient::new(self.rpc_url.clone()); @@ -282,29 +293,39 @@ impl LeaderTracker { .await .context("Failed to fetch cluster nodes")?; - let mut new_sockets = HashMap::new(); + let mut sockets = self.leader_sockets.write().await; + let mut seen = HashSet::new(); for node in nodes { + let pubkey = node.pubkey.to_string(); + seen.insert(pubkey.clone()); + // Standard TPU QUIC socket (full SocketAddr from RPC) let tpu_socket = node.tpu_quic.map(|addr| addr.to_string()); // TPU forwards QUIC socket (preferred by validators) let tpu_forwards_socket = node.tpu_forwards_quic.map(|addr| addr.to_string()); - // Only add if at least one socket is available + // Only store if at least one socket is available if tpu_socket.is_some() || tpu_forwards_socket.is_some() { - new_sockets.insert( - node.pubkey.to_string(), - TpuSockets { - tpu_socket, - tpu_forwards_socket, - }, - ); + let new_entry = TpuSockets { + tpu_socket, + tpu_forwards_socket, + }; + + let needs_update = sockets + .get(&pubkey) + .map(|existing| existing != &new_entry) + .unwrap_or(true); + + if needs_update { + sockets.insert(pubkey, new_entry); + } } } - let mut sockets = self.leader_sockets.write().await; - *sockets = new_sockets; + // Remove validators no longer present in the cluster nodes response. + sockets.retain(|pubkey, _| seen.contains(pubkey)); Ok(()) } @@ -324,9 +345,18 @@ impl LeaderTracker { } } - /// Inner slot listener that handles the WebSocket connection. + /// Inner slot listener that handles the configured connection. /// Returns when the connection ends (either normally or due to error). async fn run_slot_listener_inner(&self) -> Result<()> { + if let Some(grpc_url) = self.grpc_url.as_ref() { + return self.run_grpc_slot_listener_inner(grpc_url).await; + } + + self.run_wss_slot_listener_inner().await + } + + /// Inner slot listener that handles the WebSocket connection. + async fn run_wss_slot_listener_inner(&self) -> Result<()> { let ws_client = PubsubClient::new(&self.ws_url) .await .context("Failed to connect to WebSocket")?; @@ -350,6 +380,76 @@ impl LeaderTracker { Ok(()) } + /// Inner slot listener that handles the Yellowstone gRPC connection. + async fn run_grpc_slot_listener_inner(&self, grpc_url: &str) -> Result<()> { + use yellowstone_grpc_client::{ClientTlsConfig, GeyserGrpcBuilder}; + use yellowstone_grpc_proto::geyser::{ + SubscribeRequest, SubscribeRequestFilterSlots, + subscribe_update::UpdateOneof, + }; + + let mut builder = GeyserGrpcBuilder::from_shared(grpc_url.to_string()) + .context("Failed to build gRPC client")?; + let x_token = self.grpc_x_token.clone(); + builder = builder.x_token(x_token).context("Failed to set gRPC x-token")?; + + let mut client = builder + .tls_config(ClientTlsConfig::default().with_enabled_roots()) + .context("Failed to configure gRPC TLS")? + .connect() + .await + .context("Failed to connect to gRPC endpoint")?; + + let subscribe_request = SubscribeRequest { + slots: std::collections::HashMap::from([( + "fastlane-slot-tracker".to_string(), + SubscribeRequestFilterSlots { + interslot_updates: Some(true), + ..Default::default() + }, + )]), + ..Default::default() + }; + + let mut stream = client + .subscribe_once(subscribe_request) + .await + .context("Failed to subscribe to gRPC slot updates")?; + + let mut ready_set = false; + while let Some(result) = stream.next().await { + let update = result.context("gRPC slot stream error")?; + if let Some(UpdateOneof::Slot(slot_update)) = update.update_oneof { + let slot = slot_update.slot; + + // Record the slot update (monotonic source; bypass outlier filtering) + let curr_slot = { + let mut tracker = self.slots_tracker.write().await; + tracker.record_monotonic(slot) + }; + + // Mark as ready once we start receiving updates + if !ready_set { + let mut ready = self.ready.write().await; + *ready = true; + ready_set = true; + } + + // Check if we need to rotate to next epoch (keep schedule fresh across epoch boundaries) + let needs_rotation = { + let schedule_tracker = self.schedule_tracker.read().await; + curr_slot >= schedule_tracker.next_epoch_slot_start() + }; + + if needs_rotation { + self.rotate_epoch(curr_slot).await?; + } + } + } + + Ok(()) + } + /// Handles a single slot update event. async fn handle_slot_event(&self, slot_update: SlotUpdate) -> Result<()> { // Convert to our SlotEvent type @@ -405,6 +505,7 @@ impl std::fmt::Debug for LeaderTracker { f.debug_struct("LeaderTracker") .field("rpc_url", &self.rpc_url) .field("ws_url", &self.ws_url) + .field("grpc_url", &self.grpc_url) .finish() } } diff --git a/packages/fastlane/src/tracker/slots_tracker.rs b/packages/fastlane/src/tracker/slots_tracker.rs index 164f7e2..9c929ca 100644 --- a/packages/fastlane/src/tracker/slots_tracker.rs +++ b/packages/fastlane/src/tracker/slots_tracker.rs @@ -88,6 +88,20 @@ impl SlotsTracker { self.record(SlotEvent::End(slot)) } + /// Records a slot update from a monotonic source (e.g., gRPC). + /// + /// Ignores out-of-order or duplicate slots and bypasses outlier filtering. + pub fn record_monotonic(&mut self, slot: Slot) -> Slot { + if slot <= self.current_slot { + return self.current_slot; + } + + self.current_slot = slot; + self.recent_events.clear(); + self.recent_events.push_back(SlotEvent::Start(slot)); + self.current_slot + } + /// Estimates the current slot based on recent events. /// /// Uses a median-based approach to filter outliers: @@ -182,4 +196,3 @@ mod tests { } } - diff --git a/packages/fastlane/vendor/protobuf-src/Cargo.toml b/packages/fastlane/vendor/protobuf-src/Cargo.toml new file mode 100644 index 0000000..a325567 --- /dev/null +++ b/packages/fastlane/vendor/protobuf-src/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "protobuf-src" +version = "1.1.0+21.5" +edition = "2021" +license = "Apache-2.0" +publish = false +description = "Local override to supply a prebuilt protoc without building C++ protobuf" + +[dependencies] +protoc-bin-vendored = "2" diff --git a/packages/fastlane/vendor/protobuf-src/src/lib.rs b/packages/fastlane/vendor/protobuf-src/src/lib.rs new file mode 100644 index 0000000..c7627e4 --- /dev/null +++ b/packages/fastlane/vendor/protobuf-src/src/lib.rs @@ -0,0 +1,29 @@ +use std::path::PathBuf; + +/// Returns the path to a protoc binary. +/// +/// Prefers the PROTOC env var if set, otherwise falls back to a vendored binary. +pub fn protoc() -> PathBuf { + if let Ok(path) = std::env::var("PROTOC") { + return PathBuf::from(path); + } + + protoc_bin_vendored::protoc_bin_path() + .expect("protoc-bin-vendored should provide a protoc binary") +} + +/// Returns the path to the protobuf include directory if available. +pub fn include() -> PathBuf { + if let Ok(path) = std::env::var("PROTOC_INCLUDE") { + return PathBuf::from(path); + } + + let protoc_path = protoc(); + if let Some(bin_dir) = protoc_path.parent() { + if let Some(root_dir) = bin_dir.parent() { + return root_dir.join("include"); + } + } + + PathBuf::new() +}