Skip to content

Commit f7c7190

Browse files
authored
Add support for minimum block constraint (#2868)
This PR adds support for a minimum block constraint on queries via the `number_gte` block argument. The minimum block constraint will be used by the gateway to reduce the likelihood that a client receives query results from blocks prior to those from past queries from the same client.
1 parent 774f055 commit f7c7190

File tree

5 files changed

+75
-27
lines changed

5 files changed

+75
-27
lines changed

graphql/src/query/ext.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ impl ValueExt for q::Value {
5858
pub enum BlockConstraint {
5959
Hash(H256),
6060
Number(BlockNumber),
61+
/// Execute the query on the latest block only if the the subgraph has progressed to or past the
62+
/// given block number.
63+
Min(BlockNumber),
6164
Latest,
6265
}
6366

@@ -82,6 +85,10 @@ impl TryFromValue for BlockConstraint {
8285
Ok(BlockConstraint::Number(BlockNumber::try_from_value(
8386
number_value,
8487
)?))
88+
} else if let Some(number_value) = map.get("number_gte") {
89+
Ok(BlockConstraint::Min(BlockNumber::try_from_value(
90+
number_value,
91+
)?))
8592
} else {
8693
Err(anyhow!("invalid `BlockConstraint`"))
8794
}

graphql/src/schema/api.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,14 @@ fn add_block_height_type(schema: &mut Document) {
227227
default_value: None,
228228
directives: vec![],
229229
},
230+
InputValue {
231+
position: Pos::default(),
232+
description: None,
233+
name: "number_gte".to_owned(),
234+
value_type: Type::NamedType("Int".to_owned()),
235+
default_value: None,
236+
directives: vec![],
237+
},
230238
],
231239
});
232240
let def = Definition::TypeDefinition(typedef);
@@ -641,9 +649,12 @@ fn block_argument() -> InputValue {
641649
position: Pos::default(),
642650
description: Some(
643651
"The block at which the query should be executed. \
644-
Can either be an `{ number: Int }` containing the block number \
645-
or a `{ hash: Bytes }` value containing a block hash. Defaults \
646-
to the latest block when omitted."
652+
Can either be a `{ hash: Bytes }` value containing a block hash, \
653+
a `{ number: Int }` containing the block number, \
654+
or a `{ number_gte: Int }` containing the minimum block number. \
655+
In the case of `number_gte`, the query will be executed on the latest block only if \
656+
the subgraph has progressed to or past the minimum block number. \
657+
Defaults to the latest block when omitted."
647658
.to_owned(),
648659
),
649660
name: "block".to_string(),

graphql/src/schema/meta.graphql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ type _Block_ {
2020
hash: Bytes
2121
"The block number"
2222
number: Int!
23+
"The minimum block number"
24+
number_gte: Int!
2325
}
2426

2527
enum _SubgraphErrorPolicy_ {

graphql/src/store/resolver.rs

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -109,31 +109,25 @@ impl StoreResolver {
109109
bc: BlockConstraint,
110110
subgraph: DeploymentHash,
111111
) -> Result<BlockPtr, QueryExecutionError> {
112+
fn check_ptr(
113+
subgraph: DeploymentHash,
114+
ptr: Option<BlockPtr>,
115+
min: BlockNumber,
116+
) -> Result<BlockPtr, QueryExecutionError> {
117+
let ptr = ptr.expect("we should have already checked that the subgraph exists");
118+
if ptr.number < min {
119+
return Err(QueryExecutionError::ValueParseError(
120+
"block.number".to_owned(),
121+
format!(
122+
"subgraph {} has only indexed up to block number {} \
123+
and data for block number {} is therefore not yet available",
124+
subgraph, ptr.number, min
125+
),
126+
));
127+
}
128+
Ok(ptr)
129+
}
112130
match bc {
113-
BlockConstraint::Number(number) => store
114-
.block_ptr()
115-
.map_err(|e| StoreError::from(e).into())
116-
.and_then(|ptr| {
117-
let ptr = ptr.expect("we should have already checked that the subgraph exists");
118-
if ptr.number < number {
119-
Err(QueryExecutionError::ValueParseError(
120-
"block.number".to_owned(),
121-
format!(
122-
"subgraph {} has only indexed up to block number {} \
123-
and data for block number {} is therefore not yet available",
124-
subgraph, ptr.number, number
125-
),
126-
))
127-
} else {
128-
// We don't have a way here to look the block hash up from
129-
// the database, and even if we did, there is no guarantee
130-
// that we have the block in our cache. We therefore
131-
// always return an all zeroes hash when users specify
132-
// a block number
133-
// See 7a7b9708-adb7-4fc2-acec-88680cb07ec1
134-
Ok(BlockPtr::from((web3::types::H256::zero(), number as u64)))
135-
}
136-
}),
137131
BlockConstraint::Hash(hash) => {
138132
store
139133
.block_number(hash)
@@ -149,6 +143,23 @@ impl StoreResolver {
149143
.map(|number| BlockPtr::from((hash, number as u64)))
150144
})
151145
}
146+
BlockConstraint::Number(number) => store
147+
.block_ptr()
148+
.map_err(|e| StoreError::from(e).into())
149+
.and_then(|ptr| {
150+
check_ptr(subgraph, ptr, number)?;
151+
// We don't have a way here to look the block hash up from
152+
// the database, and even if we did, there is no guarantee
153+
// that we have the block in our cache. We therefore
154+
// always return an all zeroes hash when users specify
155+
// a block number
156+
// See 7a7b9708-adb7-4fc2-acec-88680cb07ec1
157+
Ok(BlockPtr::from((web3::types::H256::zero(), number as u64)))
158+
}),
159+
BlockConstraint::Min(number) => store
160+
.block_ptr()
161+
.map_err(|e| StoreError::from(e).into())
162+
.and_then(|ptr| check_ptr(subgraph, ptr, number)),
152163
BlockConstraint::Latest => store
153164
.block_ptr()
154165
.map_err(|e| StoreError::from(e).into())

graphql/tests/query.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1548,6 +1548,19 @@ fn query_at_block_with_vars() {
15481548
check_musicians_at(&deployment.hash, query, var, expected, qid).await;
15491549
}
15501550

1551+
async fn musicians_at_nr_gte(
1552+
deployment: &DeploymentLocator,
1553+
block: i32,
1554+
expected: Result<Vec<&str>, &str>,
1555+
qid: &str,
1556+
) {
1557+
let query =
1558+
"query by_nr($block: Int!) { musicians(block: { number_gte: $block }) { id } }";
1559+
let var = Some(("block", r::Value::Int(block.into())));
1560+
1561+
check_musicians_at(&deployment.hash, query, var, expected, qid).await;
1562+
}
1563+
15511564
async fn musicians_at_hash(
15521565
deployment: &DeploymentLocator,
15531566
block: &FakeBlock,
@@ -1570,6 +1583,10 @@ fn query_at_block_with_vars() {
15701583
musicians_at_nr(&deployment, 0, Ok(vec!["m1", "m2"]), "n0").await;
15711584
musicians_at_nr(&deployment, 1, Ok(vec!["m1", "m2", "m3", "m4"]), "n1").await;
15721585

1586+
musicians_at_nr_gte(&deployment, 7000, Err(BLOCK_NOT_INDEXED), "ngte7000").await;
1587+
musicians_at_nr_gte(&deployment, 0, Ok(vec!["m1", "m2", "m3", "m4"]), "ngte0").await;
1588+
musicians_at_nr_gte(&deployment, 1, Ok(vec!["m1", "m2", "m3", "m4"]), "ngte1").await;
1589+
15731590
musicians_at_hash(&deployment, &GENESIS_BLOCK, Ok(vec!["m1", "m2"]), "h0").await;
15741591
musicians_at_hash(
15751592
&deployment,

0 commit comments

Comments
 (0)