From 4e9b80cf3ca3ec8388df33dcfd4a670528f256fa Mon Sep 17 00:00:00 2001 From: Filipe Azevedo Date: Thu, 14 Jul 2022 15:40:39 +0100 Subject: [PATCH 1/2] Add timestamp to meta block --- Cargo.lock | 1 + graph/src/components/store/traits.rs | 10 ++++++-- graphql/src/schema/meta.graphql | 2 ++ node/src/manager/commands/rewind.rs | 2 +- server/index-node/src/resolver.rs | 5 ++-- store/postgres/src/chain_store.rs | 37 ++++++++++++++++++---------- store/postgres/src/query_store.rs | 2 +- store/postgres/tests/store.rs | 34 +++++++++++++++++++++++++ store/test-store/Cargo.toml | 1 + store/test-store/src/block_store.rs | 26 ++++++++++++------- 10 files changed, 92 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f5a7a465e91..20211d35b5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4174,6 +4174,7 @@ dependencies = [ "graphql-parser", "hex-literal", "lazy_static", + "serde", ] [[package]] diff --git a/graph/src/components/store/traits.rs b/graph/src/components/store/traits.rs index 0dcbc0f8b02..f3939bfa624 100644 --- a/graph/src/components/store/traits.rs +++ b/graph/src/components/store/traits.rs @@ -361,8 +361,14 @@ pub trait ChainStore: Send + Sync + 'static { /// may purge any other blocks with that number fn confirm_block_hash(&self, number: BlockNumber, hash: &BlockHash) -> Result; - /// Find the block with `block_hash` and return the network name and number - fn block_number(&self, hash: &BlockHash) -> Result, StoreError>; + /// Find the block with `block_hash` and return the network name, number and timestamp if present. + /// Currently, the timestamp is only returned if it's present in the top level block. This format is + /// depends on the chain and the implementation of Blockchain::Block for the specific chain. + /// eg: {"block": { "timestamp": 123123123 } } + fn block_number( + &self, + hash: &BlockHash, + ) -> Result)>, StoreError>; /// Tries to retrieve all transactions receipts for a given block. async fn transaction_receipts_in_block( diff --git a/graphql/src/schema/meta.graphql b/graphql/src/schema/meta.graphql index e14280394be..1a1faf68878 100644 --- a/graphql/src/schema/meta.graphql +++ b/graphql/src/schema/meta.graphql @@ -55,6 +55,8 @@ type _Block_ { hash: Bytes "The block number" number: Int! + "Timestamp of the block if available, format depends on the chain" + timestamp: String } enum _SubgraphErrorPolicy_ { diff --git a/node/src/manager/commands/rewind.rs b/node/src/manager/commands/rewind.rs index 2224dc572b9..43bedd1387b 100644 --- a/node/src/manager/commands/rewind.rs +++ b/node/src/manager/commands/rewind.rs @@ -36,7 +36,7 @@ fn block_ptr( None => bail!("can not find chain store for {}", chain), Some(store) => store, }; - if let Some((_, number)) = chain_store.block_number(&block_ptr_to.hash)? { + if let Some((_, number, _)) = chain_store.block_number(&block_ptr_to.hash)? { if number != block_ptr_to.number { bail!( "the given hash is for block number {} but the command specified block number {}", diff --git a/server/index-node/src/resolver.rs b/server/index-node/src/resolver.rs index e0c1ab5c294..e70e60386e3 100644 --- a/server/index-node/src/resolver.rs +++ b/server/index-node/src/resolver.rs @@ -230,8 +230,8 @@ impl IndexNodeResolver { let chain_store = chain.chain_store(); let call_cache = chain.call_cache(); - let block_number = match chain_store.block_number(&block_hash) { - Ok(Some((_, n))) => n, + let (block_number, timestamp) = match chain_store.block_number(&block_hash) { + Ok(Some((_, n, timestamp))) => (n, timestamp), Ok(None) => { error!( self.logger, @@ -277,6 +277,7 @@ impl IndexNodeResolver { block: object! { hash: cached_call.block_ptr.hash.hash_hex(), number: cached_call.block_ptr.number, + timestamp: timestamp.clone(), }, contractAddress: &cached_call.contract_address[..], returnValue: &cached_call.return_value[..], diff --git a/store/postgres/src/chain_store.rs b/store/postgres/src/chain_store.rs index a0c462e6943..a41eaf85da3 100644 --- a/store/postgres/src/chain_store.rs +++ b/store/postgres/src/chain_store.rs @@ -589,34 +589,42 @@ mod data { } } + /// timestamp's representation depends the blockchain::Block implementation, on + /// ethereum this is a U256 but on different chains it will most likely be different. pub(super) fn block_number( &self, conn: &PgConnection, hash: &BlockHash, - ) -> Result, StoreError> { + ) -> Result)>, StoreError> { + const TIMESTAMP_QUERY: &str = + "coalesce(data->'block'->>'timestamp', data->>'timestamp')"; + let number = match self { Storage::Shared => { use public::ethereum_blocks as b; b::table - .select(b::number) + .select((b::number, sql(TIMESTAMP_QUERY))) .filter(b::hash.eq(format!("{:x}", hash))) - .first::(conn) + .first::<(i64, String)>(conn) .optional()? } Storage::Private(Schema { blocks, .. }) => blocks .table() - .select(blocks.number()) + .select((blocks.number(), sql(TIMESTAMP_QUERY))) .filter(blocks.hash().eq(hash.as_slice())) - .first::(conn) + .first::<(i64, String)>(conn) .optional()?, }; - number - .map(|number| { - BlockNumber::try_from(number) - .map_err(|e| StoreError::QueryExecutionError(e.to_string())) - }) - .transpose() + + match number { + None => Ok(None), + Some((number, ts)) => { + let number = BlockNumber::try_from(number) + .map_err(|e| StoreError::QueryExecutionError(e.to_string()))?; + Ok(Some((number, Some(ts)))) + } + } } /// Find the first block that is missing from the database needed to @@ -1690,12 +1698,15 @@ impl ChainStoreTrait for ChainStore { .confirm_block_hash(&conn, &self.chain, number, hash) } - fn block_number(&self, hash: &BlockHash) -> Result, StoreError> { + fn block_number( + &self, + hash: &BlockHash, + ) -> Result)>, StoreError> { let conn = self.get_conn()?; Ok(self .storage .block_number(&conn, hash)? - .map(|number| (self.chain.clone(), number))) + .map(|(number, timestamp)| (self.chain.clone(), number, timestamp))) } async fn transaction_receipts_in_block( diff --git a/store/postgres/src/query_store.rs b/store/postgres/src/query_store.rs index 6acc9dd5be9..42a0bb855dd 100644 --- a/store/postgres/src/query_store.rs +++ b/store/postgres/src/query_store.rs @@ -71,7 +71,7 @@ impl QueryStoreTrait for QueryStore { let subgraph_network = self.network_name(); self.chain_store .block_number(block_hash)? - .map(|(network_name, number)| { + .map(|(network_name, number, _timestamp)| { if network_name == subgraph_network { Ok(number) } else { diff --git a/store/postgres/tests/store.rs b/store/postgres/tests/store.rs index 7e949d206ea..e30a1055976 100644 --- a/store/postgres/tests/store.rs +++ b/store/postgres/tests/store.rs @@ -1992,6 +1992,40 @@ fn cleanup_cached_blocks() { }) } +#[test] +/// checks if retrieving the timestamp from the data blob works. +/// on ethereum, the block has timestamp as U256 so it will always have a value +fn parse_timestamp() { + const EXPECTED_TS: &str = "0x62ceae26"; + + run_test(|store, _, _| async move { + use block_store::*; + // The test subgraph is at block 2. Since we don't ever delete + // the genesis block, the only block eligible for cleanup is BLOCK_ONE + // and the first retained block is block 2. + block_store::set_chain( + vec![ + &*GENESIS_BLOCK, + &*BLOCK_ONE, + &*BLOCK_TWO, + &*BLOCK_THREE_TIMESTAMP, + ], + NETWORK_NAME, + ); + let chain_store = store + .block_store() + .chain_store(NETWORK_NAME) + .expect("fake chain store"); + + let (_network, number, timestamp) = chain_store + .block_number(&BLOCK_THREE_TIMESTAMP.block_hash()) + .expect("block_number to return correct number and timestamp") + .unwrap(); + assert_eq!(number, 3); + assert_eq!(timestamp.unwrap(), EXPECTED_TS); + }) +} + #[test] fn reorg_tracking() { async fn update_john( diff --git a/store/test-store/Cargo.toml b/store/test-store/Cargo.toml index de8fb5f0808..05da5781ae8 100644 --- a/store/test-store/Cargo.toml +++ b/store/test-store/Cargo.toml @@ -16,3 +16,4 @@ lazy_static = "1.1" hex-literal = "0.3" diesel = { version = "1.4.8", features = ["postgres", "serde_json", "numeric", "r2d2"] } graph-chain-ethereum = { path = "../../chain/ethereum" } +serde = "1.0" \ No newline at end of file diff --git a/store/test-store/src/block_store.rs b/store/test-store/src/block_store.rs index 2ab7ddbd428..54c613535bc 100644 --- a/store/test-store/src/block_store.rs +++ b/store/test-store/src/block_store.rs @@ -6,8 +6,8 @@ use graph::components::store::BlockStore; use graph::{ blockchain::Block, prelude::{ - serde_json, web3::types::H256, BlockHash, BlockNumber, BlockPtr, EthereumBlock, - LightEthereumBlock, + serde_json, web3::types::H256, web3::types::U256, BlockHash, BlockNumber, BlockPtr, + EthereumBlock, LightEthereumBlock, }, }; @@ -16,23 +16,25 @@ lazy_static! { pub static ref GENESIS_BLOCK: FakeBlock = FakeBlock { number: super::GENESIS_PTR.number, hash: super::GENESIS_PTR.hash_hex(), + timestamp: None, parent_hash: NO_PARENT.to_string() }; pub static ref BLOCK_ONE: FakeBlock = GENESIS_BLOCK - .make_child("8511fa04b64657581e3f00e14543c1d522d5d7e771b54aa3060b662ade47da13"); + .make_child("8511fa04b64657581e3f00e14543c1d522d5d7e771b54aa3060b662ade47da13", None); pub static ref BLOCK_ONE_SIBLING: FakeBlock = - GENESIS_BLOCK.make_child("b98fb783b49de5652097a989414c767824dff7e7fd765a63b493772511db81c1"); + GENESIS_BLOCK.make_child("b98fb783b49de5652097a989414c767824dff7e7fd765a63b493772511db81c1", None); pub static ref BLOCK_ONE_NO_PARENT: FakeBlock = FakeBlock::make_no_parent( 1, "7205bdfcf4521874cf38ce38c879ff967bf3a069941286bfe267109ad275a63d" ); - pub static ref BLOCK_TWO: FakeBlock = BLOCK_ONE.make_child("f8ccbd3877eb98c958614f395dd351211afb9abba187bfc1fb4ac414b099c4a6"); + pub static ref BLOCK_TWO: FakeBlock = BLOCK_ONE.make_child("f8ccbd3877eb98c958614f395dd351211afb9abba187bfc1fb4ac414b099c4a6", None); pub static ref BLOCK_TWO_NO_PARENT: FakeBlock = FakeBlock::make_no_parent(2, "3b652b00bff5e168b1218ff47593d516123261c4487629c4175f642ee56113fe"); - pub static ref BLOCK_THREE: FakeBlock = BLOCK_TWO.make_child("7347afe69254df06729e123610b00b8b11f15cfae3241f9366fb113aec07489c"); + pub static ref BLOCK_THREE: FakeBlock = BLOCK_TWO.make_child("7347afe69254df06729e123610b00b8b11f15cfae3241f9366fb113aec07489c", None); pub static ref BLOCK_THREE_NO_PARENT: FakeBlock = FakeBlock::make_no_parent(3, "fa9ebe3f74de4c56908b49f5c4044e85825f7350f3fa08a19151de82a82a7313"); - pub static ref BLOCK_FOUR: FakeBlock = BLOCK_THREE.make_child("7cce080f5a49c2997a6cc65fc1cee9910fd8fc3721b7010c0b5d0873e2ac785e"); - pub static ref BLOCK_FIVE: FakeBlock = BLOCK_FOUR.make_child("7b0ea919e258eb2b119eb32de56b85d12d50ac6a9f7c5909f843d6172c8ba196"); + pub static ref BLOCK_THREE_TIMESTAMP: FakeBlock = BLOCK_TWO.make_child("6b834521bb753c132fdcf0e1034803ed9068e324112f8750ba93580b393a986b", Some(U256::from(1657712166))); + pub static ref BLOCK_FOUR: FakeBlock = BLOCK_THREE.make_child("7cce080f5a49c2997a6cc65fc1cee9910fd8fc3721b7010c0b5d0873e2ac785e", None); + pub static ref BLOCK_FIVE: FakeBlock = BLOCK_FOUR.make_child("7b0ea919e258eb2b119eb32de56b85d12d50ac6a9f7c5909f843d6172c8ba196", None); pub static ref BLOCK_SIX_NO_PARENT: FakeBlock = FakeBlock::make_no_parent(6, "6b834521bb753c132fdcf0e1034803ed9068e324112f8750ba93580b393a986b"); } @@ -45,14 +47,16 @@ pub struct FakeBlock { pub number: BlockNumber, pub hash: String, pub parent_hash: String, + pub timestamp: Option, } impl FakeBlock { - pub fn make_child(&self, hash: &str) -> Self { + pub fn make_child(&self, hash: &str, timestamp: Option) -> Self { FakeBlock { number: self.number + 1, hash: hash.to_owned(), parent_hash: self.hash.clone(), + timestamp, } } @@ -61,6 +65,7 @@ impl FakeBlock { number, hash: hash.to_owned(), parent_hash: NO_PARENT.to_string(), + timestamp: None, } } @@ -79,6 +84,9 @@ impl FakeBlock { block.number = Some(self.number.into()); block.parent_hash = parent_hash; block.hash = Some(H256(self.block_hash().as_slice().try_into().unwrap())); + if let Some(ts) = self.timestamp { + block.timestamp = ts; + } EthereumBlock { block: Arc::new(block), From 07fb4fcf566c41a04fae9db31313e684f5a20887 Mon Sep 17 00:00:00 2001 From: Filipe Azevedo Date: Tue, 19 Jul 2022 12:55:50 +0100 Subject: [PATCH 2/2] expose timestamp on meta block --- graph/src/components/store/traits.rs | 8 +++ graphql/src/runner.rs | 2 +- graphql/src/store/resolver.rs | 84 +++++++++++++++++++++++----- graphql/src/subscription/mod.rs | 2 +- store/postgres/src/chain_store.rs | 6 +- store/postgres/src/query_store.rs | 15 +++-- store/postgres/tests/store.rs | 31 ++++++++++ store/test-store/src/block_store.rs | 21 ++++++- 8 files changed, 145 insertions(+), 24 deletions(-) diff --git a/graph/src/components/store/traits.rs b/graph/src/components/store/traits.rs index f3939bfa624..99cebdd45a2 100644 --- a/graph/src/components/store/traits.rs +++ b/graph/src/components/store/traits.rs @@ -415,6 +415,14 @@ pub trait QueryStore: Send + Sync { fn block_number(&self, block_hash: &BlockHash) -> Result, StoreError>; + /// Returns the blocknumber as well as the timestamp. Timestamp depends on the chain block type + /// and can have multiple formats, it can also not be prevent. For now this is only available + /// for EVM chains both firehose and rpc. + fn block_number_with_timestamp( + &self, + block_hash: &BlockHash, + ) -> Result)>, StoreError>; + fn wait_stats(&self) -> Result; async fn has_deterministic_errors(&self, block: BlockNumber) -> Result; diff --git a/graphql/src/runner.rs b/graphql/src/runner.rs index 6dce3b74c60..968c4263d00 100644 --- a/graphql/src/runner.rs +++ b/graphql/src/runner.rs @@ -161,7 +161,7 @@ where let query_res = execute_query( query.clone(), Some(selection_set), - resolver.block_ptr.clone(), + resolver.block_ptr.as_ref().map(Into::into).clone(), QueryExecutionOptions { resolver, deadline: ENV_VARS.graphql.query_timeout.map(|t| Instant::now() + t), diff --git a/graphql/src/store/resolver.rs b/graphql/src/store/resolver.rs index 6769a9212e4..69576cd9641 100644 --- a/graphql/src/store/resolver.rs +++ b/graphql/src/store/resolver.rs @@ -26,13 +26,34 @@ pub struct StoreResolver { logger: Logger, pub(crate) store: Arc, subscription_manager: Arc, - pub(crate) block_ptr: Option, + pub(crate) block_ptr: Option, deployment: DeploymentHash, has_non_fatal_errors: bool, error_policy: ErrorPolicy, graphql_metrics: Arc, } +#[derive(Clone, Debug)] +pub(crate) struct BlockPtrTs { + pub ptr: BlockPtr, + pub timestamp: Option, +} + +impl From for BlockPtrTs { + fn from(ptr: BlockPtr) -> Self { + Self { + ptr, + timestamp: None, + } + } +} + +impl From<&BlockPtrTs> for BlockPtr { + fn from(ptr: &BlockPtrTs) -> Self { + ptr.ptr.cheap_clone() + } +} + impl CheapClone for StoreResolver {} impl StoreResolver { @@ -80,7 +101,7 @@ impl StoreResolver { let block_ptr = Self::locate_block(store_clone.as_ref(), bc, state).await?; let has_non_fatal_errors = store - .has_deterministic_errors(block_ptr.block_number()) + .has_deterministic_errors(block_ptr.ptr.block_number()) .await?; let resolver = StoreResolver { @@ -99,15 +120,16 @@ impl StoreResolver { pub fn block_number(&self) -> BlockNumber { self.block_ptr .as_ref() - .map(|ptr| ptr.number as BlockNumber) + .map(|ptr| ptr.ptr.number as BlockNumber) .unwrap_or(BLOCK_NUMBER_MAX) } + /// locate_block returns the block pointer and it's timestamp when available. async fn locate_block( store: &dyn QueryStore, bc: BlockConstraint, state: &DeploymentState, - ) -> Result { + ) -> Result { fn block_queryable( state: &DeploymentState, block: BlockNumber, @@ -117,23 +139,39 @@ impl StoreResolver { .map_err(|msg| QueryExecutionError::ValueParseError("block.number".to_owned(), msg)) } + fn get_block_ts( + store: &dyn QueryStore, + ptr: &BlockPtr, + ) -> Result, QueryExecutionError> { + match store + .block_number_with_timestamp(&ptr.hash) + .map_err(Into::::into)? + { + Some((_, Some(ts))) => Ok(Some(ts)), + _ => Ok(None), + } + } + match bc { BlockConstraint::Hash(hash) => { let ptr = store - .block_number(&hash) + .block_number_with_timestamp(&hash) .map_err(Into::into) - .and_then(|number| { - number + .and_then(|result| { + result .ok_or_else(|| { QueryExecutionError::ValueParseError( "block.hash".to_owned(), "no block with that hash found".to_owned(), ) }) - .map(|number| BlockPtr::new(hash, number)) + .map(|(number, ts)| BlockPtrTs { + ptr: BlockPtr::new(hash, number), + timestamp: ts, + }) })?; - block_queryable(state, ptr.number)?; + block_queryable(state, ptr.ptr.number)?; Ok(ptr) } BlockConstraint::Number(number) => { @@ -144,7 +182,7 @@ impl StoreResolver { // always return an all zeroes hash when users specify // a block number // See 7a7b9708-adb7-4fc2-acec-88680cb07ec1 - Ok(BlockPtr::from((web3::types::H256::zero(), number as u64))) + Ok(BlockPtr::from((web3::types::H256::zero(), number as u64)).into()) } BlockConstraint::Min(min) => { let ptr = state.latest_block.cheap_clone(); @@ -158,9 +196,18 @@ impl StoreResolver { ), )); } - Ok(ptr) + let timestamp = get_block_ts(store, &state.latest_block)?; + + Ok(BlockPtrTs { ptr, timestamp }) + } + BlockConstraint::Latest => { + let timestamp = get_block_ts(store, &state.latest_block)?; + + Ok(BlockPtrTs { + ptr: state.latest_block.cheap_clone(), + timestamp, + }) } - BlockConstraint::Latest => Ok(state.latest_block.cheap_clone()), } } @@ -181,7 +228,7 @@ impl StoreResolver { // locate_block indicates that we do not have a block hash // by setting the hash to `zero` // See 7a7b9708-adb7-4fc2-acec-88680cb07ec1 - let hash_h256 = ptr.hash_as_h256(); + let hash_h256 = ptr.ptr.hash_as_h256(); if hash_h256 == web3::types::H256::zero() { None } else { @@ -192,12 +239,21 @@ impl StoreResolver { let number = self .block_ptr .as_ref() - .map(|ptr| r::Value::Int((ptr.number as i32).into())) + .map(|ptr| r::Value::Int((ptr.ptr.number as i32).into())) .unwrap_or(r::Value::Null); + + let timestamp = self.block_ptr.as_ref().map(|ptr| { + ptr.timestamp + .clone() + .map(|ts| r::Value::String(ts)) + .unwrap_or(r::Value::Null) + }); + let mut map = BTreeMap::new(); let block = object! { hash: hash, number: number, + timestamp: timestamp, __typename: BLOCK_FIELD_TYPE }; map.insert("prefetch:block".into(), r::Value::List(vec![block])); diff --git a/graphql/src/subscription/mod.rs b/graphql/src/subscription/mod.rs index a6d405dbc0c..c07bf27d209 100644 --- a/graphql/src/subscription/mod.rs +++ b/graphql/src/subscription/mod.rs @@ -211,7 +211,7 @@ async fn execute_subscription_event( Err(e) => return Arc::new(e.into()), }; - let block_ptr = resolver.block_ptr.clone(); + let block_ptr = resolver.block_ptr.as_ref().map(Into::into); // Create a fresh execution context with deadline. let ctx = Arc::new(ExecutionContext { diff --git a/store/postgres/src/chain_store.rs b/store/postgres/src/chain_store.rs index a41eaf85da3..b14a1983f3d 100644 --- a/store/postgres/src/chain_store.rs +++ b/store/postgres/src/chain_store.rs @@ -606,14 +606,14 @@ mod data { b::table .select((b::number, sql(TIMESTAMP_QUERY))) .filter(b::hash.eq(format!("{:x}", hash))) - .first::<(i64, String)>(conn) + .first::<(i64, Option)>(conn) .optional()? } Storage::Private(Schema { blocks, .. }) => blocks .table() .select((blocks.number(), sql(TIMESTAMP_QUERY))) .filter(blocks.hash().eq(hash.as_slice())) - .first::<(i64, String)>(conn) + .first::<(i64, Option)>(conn) .optional()?, }; @@ -622,7 +622,7 @@ mod data { Some((number, ts)) => { let number = BlockNumber::try_from(number) .map_err(|e| StoreError::QueryExecutionError(e.to_string()))?; - Ok(Some((number, Some(ts)))) + Ok(Some((number, ts))) } } } diff --git a/store/postgres/src/query_store.rs b/store/postgres/src/query_store.rs index 42a0bb855dd..82d79cca5a7 100644 --- a/store/postgres/src/query_store.rs +++ b/store/postgres/src/query_store.rs @@ -60,8 +60,10 @@ impl QueryStoreTrait for QueryStore { async fn block_ptr(&self) -> Result, StoreError> { self.store.block_ptr(self.site.cheap_clone()).await } - - fn block_number(&self, block_hash: &BlockHash) -> Result, StoreError> { + fn block_number_with_timestamp( + &self, + block_hash: &BlockHash, + ) -> Result)>, StoreError> { // We should also really check that the block with the given hash is // on the chain starting at the subgraph's current head. That check is // very expensive though with the data structures we have currently @@ -71,9 +73,9 @@ impl QueryStoreTrait for QueryStore { let subgraph_network = self.network_name(); self.chain_store .block_number(block_hash)? - .map(|(network_name, number, _timestamp)| { + .map(|(network_name, number, timestamp)| { if network_name == subgraph_network { - Ok(number) + Ok((number, timestamp)) } else { Err(StoreError::QueryExecutionError(format!( "subgraph {} belongs to network {} but block {:x} belongs to network {}", @@ -84,6 +86,11 @@ impl QueryStoreTrait for QueryStore { .transpose() } + fn block_number(&self, block_hash: &BlockHash) -> Result, StoreError> { + self.block_number_with_timestamp(block_hash) + .map(|opt| opt.map(|(number, _)| number)) + } + fn wait_stats(&self) -> Result { self.store.wait_stats(self.replica_id) } diff --git a/store/postgres/tests/store.rs b/store/postgres/tests/store.rs index e30a1055976..ba27bf24fd8 100644 --- a/store/postgres/tests/store.rs +++ b/store/postgres/tests/store.rs @@ -2026,6 +2026,37 @@ fn parse_timestamp() { }) } +#[test] +/// checks if retrieving the timestamp from the data blob works. +/// on ethereum, the block has timestamp as U256 so it will always have a value +fn parse_null_timestamp() { + run_test(|store, _, _| async move { + use block_store::*; + // The test subgraph is at block 2. Since we don't ever delete + // the genesis block, the only block eligible for cleanup is BLOCK_ONE + // and the first retained block is block 2. + block_store::set_chain( + vec![ + &*GENESIS_BLOCK, + &*BLOCK_ONE, + &*BLOCK_TWO, + &*BLOCK_THREE_NO_TIMESTAMP, + ], + NETWORK_NAME, + ); + let chain_store = store + .block_store() + .chain_store(NETWORK_NAME) + .expect("fake chain store"); + + let (_network, number, timestamp) = chain_store + .block_number(&BLOCK_THREE_NO_TIMESTAMP.block_hash()) + .expect("block_number to return correct number and timestamp") + .unwrap(); + assert_eq!(number, 3); + assert_eq!(true, timestamp.is_none()); + }) +} #[test] fn reorg_tracking() { async fn update_john( diff --git a/store/test-store/src/block_store.rs b/store/test-store/src/block_store.rs index 54c613535bc..dafb2aebed5 100644 --- a/store/test-store/src/block_store.rs +++ b/store/test-store/src/block_store.rs @@ -33,6 +33,9 @@ lazy_static! { pub static ref BLOCK_THREE: FakeBlock = BLOCK_TWO.make_child("7347afe69254df06729e123610b00b8b11f15cfae3241f9366fb113aec07489c", None); pub static ref BLOCK_THREE_NO_PARENT: FakeBlock = FakeBlock::make_no_parent(3, "fa9ebe3f74de4c56908b49f5c4044e85825f7350f3fa08a19151de82a82a7313"); pub static ref BLOCK_THREE_TIMESTAMP: FakeBlock = BLOCK_TWO.make_child("6b834521bb753c132fdcf0e1034803ed9068e324112f8750ba93580b393a986b", Some(U256::from(1657712166))); + // This block is special and serializes in a slightly different way, this is needed to simulate non-ethereum behaviour at the store level. If you're not sure + // what you are doing, don't use this block for other tests. + pub static ref BLOCK_THREE_NO_TIMESTAMP: FakeBlock = BLOCK_TWO.make_child("6b834521bb753c132fdcf0e1034803ed9068e324112f8750ba93580b393a986b", None); pub static ref BLOCK_FOUR: FakeBlock = BLOCK_THREE.make_child("7cce080f5a49c2997a6cc65fc1cee9910fd8fc3721b7010c0b5d0873e2ac785e", None); pub static ref BLOCK_FIVE: FakeBlock = BLOCK_FOUR.make_child("7b0ea919e258eb2b119eb32de56b85d12d50ac6a9f7c5909f843d6172c8ba196", None); pub static ref BLOCK_SIX_NO_PARENT: FakeBlock = FakeBlock::make_no_parent(6, "6b834521bb753c132fdcf0e1034803ed9068e324112f8750ba93580b393a986b"); @@ -112,7 +115,23 @@ impl Block for FakeBlock { } fn data(&self) -> Result { - serde_json::to_value(self.as_ethereum_block()) + let mut value: serde_json::Value = serde_json::to_value(self.as_ethereum_block())?; + if !self.eq(&BLOCK_THREE_NO_TIMESTAMP) { + return Ok(value); + }; + + // Remove the timestamp for block BLOCK_THREE_NO_TIMESTAMP in order to simulate the non EVM behaviour + // In these cases timestamp is not there at all but LightEthereumBlock uses U256 as timestamp so it + // can never be null and therefore impossible to test without manipulating the JSON blob directly. + if let serde_json::Value::Object(ref mut map) = value { + map.entry("block").and_modify(|ref mut block| { + if let serde_json::Value::Object(ref mut block) = block { + block.remove_entry("timestamp"); + } + }); + }; + + Ok(value) } }