diff --git a/apps/hermes/server/Cargo.lock b/apps/hermes/server/Cargo.lock index 84960ed462..be9fa40418 100644 --- a/apps/hermes/server/Cargo.lock +++ b/apps/hermes/server/Cargo.lock @@ -342,7 +342,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror", + "thiserror 1.0.58", "time", ] @@ -472,7 +472,7 @@ dependencies = [ "bitflags 1.3.2", "bytes", "futures-util", - "http", + "http 0.2.12", "http-body", "hyper", "itoa", @@ -504,7 +504,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", + "http 0.2.12", "http-body", "mime", "rustversion", @@ -861,9 +861,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "caps" @@ -872,7 +872,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "190baaad529bcfbde9e1a19022c42781bdb6ff9de25721abdb8fd98c0807730b" dependencies = [ "libc", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -1756,6 +1756,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + [[package]] name = "gimli" version = "0.28.1" @@ -1804,7 +1816,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", "indexmap 2.2.6", "slab", "tokio", @@ -1868,7 +1880,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermes" -version = "0.9.3" +version = "0.10.0-alpha" dependencies = [ "anyhow", "async-trait", @@ -1913,6 +1925,7 @@ dependencies = [ "strum", "tokio", "tokio-stream", + "tokio-tungstenite 0.26.2", "tonic", "tonic-build", "tower-http", @@ -2003,6 +2016,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -2010,7 +2034,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", "pin-project-lite", ] @@ -2049,7 +2073,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 0.2.12", "http-body", "httparse", "httpdate", @@ -2069,7 +2093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", - "http", + "http 0.2.12", "hyper", "rustls 0.21.10", "tokio", @@ -2296,9 +2320,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libredox" @@ -3239,7 +3263,7 @@ dependencies = [ "pyth-sdk 0.8.0", "serde", "solana-program", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -3258,7 +3282,7 @@ dependencies = [ "sha3 0.10.8", "slow_primes", "strum", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -3297,7 +3321,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls 0.20.9", - "thiserror", + "thiserror 1.0.58", "tokio", "tracing", "webpki", @@ -3316,7 +3340,7 @@ dependencies = [ "rustls 0.20.9", "rustls-native-certs", "slab", - "thiserror", + "thiserror 1.0.58", "tinyvec", "tracing", "webpki", @@ -3353,6 +3377,12 @@ dependencies = [ "proc-macro2 1.0.92", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "radium" version = "0.7.0" @@ -3383,6 +3413,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -3403,6 +3443,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -3421,6 +3471,15 @@ dependencies = [ "getrandom 0.2.12", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -3497,7 +3556,7 @@ checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" dependencies = [ "getrandom 0.2.12", "libredox", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -3566,7 +3625,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 0.2.12", "http-body", "hyper", "hyper-rustls", @@ -4044,7 +4103,7 @@ dependencies = [ "futures", "percent-encoding", "serde", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -4090,7 +4149,7 @@ dependencies = [ "itoa", "serde", "serde_bytes", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -4281,7 +4340,7 @@ dependencies = [ "spl-token", "spl-token-2022", "spl-token-metadata-interface", - "thiserror", + "thiserror 1.0.58", "zstd", ] @@ -4303,7 +4362,7 @@ dependencies = [ "solana-program", "solana-program-runtime", "solana-sdk", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -4318,7 +4377,7 @@ dependencies = [ "solana-perf", "solana-remote-wallet", "solana-sdk", - "thiserror", + "thiserror 1.0.58", "tiny-bip39", "uriparse", "url", @@ -4353,7 +4412,7 @@ dependencies = [ "solana-thin-client", "solana-tpu-client", "solana-udp-client", - "thiserror", + "thiserror 1.0.58", "tokio", ] @@ -4388,7 +4447,7 @@ dependencies = [ "solana-measure", "solana-metrics", "solana-sdk", - "thiserror", + "thiserror 1.0.58", "tokio", ] @@ -4422,7 +4481,7 @@ dependencies = [ "sha2 0.10.8", "solana-frozen-abi-macro", "subtle", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -4570,7 +4629,7 @@ dependencies = [ "solana-frozen-abi", "solana-frozen-abi-macro", "solana-sdk-macro", - "thiserror", + "thiserror 1.0.58", "tiny-bip39", "wasm-bindgen", "zeroize", @@ -4601,7 +4660,7 @@ dependencies = [ "solana-metrics", "solana-sdk", "solana_rbpf", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -4621,7 +4680,7 @@ dependencies = [ "solana-account-decoder", "solana-rpc-client-api", "solana-sdk", - "thiserror", + "thiserror 1.0.58", "tokio", "tokio-stream", "tokio-tungstenite 0.17.2", @@ -4653,7 +4712,7 @@ dependencies = [ "solana-rpc-client-api", "solana-sdk", "solana-streamer", - "thiserror", + "thiserror 1.0.58", "tokio", ] @@ -4682,7 +4741,7 @@ dependencies = [ "qstring", "semver", "solana-sdk", - "thiserror", + "thiserror 1.0.58", "uriparse", ] @@ -4731,7 +4790,7 @@ dependencies = [ "solana-transaction-status", "solana-version", "spl-token-2022", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -4744,7 +4803,7 @@ dependencies = [ "solana-clap-utils", "solana-rpc-client", "solana-sdk", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -4795,7 +4854,7 @@ dependencies = [ "solana-logger", "solana-program", "solana-sdk-macro", - "thiserror", + "thiserror 1.0.58", "uriparse", "wasm-bindgen", ] @@ -4841,7 +4900,7 @@ dependencies = [ "solana-metrics", "solana-perf", "solana-sdk", - "thiserror", + "thiserror 1.0.58", "tokio", "x509-parser", ] @@ -4882,7 +4941,7 @@ dependencies = [ "solana-rpc-client", "solana-rpc-client-api", "solana-sdk", - "thiserror", + "thiserror 1.0.58", "tokio", ] @@ -4909,7 +4968,7 @@ dependencies = [ "spl-memo", "spl-token", "spl-token-2022", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -4923,7 +4982,7 @@ dependencies = [ "solana-net-utils", "solana-sdk", "solana-streamer", - "thiserror", + "thiserror 1.0.58", "tokio", ] @@ -4962,7 +5021,7 @@ dependencies = [ "solana-program", "solana-program-runtime", "solana-sdk", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -4990,7 +5049,7 @@ dependencies = [ "solana-program", "solana-sdk", "subtle", - "thiserror", + "thiserror 1.0.58", "zeroize", ] @@ -5009,7 +5068,7 @@ dependencies = [ "rand 0.8.5", "rustc-demangle", "scroll", - "thiserror", + "thiserror 1.0.58", "winapi", ] @@ -5057,7 +5116,7 @@ dependencies = [ "solana-program", "spl-token", "spl-token-2022", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -5092,7 +5151,7 @@ dependencies = [ "quote 1.0.35", "sha2 0.10.8", "syn 2.0.89", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -5127,7 +5186,7 @@ dependencies = [ "num-traits", "solana-program", "spl-program-error-derive", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -5168,7 +5227,7 @@ dependencies = [ "num-traits", "num_enum 0.6.1", "solana-program", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -5190,7 +5249,7 @@ dependencies = [ "spl-token-metadata-interface", "spl-transfer-hook-interface", "spl-type-length-value", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -5402,7 +5461,16 @@ version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.58", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -5416,6 +5484,17 @@ dependencies = [ "syn 2.0.89", ] +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2 1.0.92", + "quote 1.0.35", + "syn 2.0.89", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -5470,7 +5549,7 @@ dependencies = [ "rand 0.7.3", "rustc-hash", "sha2 0.9.9", - "thiserror", + "thiserror 1.0.58", "unicode-normalization", "wasm-bindgen", "zeroize", @@ -5602,6 +5681,20 @@ dependencies = [ "tungstenite 0.20.1", ] +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "native-tls", + "tokio", + "tokio-native-tls", + "tungstenite 0.26.2", +] + [[package]] name = "tokio-util" version = "0.7.10" @@ -5665,7 +5758,7 @@ dependencies = [ "base64 0.21.7", "bytes", "h2", - "http", + "http 0.2.12", "http-body", "hyper", "hyper-timeout", @@ -5726,7 +5819,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http", + "http 0.2.12", "http-body", "http-range-header", "pin-project-lite", @@ -5836,13 +5929,13 @@ dependencies = [ "base64 0.13.1", "byteorder", "bytes", - "http", + "http 0.2.12", "httparse", "log", "rand 0.8.5", "rustls 0.20.9", "sha-1", - "thiserror", + "thiserror 1.0.58", "url", "utf-8", "webpki", @@ -5858,16 +5951,34 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http", + "http 0.2.12", "httparse", "log", "rand 0.8.5", "sha1", - "thiserror", + "thiserror 1.0.58", "url", "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http 1.3.1", + "httparse", + "log", + "native-tls", + "rand 0.9.1", + "sha1", + "thiserror 2.0.12", + "utf-8", +] + [[package]] name = "typenum" version = "1.17.0" @@ -6094,6 +6205,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.92" @@ -6455,6 +6575,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "wormhole-sdk" version = "0.1.0" @@ -6466,7 +6595,7 @@ dependencies = [ "serde", "serde_wormhole", "sha3 0.10.8", - "thiserror", + "thiserror 1.0.58", ] [[package]] @@ -6492,7 +6621,7 @@ dependencies = [ "nom", "oid-registry", "rusticata-macros", - "thiserror", + "thiserror 1.0.58", "time", ] diff --git a/apps/hermes/server/Cargo.toml b/apps/hermes/server/Cargo.toml index 94a92b7e71..a1c81d22fe 100644 --- a/apps/hermes/server/Cargo.toml +++ b/apps/hermes/server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hermes" -version = "0.9.3" +version = "0.10.0-alpha" description = "Hermes is an agent that provides Verified Prices from the Pythnet Pyth Oracle." edition = "2021" @@ -44,6 +44,7 @@ sha3 = { version = "0.10.4" } strum = { version = "0.24.1", features = ["derive"] } tokio = { version = "1.26.0", features = ["full"] } tokio-stream = { version = "0.1.15", features = ["full"] } +tokio-tungstenite = { version = "0.26.2", features = ["native-tls"] } tonic = { version = "0.10.1", features = ["tls"] } tower-http = { version = "0.4.0", features = ["cors"] } tracing = { version = "0.1.37", features = ["log"] } diff --git a/apps/hermes/server/src/config/pythnet.rs b/apps/hermes/server/src/config/pythnet.rs index deab134a25..4857a45aee 100644 --- a/apps/hermes/server/src/config/pythnet.rs +++ b/apps/hermes/server/src/config/pythnet.rs @@ -21,4 +21,9 @@ pub struct Options { #[arg(default_value = DEFAULT_PYTHNET_ORACLE_PROGRAM_ADDR)] #[arg(env = "PYTHNET_ORACLE_PROGRAM_ADDR")] pub oracle_program_addr: Pubkey, + + /// Address of a PythNet quorum websocket RPC endpoint. + #[arg(long = "pythnet-quorum-ws-addr")] + #[arg(env = "PYTHNET_QUORUM_WS_ADDR")] + pub quorum_ws_addr: Option, } diff --git a/apps/hermes/server/src/network/pythnet.rs b/apps/hermes/server/src/network/pythnet.rs index cffc63dd03..099452edab 100644 --- a/apps/hermes/server/src/network/pythnet.rs +++ b/apps/hermes/server/src/network/pythnet.rs @@ -14,8 +14,8 @@ use { }, }, anyhow::{anyhow, bail, Result}, - borsh::BorshDeserialize, - futures::stream::StreamExt, + borsh::{BorshDeserialize, BorshSerialize}, + futures::{stream::StreamExt, SinkExt}, pyth_sdk::PriceIdentifier, pyth_sdk_solana::state::load_product_account, solana_account_decoder::UiAccountEncoding, @@ -29,6 +29,10 @@ use { }, std::{collections::BTreeMap, sync::Arc, time::Duration}, tokio::time::Instant, + tokio_tungstenite::{ + connect_async, + tungstenite::{client::IntoClientRequest, Message}, + }, }; /// Using a Solana RPC endpoint, fetches the target GuardianSet based on an index. @@ -432,10 +436,98 @@ where }) }; + let task_quorum_listener = match opts.pythnet.quorum_ws_addr { + Some(pythnet_quorum_ws_addr) => { + let store = state.clone(); + let mut exit = crate::EXIT.subscribe(); + tokio::spawn(async move { + loop { + let current_time = Instant::now(); + tokio::select! { + _ = exit.changed() => break, + Err(err) = run_quorom_listener(store.clone(), pythnet_quorum_ws_addr.clone()) => { + tracing::error!(error = ?err, "Error in Pythnet quorum network listener."); + if current_time.elapsed() < Duration::from_secs(30) { + tracing::error!("Pythnet quorum listener restarting too quickly. Sleep 1s."); + tokio::time::sleep(Duration::from_secs(1)).await; + } + } + } + } + tracing::info!("Shutting down Pythnet quorum listener..."); + }) + } + None => tokio::spawn(async { + tracing::warn!( + "Pythnet quorum websocket address not provided, skipping quorum listener." + ); + }), + }; + let _ = tokio::join!( task_listener, task_guardian_watcher, - task_price_feeds_metadata_updater + task_price_feeds_metadata_updater, + task_quorum_listener, ); Ok(()) } + +const QUORUM_PING_INTERVAL: Duration = Duration::from_secs(10); + +#[tracing::instrument(skip(state))] +async fn run_quorom_listener(state: Arc, pythnet_quorum_ws_endpoint: String) -> Result<()> +where + S: Wormhole, + S: Send + Sync + 'static, +{ + let mut ping_interval = tokio::time::interval(QUORUM_PING_INTERVAL); + let mut responded_to_ping = true; // Start with a true to not close the connection immediately + let request = pythnet_quorum_ws_endpoint.into_client_request()?; + let (mut ws_stream, _) = connect_async(request).await?; + + loop { + tokio::select! { + message = ws_stream.next() => { + let vaa_bytes = match message.ok_or_else(|| anyhow!("PythNet quorum stream terminated."))?? { + Message::Frame(_) => continue, + Message::Text(message) => { + match message.try_to_vec() { + Ok(bytes) => bytes, + Err(e) => { + tracing::error!(error = ?e, "Failed to convert PythNet quorum text message to bytes."); + continue; + } + } + }, + Message::Binary(bytes) => bytes.to_vec(), + Message::Ping(_) => continue, + Message::Pong(_) => { + responded_to_ping = true; + continue; + } + Message::Close(_) => break, + }; + tokio::spawn({ + let state = state.clone(); + async move { + if let Err(e) = state.process_message(vaa_bytes).await { + tracing::debug!(error = ?e, "Skipped VAA."); + } + } + }); + }, + _ = ping_interval.tick() => { + if !responded_to_ping { + return Err(anyhow!("PythNet quorum subscriber did not respond to ping. Closing connection.")); + } + responded_to_ping = false; // Reset the flag for the next ping + if let Err(e) = ws_stream.send(Message::Ping(vec![].into())).await { + tracing::error!(error = ?e, "Failed to send PythNet quorum ping message."); + return Err(anyhow!("Failed to send PythNet quorum ping message.")); + } + }, + } + } + Err(anyhow!("Pyth quorum stream terminated.")) +} diff --git a/apps/quorum/Cargo.lock b/apps/quorum/Cargo.lock index e355929ff3..4781d4730a 100644 --- a/apps/quorum/Cargo.lock +++ b/apps/quorum/Cargo.lock @@ -2671,7 +2671,7 @@ dependencies = [ [[package]] name = "quorum" -version = "0.1.0" +version = "0.1.1" dependencies = [ "anyhow", "axum", diff --git a/apps/quorum/Cargo.toml b/apps/quorum/Cargo.toml index 9bc979e485..af107ad141 100644 --- a/apps/quorum/Cargo.toml +++ b/apps/quorum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "quorum" -version = "0.1.0" +version = "0.1.1" edition = "2021" [dependencies] diff --git a/apps/quorum/src/api.rs b/apps/quorum/src/api.rs index f6e45cb90e..b5de1b26cc 100644 --- a/apps/quorum/src/api.rs +++ b/apps/quorum/src/api.rs @@ -149,7 +149,7 @@ async fn handle_observation( let body = params .get_body() .map_err(|e| anyhow::anyhow!("Failed to deserialize observation body: {}", e))?; - if signatures.len() > (state.guardian_set.addresses.len() * 2) / 3 + 1 { + if signatures.len() > (state.guardian_set.addresses.len() * 2) / 3 { let vaa: Vaa = ( Header { version: 1,