Skip to content

Commit e3c50b0

Browse files
committed
expose timestamp on meta block
1 parent 649c61a commit e3c50b0

File tree

8 files changed

+145
-24
lines changed

8 files changed

+145
-24
lines changed

graph/src/components/store/traits.rs

+8
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,14 @@ pub trait QueryStore: Send + Sync {
409409

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

412+
/// Returns the blocknumber as well as the timestamp. Timestamp depends on the chain block type
413+
/// and can have multiple formats, it can also not be prevent. For now this is only available
414+
/// for EVM chains both firehose and rpc.
415+
fn block_number_with_timestamp(
416+
&self,
417+
block_hash: &BlockHash,
418+
) -> Result<Option<(BlockNumber, Option<String>)>, StoreError>;
419+
412420
fn wait_stats(&self) -> Result<PoolWaitStats, StoreError>;
413421

414422
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
@@ -159,7 +159,7 @@ where
159159
let query_res = execute_query(
160160
query.clone(),
161161
Some(selection_set),
162-
resolver.block_ptr.clone(),
162+
resolver.block_ptr.as_ref().map(Into::into).clone(),
163163
QueryExecutionOptions {
164164
resolver,
165165
deadline: ENV_VARS.graphql.query_timeout.map(|t| Instant::now() + t),

graphql/src/store/resolver.rs

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

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

3758
impl StoreResolver {
@@ -79,7 +100,7 @@ impl StoreResolver {
79100
let block_ptr = Self::locate_block(store_clone.as_ref(), bc, state).await?;
80101

81102
let has_non_fatal_errors = store
82-
.has_deterministic_errors(block_ptr.block_number())
103+
.has_deterministic_errors(block_ptr.ptr.block_number())
83104
.await?;
84105

85106
let resolver = StoreResolver {
@@ -98,15 +119,16 @@ impl StoreResolver {
98119
pub fn block_number(&self) -> BlockNumber {
99120
self.block_ptr
100121
.as_ref()
101-
.map(|ptr| ptr.number as BlockNumber)
122+
.map(|ptr| ptr.ptr.number as BlockNumber)
102123
.unwrap_or(BLOCK_NUMBER_MAX)
103124
}
104125

126+
/// locate_block returns the block pointer and it's timestamp when available.
105127
async fn locate_block(
106128
store: &dyn QueryStore,
107129
bc: BlockConstraint,
108130
state: &DeploymentState,
109-
) -> Result<BlockPtr, QueryExecutionError> {
131+
) -> Result<BlockPtrTs, QueryExecutionError> {
110132
fn block_queryable(
111133
state: &DeploymentState,
112134
block: BlockNumber,
@@ -116,23 +138,39 @@ impl StoreResolver {
116138
.map_err(|msg| QueryExecutionError::ValueParseError("block.number".to_owned(), msg))
117139
}
118140

141+
fn get_block_ts(
142+
store: &dyn QueryStore,
143+
ptr: &BlockPtr,
144+
) -> Result<Option<String>, QueryExecutionError> {
145+
match store
146+
.block_number_with_timestamp(&ptr.hash)
147+
.map_err(Into::<QueryExecutionError>::into)?
148+
{
149+
Some((_, Some(ts))) => Ok(Some(ts)),
150+
_ => Ok(None),
151+
}
152+
}
153+
119154
match bc {
120155
BlockConstraint::Hash(hash) => {
121156
let ptr = store
122-
.block_number(&hash)
157+
.block_number_with_timestamp(&hash)
123158
.map_err(Into::into)
124-
.and_then(|number| {
125-
number
159+
.and_then(|result| {
160+
result
126161
.ok_or_else(|| {
127162
QueryExecutionError::ValueParseError(
128163
"block.hash".to_owned(),
129164
"no block with that hash found".to_owned(),
130165
)
131166
})
132-
.map(|number| BlockPtr::new(hash, number))
167+
.map(|(number, ts)| BlockPtrTs {
168+
ptr: BlockPtr::new(hash, number),
169+
timestamp: ts,
170+
})
133171
})?;
134172

135-
block_queryable(state, ptr.number)?;
173+
block_queryable(state, ptr.ptr.number)?;
136174
Ok(ptr)
137175
}
138176
BlockConstraint::Number(number) => {
@@ -143,7 +181,7 @@ impl StoreResolver {
143181
// always return an all zeroes hash when users specify
144182
// a block number
145183
// See 7a7b9708-adb7-4fc2-acec-88680cb07ec1
146-
Ok(BlockPtr::from((web3::types::H256::zero(), number as u64)))
184+
Ok(BlockPtr::from((web3::types::H256::zero(), number as u64)).into())
147185
}
148186
BlockConstraint::Min(min) => {
149187
let ptr = state.latest_block.cheap_clone();
@@ -157,9 +195,18 @@ impl StoreResolver {
157195
),
158196
));
159197
}
160-
Ok(ptr)
198+
let timestamp = get_block_ts(store, &state.latest_block)?;
199+
200+
Ok(BlockPtrTs { ptr, timestamp })
201+
}
202+
BlockConstraint::Latest => {
203+
let timestamp = get_block_ts(store, &state.latest_block)?;
204+
205+
Ok(BlockPtrTs {
206+
ptr: state.latest_block.cheap_clone(),
207+
timestamp,
208+
})
161209
}
162-
BlockConstraint::Latest => Ok(state.latest_block.cheap_clone()),
163210
}
164211
}
165212

@@ -180,7 +227,7 @@ impl StoreResolver {
180227
// locate_block indicates that we do not have a block hash
181228
// by setting the hash to `zero`
182229
// See 7a7b9708-adb7-4fc2-acec-88680cb07ec1
183-
let hash_h256 = ptr.hash_as_h256();
230+
let hash_h256 = ptr.ptr.hash_as_h256();
184231
if hash_h256 == web3::types::H256::zero() {
185232
None
186233
} else {
@@ -191,12 +238,21 @@ impl StoreResolver {
191238
let number = self
192239
.block_ptr
193240
.as_ref()
194-
.map(|ptr| r::Value::Int((ptr.number as i32).into()))
241+
.map(|ptr| r::Value::Int((ptr.ptr.number as i32).into()))
195242
.unwrap_or(r::Value::Null);
243+
244+
let timestamp = self.block_ptr.as_ref().map(|ptr| {
245+
ptr.timestamp
246+
.clone()
247+
.map(|ts| r::Value::String(ts))
248+
.unwrap_or(r::Value::Null)
249+
});
250+
196251
let mut map = BTreeMap::new();
197252
let block = object! {
198253
hash: hash,
199254
number: number,
255+
timestamp: timestamp,
200256
__typename: BLOCK_FIELD_TYPE
201257
};
202258
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 {

store/postgres/src/chain_store.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -606,14 +606,14 @@ mod data {
606606
b::table
607607
.select((b::number, sql(TIMESTAMP_QUERY)))
608608
.filter(b::hash.eq(format!("{:x}", hash)))
609-
.first::<(i64, String)>(conn)
609+
.first::<(i64, Option<String>)>(conn)
610610
.optional()?
611611
}
612612
Storage::Private(Schema { blocks, .. }) => blocks
613613
.table()
614614
.select((blocks.number(), sql(TIMESTAMP_QUERY)))
615615
.filter(blocks.hash().eq(hash.as_slice()))
616-
.first::<(i64, String)>(conn)
616+
.first::<(i64, Option<String>)>(conn)
617617
.optional()?,
618618
};
619619

@@ -622,7 +622,7 @@ mod data {
622622
Some((number, ts)) => {
623623
let number = BlockNumber::try_from(number)
624624
.map_err(|e| StoreError::QueryExecutionError(e.to_string()))?;
625-
Ok(Some((number, Some(ts))))
625+
Ok(Some((number, ts)))
626626
}
627627
}
628628
}

store/postgres/src/query_store.rs

+11-4
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,10 @@ impl QueryStoreTrait for QueryStore {
5757
async fn block_ptr(&self) -> Result<Option<BlockPtr>, StoreError> {
5858
self.store.block_ptr(self.site.cheap_clone()).await
5959
}
60-
61-
fn block_number(&self, block_hash: &BlockHash) -> Result<Option<BlockNumber>, StoreError> {
60+
fn block_number_with_timestamp(
61+
&self,
62+
block_hash: &BlockHash,
63+
) -> Result<Option<(BlockNumber, Option<String>)>, StoreError> {
6264
// We should also really check that the block with the given hash is
6365
// on the chain starting at the subgraph's current head. That check is
6466
// very expensive though with the data structures we have currently
@@ -68,9 +70,9 @@ impl QueryStoreTrait for QueryStore {
6870
let subgraph_network = self.network_name();
6971
self.chain_store
7072
.block_number(block_hash)?
71-
.map(|(network_name, number, _timestamp)| {
73+
.map(|(network_name, number, timestamp)| {
7274
if network_name == subgraph_network {
73-
Ok(number)
75+
Ok((number, timestamp))
7476
} else {
7577
Err(StoreError::QueryExecutionError(format!(
7678
"subgraph {} belongs to network {} but block {:x} belongs to network {}",
@@ -81,6 +83,11 @@ impl QueryStoreTrait for QueryStore {
8183
.transpose()
8284
}
8385

86+
fn block_number(&self, block_hash: &BlockHash) -> Result<Option<BlockNumber>, StoreError> {
87+
self.block_number_with_timestamp(block_hash)
88+
.map(|opt| opt.map(|(number, _)| number))
89+
}
90+
8491
fn wait_stats(&self) -> Result<PoolWaitStats, StoreError> {
8592
self.store.wait_stats(self.replica_id)
8693
}

store/postgres/tests/store.rs

+31
Original file line numberDiff line numberDiff line change
@@ -2027,6 +2027,37 @@ fn parse_timestamp() {
20272027
})
20282028
}
20292029

2030+
#[test]
2031+
/// checks if retrieving the timestamp from the data blob works.
2032+
/// on ethereum, the block has timestamp as U256 so it will always have a value
2033+
fn parse_null_timestamp() {
2034+
run_test(|store, _, _| async move {
2035+
use block_store::*;
2036+
// The test subgraph is at block 2. Since we don't ever delete
2037+
// the genesis block, the only block eligible for cleanup is BLOCK_ONE
2038+
// and the first retained block is block 2.
2039+
block_store::set_chain(
2040+
vec![
2041+
&*GENESIS_BLOCK,
2042+
&*BLOCK_ONE,
2043+
&*BLOCK_TWO,
2044+
&*BLOCK_THREE_NO_TIMESTAMP,
2045+
],
2046+
NETWORK_NAME,
2047+
);
2048+
let chain_store = store
2049+
.block_store()
2050+
.chain_store(NETWORK_NAME)
2051+
.expect("fake chain store");
2052+
2053+
let (_network, number, timestamp) = chain_store
2054+
.block_number(&BLOCK_THREE_NO_TIMESTAMP.block_hash())
2055+
.expect("block_number to return correct number and timestamp")
2056+
.unwrap();
2057+
assert_eq!(number, 3);
2058+
assert_eq!(true, timestamp.is_none());
2059+
})
2060+
}
20302061
#[test]
20312062
fn reorg_tracking() {
20322063
async fn update_john(

store/test-store/src/block_store.rs

+20-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ lazy_static! {
3333
pub static ref BLOCK_THREE: FakeBlock = BLOCK_TWO.make_child("7347afe69254df06729e123610b00b8b11f15cfae3241f9366fb113aec07489c", None);
3434
pub static ref BLOCK_THREE_NO_PARENT: FakeBlock = FakeBlock::make_no_parent(3, "fa9ebe3f74de4c56908b49f5c4044e85825f7350f3fa08a19151de82a82a7313");
3535
pub static ref BLOCK_THREE_TIMESTAMP: FakeBlock = BLOCK_TWO.make_child("6b834521bb753c132fdcf0e1034803ed9068e324112f8750ba93580b393a986b", Some(U256::from(1657712166)));
36+
// 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
37+
// what you are doing, don't use this block for other tests.
38+
pub static ref BLOCK_THREE_NO_TIMESTAMP: FakeBlock = BLOCK_TWO.make_child("6b834521bb753c132fdcf0e1034803ed9068e324112f8750ba93580b393a986b", None);
3639
pub static ref BLOCK_FOUR: FakeBlock = BLOCK_THREE.make_child("7cce080f5a49c2997a6cc65fc1cee9910fd8fc3721b7010c0b5d0873e2ac785e", None);
3740
pub static ref BLOCK_FIVE: FakeBlock = BLOCK_FOUR.make_child("7b0ea919e258eb2b119eb32de56b85d12d50ac6a9f7c5909f843d6172c8ba196", None);
3841
pub static ref BLOCK_SIX_NO_PARENT: FakeBlock = FakeBlock::make_no_parent(6, "6b834521bb753c132fdcf0e1034803ed9068e324112f8750ba93580b393a986b");
@@ -112,7 +115,23 @@ impl Block for FakeBlock {
112115
}
113116

114117
fn data(&self) -> Result<serde_json::Value, serde_json::Error> {
115-
serde_json::to_value(self.as_ethereum_block())
118+
let mut value: serde_json::Value = serde_json::to_value(self.as_ethereum_block())?;
119+
if !self.eq(&BLOCK_THREE_NO_TIMESTAMP) {
120+
return Ok(value);
121+
};
122+
123+
// Remove the timestamp for block BLOCK_THREE_NO_TIMESTAMP in order to simulate the non EVM behaviour
124+
// In these cases timestamp is not there at all but LightEthereumBlock uses U256 as timestamp so it
125+
// can never be null and therefore impossible to test without manipulating the JSON blob directly.
126+
if let serde_json::Value::Object(ref mut map) = value {
127+
map.entry("block").and_modify(|ref mut block| {
128+
if let serde_json::Value::Object(ref mut block) = block {
129+
block.remove_entry("timestamp");
130+
}
131+
});
132+
};
133+
134+
Ok(value)
116135
}
117136
}
118137

0 commit comments

Comments
 (0)