Skip to content

Commit e3d2d38

Browse files
Return HTTP 404 for pruned blob requests (#6331)
* Return HTTP 404 for pruned blob requests
1 parent ae83901 commit e3d2d38

File tree

2 files changed

+137
-3
lines changed

2 files changed

+137
-3
lines changed

beacon_node/http_api/src/block_id.rs

+20-3
Original file line numberDiff line numberDiff line change
@@ -274,10 +274,27 @@ impl BlockId {
274274
warp_utils::reject::custom_not_found(format!("beacon block with root {}", root))
275275
})?;
276276

277+
// Error if the block is pre-Deneb and lacks blobs.
278+
let blob_kzg_commitments = block.message().body().blob_kzg_commitments().map_err(|_| {
279+
warp_utils::reject::custom_bad_request(
280+
"block is pre-Deneb and has no blobs".to_string(),
281+
)
282+
})?;
283+
277284
// Return the `BlobSidecarList` identified by `self`.
278-
let blob_sidecar_list = chain
279-
.get_blobs(&root)
280-
.map_err(warp_utils::reject::beacon_chain_error)?;
285+
let blob_sidecar_list = if !blob_kzg_commitments.is_empty() {
286+
chain
287+
.store
288+
.get_blobs(&root)
289+
.map_err(|e| warp_utils::reject::beacon_chain_error(e.into()))?
290+
.ok_or_else(|| {
291+
warp_utils::reject::custom_not_found(format!(
292+
"no blobs stored for block {root}"
293+
))
294+
})?
295+
} else {
296+
BlobSidecarList::default()
297+
};
281298

282299
let blob_sidecar_list_filtered = match indices.indices {
283300
Some(vec) => {

beacon_node/http_api/tests/tests.rs

+117
Original file line numberDiff line numberDiff line change
@@ -1667,6 +1667,93 @@ impl ApiTester {
16671667
self
16681668
}
16691669

1670+
/// Test fetching of blob sidecars that are not available in the database due to pruning.
1671+
///
1672+
/// If `zero_blobs` is false, test a block with >0 blobs, which should be unavailable.
1673+
/// If `zero_blobs` is true, then test a block with 0 blobs, which should still be available.
1674+
pub async fn test_get_blob_sidecars_pruned(self, zero_blobs: bool) -> Self {
1675+
// Prune all blobs prior to the database's split epoch.
1676+
let store = &self.chain.store;
1677+
let split_epoch = store.get_split_slot().epoch(E::slots_per_epoch());
1678+
let force_prune = true;
1679+
self.chain
1680+
.store
1681+
.try_prune_blobs(force_prune, split_epoch)
1682+
.unwrap();
1683+
1684+
let oldest_blob_slot = store.get_blob_info().oldest_blob_slot.unwrap();
1685+
1686+
assert_ne!(
1687+
oldest_blob_slot, 0,
1688+
"blob pruning should have pruned some blobs"
1689+
);
1690+
1691+
// Find a block with either 0 blobs or 1+ depending on the value of `zero_blobs`.
1692+
let mut test_slot = None;
1693+
for slot in 0..oldest_blob_slot.as_u64() {
1694+
let block_id = BlockId(CoreBlockId::Slot(Slot::new(slot)));
1695+
let (block, _, _) = block_id.blinded_block(&self.chain).unwrap();
1696+
let num_blobs = block.num_expected_blobs();
1697+
1698+
if (zero_blobs && num_blobs == 0) || (!zero_blobs && num_blobs > 0) {
1699+
test_slot = Some(Slot::new(slot));
1700+
break;
1701+
}
1702+
}
1703+
let test_slot = test_slot.expect(&format!(
1704+
"should be able to find a block matching zero_blobs={zero_blobs}"
1705+
));
1706+
1707+
match self
1708+
.client
1709+
.get_blobs::<E>(CoreBlockId::Slot(test_slot), None)
1710+
.await
1711+
{
1712+
Ok(result) => {
1713+
if zero_blobs {
1714+
assert_eq!(
1715+
&result.unwrap().data[..],
1716+
&[],
1717+
"empty blobs are always available"
1718+
);
1719+
} else {
1720+
assert_eq!(result, None, "blobs should have been pruned");
1721+
}
1722+
}
1723+
Err(e) => panic!("failed with non-404 status: {e:?}"),
1724+
}
1725+
1726+
self
1727+
}
1728+
1729+
pub async fn test_get_blob_sidecars_pre_deneb(self) -> Self {
1730+
let oldest_blob_slot = self.chain.store.get_blob_info().oldest_blob_slot.unwrap();
1731+
assert_ne!(
1732+
oldest_blob_slot, 0,
1733+
"oldest_blob_slot should be non-zero and post-Deneb"
1734+
);
1735+
let test_slot = oldest_blob_slot - 1;
1736+
assert!(
1737+
!self
1738+
.chain
1739+
.spec
1740+
.fork_name_at_slot::<E>(test_slot)
1741+
.deneb_enabled(),
1742+
"Deneb should not be enabled at {test_slot}"
1743+
);
1744+
1745+
match self
1746+
.client
1747+
.get_blobs::<E>(CoreBlockId::Slot(test_slot), None)
1748+
.await
1749+
{
1750+
Ok(result) => panic!("queries for pre-Deneb slots should fail. got: {result:?}"),
1751+
Err(e) => assert_eq!(e.status().unwrap(), 400),
1752+
}
1753+
1754+
self
1755+
}
1756+
16701757
pub async fn test_beacon_blocks_attestations(self) -> Self {
16711758
for block_id in self.interesting_block_ids() {
16721759
let result = self
@@ -6846,6 +6933,36 @@ async fn get_blob_sidecars() {
68466933
.await;
68476934
}
68486935

6936+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6937+
async fn get_blob_sidecars_pruned() {
6938+
let mut config = ApiTesterConfig::default();
6939+
config.spec.altair_fork_epoch = Some(Epoch::new(0));
6940+
config.spec.bellatrix_fork_epoch = Some(Epoch::new(0));
6941+
config.spec.capella_fork_epoch = Some(Epoch::new(0));
6942+
config.spec.deneb_fork_epoch = Some(Epoch::new(0));
6943+
6944+
ApiTester::new_from_config(config)
6945+
.await
6946+
.test_get_blob_sidecars_pruned(false)
6947+
.await
6948+
.test_get_blob_sidecars_pruned(true)
6949+
.await;
6950+
}
6951+
6952+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
6953+
async fn get_blob_sidecars_pre_deneb() {
6954+
let mut config = ApiTesterConfig::default();
6955+
config.spec.altair_fork_epoch = Some(Epoch::new(0));
6956+
config.spec.bellatrix_fork_epoch = Some(Epoch::new(0));
6957+
config.spec.capella_fork_epoch = Some(Epoch::new(0));
6958+
config.spec.deneb_fork_epoch = Some(Epoch::new(1));
6959+
6960+
ApiTester::new_from_config(config)
6961+
.await
6962+
.test_get_blob_sidecars_pre_deneb()
6963+
.await;
6964+
}
6965+
68496966
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
68506967
async fn post_validator_liveness_epoch() {
68516968
ApiTester::new()

0 commit comments

Comments
 (0)