diff --git a/Cargo.lock b/Cargo.lock index 8dd0b4c850f..d9dd845d24c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4139,6 +4139,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 0d7a458e045..3cfb631f06c 100644 --- a/graph/src/components/store/traits.rs +++ b/graph/src/components/store/traits.rs @@ -353,8 +353,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/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 9b4a5335b04..1283a698221 100644 --- a/server/index-node/src/resolver.rs +++ b/server/index-node/src/resolver.rs @@ -228,8 +228,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, @@ -275,6 +275,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 08b58b2fb39..96d687c848b 100644 --- a/store/postgres/src/chain_store.rs +++ b/store/postgres/src/chain_store.rs @@ -69,6 +69,7 @@ mod data { use graph::prelude::{ serde_json as json, BlockNumber, BlockPtr, CachedEthereumCall, Error, StoreError, }; + use serde::Deserialize; use std::fmt; use std::iter::FromIterator; use std::{convert::TryFrom, io::Write}; @@ -593,30 +594,57 @@ mod data { &self, conn: &PgConnection, hash: &BlockHash, - ) -> Result, StoreError> { + ) -> Result)>, StoreError> { + #[derive(Deserialize)] + struct Block { + /// timestamp's representation depends the blockchain::Block implementation, on + /// ethereum this is a U256 but on different chains it will most likely be different. + /// Parsing as string should be safe for most cases as serde should convert. + timestamp: String, + } + #[derive(Deserialize)] + struct Data { + block: Block, + } + let number = match self { Storage::Shared => { use public::ethereum_blocks as b; b::table - .select(b::number) + .select((b::number, b::data)) .filter(b::hash.eq(format!("{:x}", hash))) - .first::(conn) + .first::<(i64, json::Value)>(conn) .optional()? } Storage::Private(Schema { blocks, .. }) => blocks .table() - .select(blocks.number()) + .select((blocks.number(), blocks.data())) .filter(blocks.hash().eq(hash.as_slice())) - .first::(conn) + .first::<(i64, json::Value)>(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, json)) => { + let number = BlockNumber::try_from(number) + .map_err(|e| StoreError::QueryExecutionError(e.to_string()))?; + + match json { + obj @ json::Value::Object(_) => { + let x: Option = json::from_value(obj).ok(); + return Ok(Some(( + number, + x.map(|obj| obj.block.timestamp).map(Into::into), + ))); + } + _ => {} + } + + Ok(Some((number, None))) + } + } } /// Find the first block that is missing from the database needed to @@ -1687,12 +1715,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 f03c82cd551..3d10967eafc 100644 --- a/store/postgres/src/query_store.rs +++ b/store/postgres/src/query_store.rs @@ -68,7 +68,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 458142273d3..696f7f1aa34 100644 --- a/store/postgres/tests/store.rs +++ b/store/postgres/tests/store.rs @@ -1981,6 +1981,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 5633e99f761..122f23c08c4 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),