Skip to content

Commit 65b7dde

Browse files
authored
fix(node): add index check to sliver PUT endpoint (#2966)
1 parent 38b4f9f commit 65b7dde

File tree

7 files changed

+108
-5
lines changed

7 files changed

+108
-5
lines changed

.github/workflows/code.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,6 @@ jobs:
137137
- name: Run all unit tests and doctests with `cargo test`
138138
run: cargo test
139139

140-
141140
simtests:
142141
name: Run all simtests
143142
needs: diff

.github/workflows/update-ws-install.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
SUI_KEYSTORE: "${{ secrets.SUI_KEYSTORE }}"
4141
WALRUS_CONFIG: "${{ vars.WALRUS_CONFIG }}"
4242
SUI_NETWORK: mainnet
43-
SITE_BUILDER_VERSION: v1 # keep uploading this as single blobs instead of quilts for now (SEW-509)
43+
SITE_BUILDER_VERSION: v1 # keep uploading this as single blobs instead of quilts for now (SEW-509)
4444

4545
- name: Create temporary directory
4646
run: "mkdir -p ${{ env.BUILD_DIR }}"

crates/walrus-core/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,11 @@ impl Sliver {
598598
by_axis::flat_map!(self.as_ref(), |x| x.verify(encoding_config, metadata))
599599
}
600600

601+
/// Returns the [`SliverIndex`] of this sliver (primary or secondary).
602+
pub fn sliver_index(&self) -> SliverIndex {
603+
by_axis::flat_map!(self.as_ref(), |x| x.index)
604+
}
605+
601606
/// Returns the [`Sliver<T>`][Sliver] contained within the enum.
602607
pub fn to_raw<T>(self) -> Result<encoding::SliverData<T>, WrongSliverVariantError>
603608
where

crates/walrus-service/src/node.rs

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4003,6 +4003,20 @@ impl ServiceState for StorageNodeInner {
40034003
intent: UploadIntent,
40044004
) -> Result<bool, StoreSliverError> {
40054005
self.check_index(sliver_pair_index)?;
4006+
4007+
let n_shards = self.n_shards();
4008+
let expected_pair_index = match sliver.r#type() {
4009+
SliverType::Primary => sliver.sliver_index().to_pair_index::<Primary>(n_shards),
4010+
SliverType::Secondary => sliver.sliver_index().to_pair_index::<Secondary>(n_shards),
4011+
};
4012+
ensure!(
4013+
sliver_pair_index == expected_pair_index,
4014+
StoreSliverError::SliverIndexMismatch {
4015+
sliver_pair_index,
4016+
sliver_index: sliver.sliver_index(),
4017+
}
4018+
);
4019+
40064020
let (metadata_persisted, persisted) = self
40074021
.resolve_metadata_for_sliver(&blob_id, intent.is_pending())
40084022
.await?;
@@ -4424,7 +4438,9 @@ fn map_sliver_error_to_metadata(error: StoreSliverError) -> StoreMetadataError {
44244438
StoreSliverError::UnsupportedEncodingType(kind) => {
44254439
StoreMetadataError::UnsupportedEncodingType(kind)
44264440
}
4427-
StoreSliverError::SliverOutOfRange(_) | StoreSliverError::InvalidSliver(_) => {
4441+
StoreSliverError::SliverOutOfRange(_)
4442+
| StoreSliverError::InvalidSliver(_)
4443+
| StoreSliverError::SliverIndexMismatch { .. } => {
44284444
StoreMetadataError::Internal(anyhow!("sliver cache flush failed: {error:?}"))
44294445
}
44304446
StoreSliverError::ShardNotAssigned(inner) => StoreMetadataError::Internal(inner.into()),
@@ -4937,6 +4953,77 @@ mod tests {
49374953
assert_eq!(stored_status, StoredOnNodeStatus::Stored);
49384954
Ok(())
49394955
}
4956+
4957+
#[tokio::test]
4958+
async fn store_sliver_rejects_inconsistent_sliver_pair_index() -> TestResult {
4959+
let (cluster, _) = cluster_at_epoch1_without_blobs(&[&[0, 1, 2, 3]], None).await?;
4960+
let storage_node = cluster.nodes[0].storage_node.clone();
4961+
let encoding_config = storage_node.as_ref().inner.encoding_config.as_ref().clone();
4962+
let encoded = EncodedBlob::new(BLOB, encoding_config);
4963+
let blob_id = *encoded.blob_id();
4964+
let pair_0 = encoded.assigned_sliver_pair(SHARD_INDEX);
4965+
let pair_1 = encoded.assigned_sliver_pair(OTHER_SHARD_INDEX);
4966+
4967+
assert!(
4968+
storage_node
4969+
.as_ref()
4970+
.store_metadata(
4971+
encoded.metadata.clone().into_unverified(),
4972+
UploadIntent::Pending
4973+
)
4974+
.await?
4975+
);
4976+
4977+
// Primary sliver from pair 0 has sliver index 0, so it must be stored with pair index 0.
4978+
// Passing pair_1.index() is inconsistent and must be rejected.
4979+
let err = storage_node
4980+
.as_ref()
4981+
.store_sliver(
4982+
blob_id,
4983+
pair_1.index(),
4984+
Sliver::Primary(pair_0.primary.clone()),
4985+
UploadIntent::Pending,
4986+
)
4987+
.await
4988+
.expect_err("store_sliver must reject inconsistent sliver pair index");
4989+
assert!(
4990+
matches!(err, StoreSliverError::SliverIndexMismatch { .. }),
4991+
"expected SliverIndexMismatch, got {err:?}"
4992+
);
4993+
4994+
// Secondary sliver from pair 0 has sliver index n_shards-1 (for pair index 0).
4995+
// Passing pair_1.index() is inconsistent and must be rejected.
4996+
let err = storage_node
4997+
.as_ref()
4998+
.store_sliver(
4999+
blob_id,
5000+
pair_1.index(),
5001+
Sliver::Secondary(pair_0.secondary.clone()),
5002+
UploadIntent::Pending,
5003+
)
5004+
.await
5005+
.expect_err("store_sliver must reject inconsistent sliver pair index for secondary");
5006+
assert!(
5007+
matches!(err, StoreSliverError::SliverIndexMismatch { .. }),
5008+
"expected SliverIndexMismatch, got {err:?}"
5009+
);
5010+
5011+
// Consistent indices must be accepted.
5012+
for pair in [pair_0, pair_1] {
5013+
storage_node
5014+
.as_ref()
5015+
.store_sliver(
5016+
blob_id,
5017+
pair.index(),
5018+
Sliver::Primary(pair.primary.clone()),
5019+
UploadIntent::Pending,
5020+
)
5021+
.await?;
5022+
}
5023+
5024+
Ok(())
5025+
}
5026+
49405027
async fn check_sliver_status<A: EncodingAxis>(
49415028
storage_node: &StorageNodeHandle,
49425029
pair_index: SliverPairIndex,

crates/walrus-service/src/node/errors.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ use walrus_core::{
1616
Epoch,
1717
SUPPORTED_ENCODING_TYPES,
1818
ShardIndex,
19+
SliverIndex,
20+
SliverPairIndex,
1921
encoding::SliverVerificationError,
2022
inconsistency::InconsistencyVerificationError,
2123
messages::MessageVerificationError,
@@ -310,6 +312,16 @@ pub enum StoreSliverError {
310312
#[rest_api_error(reason = "INVALID_SLIVER", status = ApiStatusCode::InvalidArgument)]
311313
InvalidSliver(#[from] SliverVerificationError),
312314

315+
/// The sliver's index is inconsistent with the requested sliver pair index.
316+
#[error(
317+
"sliver index {sliver_index} is inconsistent with sliver pair index {sliver_pair_index}"
318+
)]
319+
#[rest_api_error(reason = "SLIVER_INDEX_MISMATCH", status = ApiStatusCode::InvalidArgument)]
320+
SliverIndexMismatch {
321+
sliver_pair_index: SliverPairIndex,
322+
sliver_index: SliverIndex,
323+
},
324+
313325
#[error(transparent)]
314326
#[rest_api_error(delegate)]
315327
ShardNotAssigned(#[from] ShardNotAssigned),

crates/walrus-service/storage_openapi.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

crates/walrus-service/storage_openapi.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,7 @@ paths:
492492
schema:
493493
$ref: '#/components/schemas/ApiSuccess_String'
494494
'400':
495-
description: May be returned when (1) The blob has not been registered or has already expired. (2) The index identifying the resource is out-of-range for the system. (3) The metadata for the blob is required but missing. (4) The node refused the sliver because the pending upload cache is full. (5) The provided sliver failed verification against the previously uploaded metadata for that blob ID. (6) The shard associated with the operation is not assigned to this storage node. (7) The sliver was too large to fit in the pending cache for the node.
495+
description: May be returned when (1) The blob has not been registered or has already expired. (2) The index identifying the resource is out-of-range for the system. (3) The metadata for the blob is required but missing. (4) The node refused the sliver because the pending upload cache is full. (5) The provided sliver failed verification against the previously uploaded metadata for that blob ID. (6) The shard associated with the operation is not assigned to this storage node. (7) The sliver was too large to fit in the pending cache for the node. (8) The sliver's index is inconsistent with the requested sliver pair index.
496496
content:
497497
application/json:
498498
schema:

0 commit comments

Comments
 (0)