diff --git a/store/postgres/src/deployment_store.rs b/store/postgres/src/deployment_store.rs index d764d451ad6..9162f5ffc9e 100644 --- a/store/postgres/src/deployment_store.rs +++ b/store/postgres/src/deployment_store.rs @@ -993,6 +993,20 @@ impl DeploymentStore { // Unfortunately the machinery needed to do this is not yet in place. // The best we can do right now is just to make sure that the block number // is high enough. + let state = deployment::state(&mut conn, site.as_ref()).await?; + if block.number < state.earliest_block_number { + // Pruning removed the causality-region digests this block's POI is + // computed from. Querying anyway returns a well-formed but wrong POI + // that nobody can tell apart from a correct one, which is worse than + // failing: an allocation closing against a pruned epoch block would + // submit it and could be disputed. + return Err(StoreError::Unknown(anyhow!( + "block {} has been pruned for deployment {}; the earliest available block is {}", + block.number, + site.deployment, + state.earliest_block_number + ))); + } if latest_block_ptr.number < block.number { // If a subgraph has failed deterministically then any blocks past head // should return the same POI diff --git a/store/test-store/tests/postgres.rs b/store/test-store/tests/postgres.rs index 1d5a16ba851..d1be7b8fa53 100644 --- a/store/test-store/tests/postgres.rs +++ b/store/test-store/tests/postgres.rs @@ -2,6 +2,7 @@ pub mod postgres { pub mod aggregation; pub mod chain_head; pub mod graft; + pub mod pruned_poi; pub mod relational; pub mod relational_bytes; pub mod store; diff --git a/store/test-store/tests/postgres/pruned_poi.rs b/store/test-store/tests/postgres/pruned_poi.rs new file mode 100644 index 00000000000..aebaa5f92e5 --- /dev/null +++ b/store/test-store/tests/postgres/pruned_poi.rs @@ -0,0 +1,75 @@ +// A proof of indexing for a block that pruning has already removed cannot be computed: +// the causality-region digests it is hashed from are gone. Returning a well-formed but +// wrong POI is worse than failing, because nothing downstream can tell the difference. + +use graph::prelude::{DeploymentHash, SubgraphStore as _}; +use test_store::*; + +const SUBGRAPH_GQL: &str = " + type User @entity { + id: ID!, + name: String + } +"; + +/// The block pointer the POS tests use: a deterministic hash per number. +fn ptr(n: i32) -> graph::prelude::BlockPtr { + let hash = graph::prelude::alloy::primitives::B256::from( + graph::prelude::alloy::primitives::U256::from(n as u64), + ); + graph::prelude::BlockPtr::new(hash.into(), n.into()) +} + +/// Put the subgraph at `head` and move its history window forward to `earliest`, +/// which is what the pruner leaves behind once it has passed the blocks below it. +async fn set_head_and_prune(deployment: &DeploymentHash, head: i32, earliest: i32) { + let mut conn = PRIMARY_POOL.get().await.unwrap(); + let hash = deployment.as_str().to_string(); + let hash_bytes = format!("\\x{}", "11".repeat(32)); + let stmt = format!( + "UPDATE subgraphs.deployment SET earliest_block_number = {earliest} WHERE subgraph = '{hash}';\ + UPDATE subgraphs.head SET block_number = {head}, block_hash = '{hash_bytes}' WHERE id = \ + (SELECT id FROM subgraphs.deployment WHERE subgraph = '{hash}');" + ); + diesel_async::SimpleAsyncConnection::batch_execute(&mut conn, &stmt) + .await + .unwrap(); +} + +#[test] +fn proof_of_indexing_refuses_a_pruned_block() { + run_test_sequentially(|store| async move { + let id = DeploymentHash::new("prunedPoi").unwrap(); + create_test_subgraph(&id, SUBGRAPH_GQL).await; + + // The subgraph is at block 200; everything below 100 has been pruned away. + set_head_and_prune(&id, 200, 100).await; + + let sg_store = store.subgraph_store(); + let indexer = None; + + // A retained block is unaffected. + let retained = sg_store + .get_proof_of_indexing(&id, &indexer, ptr(120)) + .await; + assert!( + retained.is_ok(), + "a retained block must still produce a POI: {retained:?}" + ); + + // A pruned block must be an error, not a silently wrong hash. + let pruned = sg_store + .get_proof_of_indexing(&id, &indexer, ptr(50)) + .await; + match pruned { + Err(e) => { + let msg = format!("{e}"); + assert!( + msg.contains("pruned") || msg.contains("earliest"), + "the error should say the block was pruned, got: {msg}" + ); + } + Ok(v) => panic!("a pruned block returned {v:?} instead of an error"), + } + }) +}