Skip to content

Commit 0538112

Browse files
mangasKai Wetlesen
authored and
Kai Wetlesen
committed
Add timestamp to meta block (graphprotocol#3738)
* Add timestamp to meta block * expose timestamp on meta block
1 parent 38a2e28 commit 0538112

File tree

13 files changed

+233
-48
lines changed

13 files changed

+233
-48
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

graph/src/components/store/traits.rs

+16-2
Original file line numberDiff line numberDiff line change
@@ -361,8 +361,14 @@ pub trait ChainStore: Send + Sync + 'static {
361361
/// may purge any other blocks with that number
362362
fn confirm_block_hash(&self, number: BlockNumber, hash: &BlockHash) -> Result<usize, Error>;
363363

364-
/// Find the block with `block_hash` and return the network name and number
365-
fn block_number(&self, hash: &BlockHash) -> Result<Option<(String, BlockNumber)>, StoreError>;
364+
/// Find the block with `block_hash` and return the network name, number and timestamp if present.
365+
/// Currently, the timestamp is only returned if it's present in the top level block. This format is
366+
/// depends on the chain and the implementation of Blockchain::Block for the specific chain.
367+
/// eg: {"block": { "timestamp": 123123123 } }
368+
fn block_number(
369+
&self,
370+
hash: &BlockHash,
371+
) -> Result<Option<(String, BlockNumber, Option<String>)>, StoreError>;
366372

367373
/// Tries to retrieve all transactions receipts for a given block.
368374
async fn transaction_receipts_in_block(
@@ -409,6 +415,14 @@ pub trait QueryStore: Send + Sync {
409415

410416
fn block_number(&self, block_hash: &BlockHash) -> Result<Option<BlockNumber>, StoreError>;
411417

418+
/// Returns the blocknumber as well as the timestamp. Timestamp depends on the chain block type
419+
/// and can have multiple formats, it can also not be prevent. For now this is only available
420+
/// for EVM chains both firehose and rpc.
421+
fn block_number_with_timestamp(
422+
&self,
423+
block_hash: &BlockHash,
424+
) -> Result<Option<(BlockNumber, Option<String>)>, StoreError>;
425+
412426
fn wait_stats(&self) -> Result<PoolWaitStats, StoreError>;
413427

414428
async fn has_deterministic_errors(&self, block: BlockNumber) -> Result<bool, StoreError>;

graphql/src/runner.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ where
161161
let query_res = execute_query(
162162
query.clone(),
163163
Some(selection_set),
164-
resolver.block_ptr.clone(),
164+
resolver.block_ptr.as_ref().map(Into::into).clone(),
165165
QueryExecutionOptions {
166166
resolver,
167167
deadline: ENV_VARS.graphql.query_timeout.map(|t| Instant::now() + t),

graphql/src/schema/meta.graphql

+2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ type _Block_ {
5555
hash: Bytes
5656
"The block number"
5757
number: Int!
58+
"Timestamp of the block if available, format depends on the chain"
59+
timestamp: String
5860
}
5961

6062
enum _SubgraphErrorPolicy_ {

graphql/src/store/resolver.rs

+70-14
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,34 @@ pub struct StoreResolver {
2626
logger: Logger,
2727
pub(crate) store: Arc<dyn QueryStore>,
2828
subscription_manager: Arc<dyn SubscriptionManager>,
29-
pub(crate) block_ptr: Option<BlockPtr>,
29+
pub(crate) block_ptr: Option<BlockPtrTs>,
3030
deployment: DeploymentHash,
3131
has_non_fatal_errors: bool,
3232
error_policy: ErrorPolicy,
3333
graphql_metrics: Arc<GraphQLMetrics>,
3434
}
3535

36+
#[derive(Clone, Debug)]
37+
pub(crate) struct BlockPtrTs {
38+
pub ptr: BlockPtr,
39+
pub timestamp: Option<String>,
40+
}
41+
42+
impl From<BlockPtr> for BlockPtrTs {
43+
fn from(ptr: BlockPtr) -> Self {
44+
Self {
45+
ptr,
46+
timestamp: None,
47+
}
48+
}
49+
}
50+
51+
impl From<&BlockPtrTs> for BlockPtr {
52+
fn from(ptr: &BlockPtrTs) -> Self {
53+
ptr.ptr.cheap_clone()
54+
}
55+
}
56+
3657
impl CheapClone for StoreResolver {}
3758

3859
impl StoreResolver {
@@ -80,7 +101,7 @@ impl StoreResolver {
80101
let block_ptr = Self::locate_block(store_clone.as_ref(), bc, state).await?;
81102

82103
let has_non_fatal_errors = store
83-
.has_deterministic_errors(block_ptr.block_number())
104+
.has_deterministic_errors(block_ptr.ptr.block_number())
84105
.await?;
85106

86107
let resolver = StoreResolver {
@@ -99,15 +120,16 @@ impl StoreResolver {
99120
pub fn block_number(&self) -> BlockNumber {
100121
self.block_ptr
101122
.as_ref()
102-
.map(|ptr| ptr.number as BlockNumber)
123+
.map(|ptr| ptr.ptr.number as BlockNumber)
103124
.unwrap_or(BLOCK_NUMBER_MAX)
104125
}
105126

127+
/// locate_block returns the block pointer and it's timestamp when available.
106128
async fn locate_block(
107129
store: &dyn QueryStore,
108130
bc: BlockConstraint,
109131
state: &DeploymentState,
110-
) -> Result<BlockPtr, QueryExecutionError> {
132+
) -> Result<BlockPtrTs, QueryExecutionError> {
111133
fn block_queryable(
112134
state: &DeploymentState,
113135
block: BlockNumber,
@@ -117,23 +139,39 @@ impl StoreResolver {
117139
.map_err(|msg| QueryExecutionError::ValueParseError("block.number".to_owned(), msg))
118140
}
119141

142+
fn get_block_ts(
143+
store: &dyn QueryStore,
144+
ptr: &BlockPtr,
145+
) -> Result<Option<String>, QueryExecutionError> {
146+
match store
147+
.block_number_with_timestamp(&ptr.hash)
148+
.map_err(Into::<QueryExecutionError>::into)?
149+
{
150+
Some((_, Some(ts))) => Ok(Some(ts)),
151+
_ => Ok(None),
152+
}
153+
}
154+
120155
match bc {
121156
BlockConstraint::Hash(hash) => {
122157
let ptr = store
123-
.block_number(&hash)
158+
.block_number_with_timestamp(&hash)
124159
.map_err(Into::into)
125-
.and_then(|number| {
126-
number
160+
.and_then(|result| {
161+
result
127162
.ok_or_else(|| {
128163
QueryExecutionError::ValueParseError(
129164
"block.hash".to_owned(),
130165
"no block with that hash found".to_owned(),
131166
)
132167
})
133-
.map(|number| BlockPtr::new(hash, number))
168+
.map(|(number, ts)| BlockPtrTs {
169+
ptr: BlockPtr::new(hash, number),
170+
timestamp: ts,
171+
})
134172
})?;
135173

136-
block_queryable(state, ptr.number)?;
174+
block_queryable(state, ptr.ptr.number)?;
137175
Ok(ptr)
138176
}
139177
BlockConstraint::Number(number) => {
@@ -144,7 +182,7 @@ impl StoreResolver {
144182
// always return an all zeroes hash when users specify
145183
// a block number
146184
// See 7a7b9708-adb7-4fc2-acec-88680cb07ec1
147-
Ok(BlockPtr::from((web3::types::H256::zero(), number as u64)))
185+
Ok(BlockPtr::from((web3::types::H256::zero(), number as u64)).into())
148186
}
149187
BlockConstraint::Min(min) => {
150188
let ptr = state.latest_block.cheap_clone();
@@ -158,9 +196,18 @@ impl StoreResolver {
158196
),
159197
));
160198
}
161-
Ok(ptr)
199+
let timestamp = get_block_ts(store, &state.latest_block)?;
200+
201+
Ok(BlockPtrTs { ptr, timestamp })
202+
}
203+
BlockConstraint::Latest => {
204+
let timestamp = get_block_ts(store, &state.latest_block)?;
205+
206+
Ok(BlockPtrTs {
207+
ptr: state.latest_block.cheap_clone(),
208+
timestamp,
209+
})
162210
}
163-
BlockConstraint::Latest => Ok(state.latest_block.cheap_clone()),
164211
}
165212
}
166213

@@ -181,7 +228,7 @@ impl StoreResolver {
181228
// locate_block indicates that we do not have a block hash
182229
// by setting the hash to `zero`
183230
// See 7a7b9708-adb7-4fc2-acec-88680cb07ec1
184-
let hash_h256 = ptr.hash_as_h256();
231+
let hash_h256 = ptr.ptr.hash_as_h256();
185232
if hash_h256 == web3::types::H256::zero() {
186233
None
187234
} else {
@@ -192,12 +239,21 @@ impl StoreResolver {
192239
let number = self
193240
.block_ptr
194241
.as_ref()
195-
.map(|ptr| r::Value::Int((ptr.number as i32).into()))
242+
.map(|ptr| r::Value::Int((ptr.ptr.number as i32).into()))
196243
.unwrap_or(r::Value::Null);
244+
245+
let timestamp = self.block_ptr.as_ref().map(|ptr| {
246+
ptr.timestamp
247+
.clone()
248+
.map(|ts| r::Value::String(ts))
249+
.unwrap_or(r::Value::Null)
250+
});
251+
197252
let mut map = BTreeMap::new();
198253
let block = object! {
199254
hash: hash,
200255
number: number,
256+
timestamp: timestamp,
201257
__typename: BLOCK_FIELD_TYPE
202258
};
203259
map.insert("prefetch:block".into(), r::Value::List(vec![block]));

graphql/src/subscription/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ async fn execute_subscription_event(
211211
Err(e) => return Arc::new(e.into()),
212212
};
213213

214-
let block_ptr = resolver.block_ptr.clone();
214+
let block_ptr = resolver.block_ptr.as_ref().map(Into::into);
215215

216216
// Create a fresh execution context with deadline.
217217
let ctx = Arc::new(ExecutionContext {

node/src/manager/commands/rewind.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ fn block_ptr(
3636
None => bail!("can not find chain store for {}", chain),
3737
Some(store) => store,
3838
};
39-
if let Some((_, number)) = chain_store.block_number(&block_ptr_to.hash)? {
39+
if let Some((_, number, _)) = chain_store.block_number(&block_ptr_to.hash)? {
4040
if number != block_ptr_to.number {
4141
bail!(
4242
"the given hash is for block number {} but the command specified block number {}",

server/index-node/src/resolver.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,8 @@ impl<S: Store> IndexNodeResolver<S> {
230230
let chain_store = chain.chain_store();
231231
let call_cache = chain.call_cache();
232232

233-
let block_number = match chain_store.block_number(&block_hash) {
234-
Ok(Some((_, n))) => n,
233+
let (block_number, timestamp) = match chain_store.block_number(&block_hash) {
234+
Ok(Some((_, n, timestamp))) => (n, timestamp),
235235
Ok(None) => {
236236
error!(
237237
self.logger,
@@ -277,6 +277,7 @@ impl<S: Store> IndexNodeResolver<S> {
277277
block: object! {
278278
hash: cached_call.block_ptr.hash.hash_hex(),
279279
number: cached_call.block_ptr.number,
280+
timestamp: timestamp.clone(),
280281
},
281282
contractAddress: &cached_call.contract_address[..],
282283
returnValue: &cached_call.return_value[..],

store/postgres/src/chain_store.rs

+24-13
Original file line numberDiff line numberDiff line change
@@ -589,34 +589,42 @@ mod data {
589589
}
590590
}
591591

592+
/// timestamp's representation depends the blockchain::Block implementation, on
593+
/// ethereum this is a U256 but on different chains it will most likely be different.
592594
pub(super) fn block_number(
593595
&self,
594596
conn: &PgConnection,
595597
hash: &BlockHash,
596-
) -> Result<Option<BlockNumber>, StoreError> {
598+
) -> Result<Option<(BlockNumber, Option<String>)>, StoreError> {
599+
const TIMESTAMP_QUERY: &str =
600+
"coalesce(data->'block'->>'timestamp', data->>'timestamp')";
601+
597602
let number = match self {
598603
Storage::Shared => {
599604
use public::ethereum_blocks as b;
600605

601606
b::table
602-
.select(b::number)
607+
.select((b::number, sql(TIMESTAMP_QUERY)))
603608
.filter(b::hash.eq(format!("{:x}", hash)))
604-
.first::<i64>(conn)
609+
.first::<(i64, Option<String>)>(conn)
605610
.optional()?
606611
}
607612
Storage::Private(Schema { blocks, .. }) => blocks
608613
.table()
609-
.select(blocks.number())
614+
.select((blocks.number(), sql(TIMESTAMP_QUERY)))
610615
.filter(blocks.hash().eq(hash.as_slice()))
611-
.first::<i64>(conn)
616+
.first::<(i64, Option<String>)>(conn)
612617
.optional()?,
613618
};
614-
number
615-
.map(|number| {
616-
BlockNumber::try_from(number)
617-
.map_err(|e| StoreError::QueryExecutionError(e.to_string()))
618-
})
619-
.transpose()
619+
620+
match number {
621+
None => Ok(None),
622+
Some((number, ts)) => {
623+
let number = BlockNumber::try_from(number)
624+
.map_err(|e| StoreError::QueryExecutionError(e.to_string()))?;
625+
Ok(Some((number, ts)))
626+
}
627+
}
620628
}
621629

622630
/// Find the first block that is missing from the database needed to
@@ -1690,12 +1698,15 @@ impl ChainStoreTrait for ChainStore {
16901698
.confirm_block_hash(&conn, &self.chain, number, hash)
16911699
}
16921700

1693-
fn block_number(&self, hash: &BlockHash) -> Result<Option<(String, BlockNumber)>, StoreError> {
1701+
fn block_number(
1702+
&self,
1703+
hash: &BlockHash,
1704+
) -> Result<Option<(String, BlockNumber, Option<String>)>, StoreError> {
16941705
let conn = self.get_conn()?;
16951706
Ok(self
16961707
.storage
16971708
.block_number(&conn, hash)?
1698-
.map(|number| (self.chain.clone(), number)))
1709+
.map(|(number, timestamp)| (self.chain.clone(), number, timestamp)))
16991710
}
17001711

17011712
async fn transaction_receipts_in_block(

store/postgres/src/query_store.rs

+11-4
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,10 @@ impl QueryStoreTrait for QueryStore {
6060
async fn block_ptr(&self) -> Result<Option<BlockPtr>, StoreError> {
6161
self.store.block_ptr(self.site.cheap_clone()).await
6262
}
63-
64-
fn block_number(&self, block_hash: &BlockHash) -> Result<Option<BlockNumber>, StoreError> {
63+
fn block_number_with_timestamp(
64+
&self,
65+
block_hash: &BlockHash,
66+
) -> Result<Option<(BlockNumber, Option<String>)>, StoreError> {
6567
// We should also really check that the block with the given hash is
6668
// on the chain starting at the subgraph's current head. That check is
6769
// very expensive though with the data structures we have currently
@@ -71,9 +73,9 @@ impl QueryStoreTrait for QueryStore {
7173
let subgraph_network = self.network_name();
7274
self.chain_store
7375
.block_number(block_hash)?
74-
.map(|(network_name, number)| {
76+
.map(|(network_name, number, timestamp)| {
7577
if network_name == subgraph_network {
76-
Ok(number)
78+
Ok((number, timestamp))
7779
} else {
7880
Err(StoreError::QueryExecutionError(format!(
7981
"subgraph {} belongs to network {} but block {:x} belongs to network {}",
@@ -84,6 +86,11 @@ impl QueryStoreTrait for QueryStore {
8486
.transpose()
8587
}
8688

89+
fn block_number(&self, block_hash: &BlockHash) -> Result<Option<BlockNumber>, StoreError> {
90+
self.block_number_with_timestamp(block_hash)
91+
.map(|opt| opt.map(|(number, _)| number))
92+
}
93+
8794
fn wait_stats(&self) -> Result<PoolWaitStats, StoreError> {
8895
self.store.wait_stats(self.replica_id)
8996
}

0 commit comments

Comments
 (0)