diff --git a/apps/fortuna/Cargo.lock b/apps/fortuna/Cargo.lock index 99205a5472..f676901674 100644 --- a/apps/fortuna/Cargo.lock +++ b/apps/fortuna/Cargo.lock @@ -1647,7 +1647,7 @@ dependencies = [ [[package]] name = "fortuna" -version = "7.6.2" +version = "7.6.3" dependencies = [ "anyhow", "axum", diff --git a/apps/fortuna/Cargo.toml b/apps/fortuna/Cargo.toml index 18b1ef36d4..19852b1094 100644 --- a/apps/fortuna/Cargo.toml +++ b/apps/fortuna/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fortuna" -version = "7.6.2" +version = "7.6.3" edition = "2021" [lib] diff --git a/apps/fortuna/src/api.rs b/apps/fortuna/src/api.rs index 9334cb48f1..f960bad0ba 100644 --- a/apps/fortuna/src/api.rs +++ b/apps/fortuna/src/api.rs @@ -2,7 +2,7 @@ use { crate::{ chain::reader::{BlockNumber, BlockStatus, EntropyReader}, history::History, - state::HashChainState, + state::MonitoredHashChainState, }, anyhow::Result, axum::{ @@ -91,7 +91,7 @@ pub struct BlockchainState { /// Obtained from the response of eth_chainId rpc call pub network_id: u64, /// The hash chain(s) required to serve random numbers for this blockchain - pub state: Arc, + pub state: Arc, /// The contract that the server is fulfilling requests for. pub contract: Arc, /// The address of the provider that this server is operating for. @@ -212,7 +212,7 @@ mod test { }, chain::reader::{mock::MockEntropyReader, BlockStatus}, history::History, - state::{HashChainState, PebbleHashChain}, + state::{HashChainState, MonitoredHashChainState, PebbleHashChain}, }, axum::http::StatusCode, axum_test::{TestResponse, TestServer}, @@ -241,10 +241,17 @@ mod test { async fn test_server() -> (TestServer, Arc, Arc) { let eth_read = Arc::new(MockEntropyReader::with_requests(10, &[])); + let eth_state = MonitoredHashChainState::new( + ETH_CHAIN.clone(), + Default::default(), + "ethereum".into(), + PROVIDER, + ); + let eth_state = BlockchainState { id: "ethereum".into(), network_id: 1, - state: ETH_CHAIN.clone(), + state: Arc::new(eth_state), contract: eth_read.clone(), provider_address: PROVIDER, reveal_delay_blocks: 1, @@ -255,10 +262,17 @@ mod test { let avax_read = Arc::new(MockEntropyReader::with_requests(10, &[])); + let avax_state = MonitoredHashChainState::new( + AVAX_CHAIN.clone(), + Default::default(), + "avalanche".into(), + PROVIDER, + ); + let avax_state = BlockchainState { id: "avalanche".into(), network_id: 43114, - state: AVAX_CHAIN.clone(), + state: Arc::new(avax_state), contract: avax_read.clone(), provider_address: PROVIDER, reveal_delay_blocks: 2, diff --git a/apps/fortuna/src/chain/ethereum.rs b/apps/fortuna/src/chain/ethereum.rs index aaa92352dc..943f0f36b3 100644 --- a/apps/fortuna/src/chain/ethereum.rs +++ b/apps/fortuna/src/chain/ethereum.rs @@ -309,6 +309,7 @@ impl EntropyReader for PythRandom> { let mut event = self.requested_with_callback_filter(); event.filter = event .filter + .address(self.address()) .from_block(from_block) .to_block(to_block) .topic1(provider); diff --git a/apps/fortuna/src/command/generate.rs b/apps/fortuna/src/command/generate.rs index 825c1f1386..8ff848766c 100644 --- a/apps/fortuna/src/command/generate.rs +++ b/apps/fortuna/src/command/generate.rs @@ -48,6 +48,7 @@ pub async fn generate(opts: &GenerateOptions) -> Result<()> { let mut event = contract.revealed_with_callback_filter(); event.filter = event .filter + .address(contract.address()) .from_block(last_block_number) .to_block(current_block_number); diff --git a/apps/fortuna/src/command/run.rs b/apps/fortuna/src/command/run.rs index c3bfe66e1e..cd2fd65d7b 100644 --- a/apps/fortuna/src/command/run.rs +++ b/apps/fortuna/src/command/run.rs @@ -7,7 +7,7 @@ use { eth_utils::traced_client::RpcMetrics, history::History, keeper::{self, keeper_metrics::KeeperMetrics}, - state::{HashChainState, PebbleHashChain}, + state::{HashChainState, MonitoredHashChainState, PebbleHashChain}, }, anyhow::{anyhow, Error, Result}, axum::Router, @@ -183,6 +183,7 @@ async fn setup_chain_and_run_keeper( chain_id, &chain_config, rpc_metrics.clone(), + keeper_metrics.clone(), ) .await?; chains.write().await.insert( @@ -210,6 +211,7 @@ async fn setup_chain_state( chain_id: &ChainId, chain_config: &EthereumConfig, rpc_metrics: Arc, + keeper_metrics: Arc, ) -> Result { let contract = Arc::new(InstrumentedPythContract::from_config( chain_config, @@ -284,10 +286,7 @@ async fn setup_chain_state( hash_chains.push(pebble_hash_chain); } - let chain_state = HashChainState { - offsets, - hash_chains, - }; + let chain_state = HashChainState::new(offsets, hash_chains)?; if chain_state.reveal(provider_info.original_commitment_sequence_number)? != provider_info.original_commitment @@ -297,9 +296,16 @@ async fn setup_chain_state( tracing::info!("Root of chain id {} matches commitment", &chain_id); } + let monitored_chain_state = MonitoredHashChainState::new( + Arc::new(chain_state), + keeper_metrics.clone(), + chain_id.clone(), + *provider, + ); + let state = BlockchainState { id: chain_id.clone(), - state: Arc::new(chain_state), + state: Arc::new(monitored_chain_state), network_id, contract, provider_address: *provider, diff --git a/apps/fortuna/src/command/setup_provider.rs b/apps/fortuna/src/command/setup_provider.rs index 4096b6b42d..a0f6ff2737 100644 --- a/apps/fortuna/src/command/setup_provider.rs +++ b/apps/fortuna/src/command/setup_provider.rs @@ -122,12 +122,12 @@ async fn setup_chain_provider( provider_config.chain_sample_interval, ) .await?; - let chain_state = HashChainState { - offsets: vec![provider_info + let chain_state = HashChainState::new( + vec![provider_info .original_commitment_sequence_number .try_into()?], - hash_chains: vec![hash_chain], - }; + vec![hash_chain], + )?; if chain_state.reveal(provider_info.original_commitment_sequence_number)? != provider_info.original_commitment diff --git a/apps/fortuna/src/keeper/keeper_metrics.rs b/apps/fortuna/src/keeper/keeper_metrics.rs index 8e0a13ef87..f1c37f0cf7 100644 --- a/apps/fortuna/src/keeper/keeper_metrics.rs +++ b/apps/fortuna/src/keeper/keeper_metrics.rs @@ -41,6 +41,7 @@ pub struct KeeperMetrics { pub final_gas_multiplier: Family, pub final_fee_multiplier: Family, pub gas_price_estimate: Family>, + pub highest_revealed_sequence_number: Family, pub accrued_pyth_fees: Family>, pub block_timestamp_lag: Family, pub latest_block_timestamp: Family, @@ -88,6 +89,7 @@ impl Default for KeeperMetrics { Histogram::new(vec![100.0, 110.0, 120.0, 140.0, 160.0, 180.0, 200.0].into_iter()) }), gas_price_estimate: Family::default(), + highest_revealed_sequence_number: Family::default(), accrued_pyth_fees: Family::default(), block_timestamp_lag: Family::default(), latest_block_timestamp: Family::default(), @@ -223,6 +225,12 @@ impl KeeperMetrics { keeper_metrics.gas_price_estimate.clone(), ); + writable_registry.register( + "highest_revealed_sequence_number", + "The highest sequence number revealed by the keeper either via callbacks or manual reveal", + keeper_metrics.highest_revealed_sequence_number.clone(), + ); + writable_registry.register( "accrued_pyth_fees", "Accrued Pyth fees on the contract", diff --git a/apps/fortuna/src/state.rs b/apps/fortuna/src/state.rs index 7bd7aeb453..3d4c756d7f 100644 --- a/apps/fortuna/src/state.rs +++ b/apps/fortuna/src/state.rs @@ -1,8 +1,12 @@ use { - crate::api::ChainId, + crate::{ + api::ChainId, + keeper::keeper_metrics::{AccountLabel, KeeperMetrics}, + }, anyhow::{ensure, Result}, ethers::types::Address, sha3::{Digest, Keccak256}, + std::sync::Arc, tokio::task::spawn_blocking, }; @@ -127,11 +131,22 @@ impl PebbleHashChain { /// which requires tracking multiple hash chains here. pub struct HashChainState { // The sequence number where the hash chain starts. Must be stored in sorted order. - pub offsets: Vec, - pub hash_chains: Vec, + offsets: Vec, + hash_chains: Vec, } impl HashChainState { + pub fn new(offsets: Vec, hash_chains: Vec) -> Result { + if offsets.len() != hash_chains.len() { + return Err(anyhow::anyhow!( + "Offsets and hash chains must have the same length." + )); + } + Ok(HashChainState { + offsets, + hash_chains, + }) + } pub fn from_chain_at_offset(offset: usize, chain: PebbleHashChain) -> HashChainState { HashChainState { offsets: vec![offset], @@ -152,12 +167,54 @@ impl HashChainState { } } +pub struct MonitoredHashChainState { + hash_chain_state: Arc, + metrics: Arc, + account_label: AccountLabel, +} +impl MonitoredHashChainState { + pub fn new( + hash_chain_state: Arc, + metrics: Arc, + chain_id: ChainId, + provider_address: Address, + ) -> Self { + Self { + hash_chain_state, + metrics, + account_label: AccountLabel { + chain_id, + address: provider_address.to_string(), + }, + } + } + + pub fn reveal(&self, sequence_number: u64) -> Result<[u8; 32]> { + let res = self.hash_chain_state.reveal(sequence_number); + if res.is_ok() { + let metric = self + .metrics + .highest_revealed_sequence_number + .get_or_create(&self.account_label); + if metric.get() < sequence_number as i64 { + metric.set(sequence_number as i64); + } + } + res + } +} + #[cfg(test)] mod test { use { - crate::state::{HashChainState, PebbleHashChain}, + crate::{ + keeper::keeper_metrics::{AccountLabel, KeeperMetrics}, + state::{HashChainState, MonitoredHashChainState, PebbleHashChain}, + }, anyhow::Result, + ethers::types::Address, sha3::{Digest, Keccak256}, + std::{sync::Arc, vec}, }; fn run_hash_chain_test(secret: [u8; 32], length: usize, sample_interval: usize) { @@ -294,4 +351,65 @@ mod test { Ok(()) } + #[test] + fn test_inconsistent_lengths() -> Result<()> { + let chain1 = PebbleHashChain::new([0u8; 32], 10, 1); + let chain2 = PebbleHashChain::new([1u8; 32], 10, 1); + + let hash_chain_state = HashChainState::new(vec![5], vec![chain1.clone(), chain2.clone()]); + assert!(hash_chain_state.is_err()); + let hash_chain_state = HashChainState::new(vec![5, 10], vec![chain1.clone()]); + assert!(hash_chain_state.is_err()); + let hash_chain_state = + HashChainState::new(vec![5, 10], vec![chain1.clone(), chain2.clone()]); + assert!(hash_chain_state.is_ok()); + + Ok(()) + } + + #[test] + fn test_highest_revealed_sequence_number() { + let chain = PebbleHashChain::new([0u8; 32], 100, 1); + let hash_chain_state = HashChainState::new(vec![0], vec![chain]).unwrap(); + let metrics = Arc::new(KeeperMetrics::default()); + let provider = Address::random(); + let monitored = MonitoredHashChainState::new( + Arc::new(hash_chain_state), + metrics.clone(), + "ethereum".to_string(), + provider, + ); + let label = AccountLabel { + chain_id: "ethereum".to_string(), + address: provider.to_string(), + }; + + assert!(monitored.reveal(5).is_ok()); + let current = metrics + .highest_revealed_sequence_number + .get_or_create(&label) + .get(); + assert_eq!(current, 5); + + assert!(monitored.reveal(15).is_ok()); + let current = metrics + .highest_revealed_sequence_number + .get_or_create(&label) + .get(); + assert_eq!(current, 15); + + assert!(monitored.reveal(10).is_ok()); + let current = metrics + .highest_revealed_sequence_number + .get_or_create(&label) + .get(); + assert_eq!(current, 15); + + assert!(monitored.reveal(1000).is_err()); + let current = metrics + .highest_revealed_sequence_number + .get_or_create(&label) + .get(); + assert_eq!(current, 15); + } }