Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 44 additions & 5 deletions contracts/bucket_object/sources/bucket_object.move
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ public fun has_current_version(self: &BucketObject): bool {
self.inner().has_current_version()
}

public fun is_deleted(self: &BucketObject): bool {
self.inner().has_current_version()
&& object_version::delete_marker(self.inner().current_version())
}

public fun current_version(self: &BucketObject): &ObjectVersion {
self.inner().current_version()
}
Expand Down Expand Up @@ -143,7 +148,11 @@ public fun put_object_if_absent_and_register(
object_etag: String,
ctx: &mut TxContext,
) {
assert!(!self.inner().has_current_version(), EObjectAlreadyExists);
assert!(
!self.inner().has_current_version()
|| object_version::delete_marker(self.inner().current_version()),
EObjectAlreadyExists,
);
register_and_stage_new_version(
self,
blob_bucket,
Expand Down Expand Up @@ -196,14 +205,44 @@ public fun update_object_if_match_and_register(
);
}

public fun delete_object(self: &mut BucketObject, object_etag: String) {
assert!(!self.inner().has_pending_version(), EPendingVersionAlreadyExists);
let bucket_object_id = object::id(self);
let next_generation = self.inner().generation() + 1;
stage_pending_version(
self,
object_version::new_delete_marker(
bucket_object_id,
next_generation,
object_etag,
),
);
self.inner_mut().promote_pending_version();
}

public fun delete_object_if_match(
self: &mut BucketObject,
expected_object_etag: String,
object_etag: String,
) {
assert!(self.inner().has_current_version(), ECurrentVersionMissing);
assert!(
object_version::object_etag(self.inner().current_version()) == expected_object_etag,
EObjectEtagMismatch,
);
delete_object(self, object_etag);
}

public fun finalize_pending_version_if_certified(self: &mut BucketObject, blob_bucket: &BlobBucket) {
assert!(object::id(blob_bucket) == self.inner().blob_bucket_id(), EBlobBucketMismatch);
assert!(self.inner().has_pending_version(), EPendingVersionMissing);
let pending_version = self.inner().pending_version();
assert!(
blob_bucket.is_blob_certified(object_version::blob_id(pending_version)),
EPendingVersionNotCertified,
);
if (!object_version::delete_marker(pending_version)) {
assert!(
blob_bucket.is_blob_certified(object_version::blob_id(pending_version)),
EPendingVersionNotCertified,
);
};
self.inner_mut().promote_pending_version();
}

Expand Down
33 changes: 27 additions & 6 deletions contracts/bucket_object/sources/object_version.move
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use std::string::String;
public struct ObjectVersion has copy, store, drop {
bucket_object_id: ID,
generation: u64,
blob_id: u256,
pooled_blob_object_id: ID,
blob_id: option::Option<u256>,
pooled_blob_object_id: option::Option<ID>,
size: u64,
content_etag: String,
object_etag: String,
Expand All @@ -29,15 +29,32 @@ public fun new(
ObjectVersion {
bucket_object_id,
generation,
blob_id,
pooled_blob_object_id,
blob_id: option::some(blob_id),
pooled_blob_object_id: option::some(pooled_blob_object_id),
size,
content_etag,
object_etag,
delete_marker,
}
}

public fun new_delete_marker(
bucket_object_id: ID,
generation: u64,
object_etag: String,
): ObjectVersion {
ObjectVersion {
bucket_object_id,
generation,
blob_id: option::none(),
pooled_blob_object_id: option::none(),
size: 0,
content_etag: b"".to_string(),
object_etag,
delete_marker: true,
}
}

#[test_only]
public fun new_for_testing(
bucket_object_id: ID,
Expand Down Expand Up @@ -69,12 +86,16 @@ public fun generation(self: &ObjectVersion): u64 {
self.generation
}

public fun has_blob(self: &ObjectVersion): bool {
self.blob_id.is_some()
}

public fun blob_id(self: &ObjectVersion): u256 {
self.blob_id
*self.blob_id.borrow()
}

public fun pooled_blob_object_id(self: &ObjectVersion): ID {
self.pooled_blob_object_id
*self.pooled_blob_object_id.borrow()
}

public fun size(self: &ObjectVersion): u64 {
Expand Down
170 changes: 170 additions & 0 deletions contracts/bucket_object/tests/bucket_object_tests.move
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,141 @@ fun update_object_if_match_and_register_stages_next_generation() {
destroy_bucket_object_fixture(bucket_object, blob_bucket, blob_bucket_cap, pool_payment, system);
}

#[test]
fun delete_object_promotes_delete_marker() {
let sk = test_utils::bls_sk_for_testing();
let ctx = &mut tx_context::dummy();
let mut system = system::new_for_testing(ctx);
let encoded_size = encoding::encoded_blob_length(SIZE, RS2, system.n_shards());
let mut pool_payment = test_utils::mint_frost(N_COINS, ctx);
let (mut blob_bucket, blob_bucket_cap) = blob_bucket::new_for_testing(
&mut system,
encoded_size,
3,
&mut pool_payment,
ctx,
);
let mut bucket_object = bucket_object::new_for_testing(
object::id(&blob_bucket),
b"index.html".to_string(),
ctx,
);
register_and_finalize_initial_object_version(
&mut bucket_object,
&mut blob_bucket,
&blob_bucket_cap,
&mut system,
&sk,
ctx,
);

bucket_object::delete_object(&mut bucket_object, b"object-etag-delete".to_string());

assert_eq!(bucket_object::generation(&bucket_object), 2);
assert!(bucket_object::has_current_version(&bucket_object));
assert!(!bucket_object::has_pending_version(&bucket_object));
assert!(bucket_object::is_deleted(&bucket_object));
assert!(object_version::delete_marker(bucket_object::current_version(&bucket_object)));
assert_eq!(
object_version::object_etag(bucket_object::current_version(&bucket_object)),
b"object-etag-delete".to_string(),
);

destroy_bucket_object_fixture(bucket_object, blob_bucket, blob_bucket_cap, pool_payment, system);
}

#[test, expected_failure(abort_code = bucket_object::EObjectEtagMismatch)]
fun delete_object_if_match_requires_matching_etag() {
let sk = test_utils::bls_sk_for_testing();
let ctx = &mut tx_context::dummy();
let mut system = system::new_for_testing(ctx);
let encoded_size = encoding::encoded_blob_length(SIZE, RS2, system.n_shards());
let mut pool_payment = test_utils::mint_frost(N_COINS, ctx);
let (mut blob_bucket, blob_bucket_cap) = blob_bucket::new_for_testing(
&mut system,
encoded_size,
3,
&mut pool_payment,
ctx,
);
let mut bucket_object = bucket_object::new_for_testing(
object::id(&blob_bucket),
b"index.html".to_string(),
ctx,
);
register_and_finalize_initial_object_version(
&mut bucket_object,
&mut blob_bucket,
&blob_bucket_cap,
&mut system,
&sk,
ctx,
);

bucket_object::delete_object_if_match(
&mut bucket_object,
b"wrong-etag".to_string(),
b"object-etag-delete".to_string(),
);

abort
}

#[test]
fun put_object_if_absent_allows_current_delete_marker() {
let sk = test_utils::bls_sk_for_testing();
let ctx = &mut tx_context::dummy();
let mut system = system::new_for_testing(ctx);
let encoded_size = encoding::encoded_blob_length(SIZE, RS2, system.n_shards());
let mut pool_payment = test_utils::mint_frost(N_COINS, ctx);
let (mut blob_bucket, blob_bucket_cap) = blob_bucket::new_for_testing(
&mut system,
encoded_size * 2,
3,
&mut pool_payment,
ctx,
);
let mut bucket_object = bucket_object::new_for_testing(
object::id(&blob_bucket),
b"index.html".to_string(),
ctx,
);
register_and_finalize_initial_object_version(
&mut bucket_object,
&mut blob_bucket,
&blob_bucket_cap,
&mut system,
&sk,
ctx,
);
bucket_object::delete_object(&mut bucket_object, b"object-etag-delete".to_string());

let mut write_payment = test_utils::mint_frost(WRITE_PAYMENT, ctx);
bucket_object::put_object_if_absent_and_register(
&mut bucket_object,
&mut blob_bucket,
&blob_bucket_cap,
&mut system,
NEXT_ROOT_HASH,
SIZE,
RS2,
true,
&mut write_payment,
b"content-etag-v2".to_string(),
b"object-etag-v2".to_string(),
ctx,
);

let next_blob_id = blob::derive_blob_id(NEXT_ROOT_HASH, RS2, SIZE);
assert!(bucket_object::is_deleted(&bucket_object));
assert!(bucket_object::has_pending_version(&bucket_object));
assert_eq!(object_version::generation(bucket_object::pending_version(&bucket_object)), 3);
assert_eq!(object_version::blob_id(bucket_object::pending_version(&bucket_object)), next_blob_id);

write_payment.burn_for_testing();
destroy_bucket_object_fixture(bucket_object, blob_bucket, blob_bucket_cap, pool_payment, system);
}

#[test, expected_failure(abort_code = bucket_object::EObjectEtagMismatch)]
fun update_object_if_match_requires_matching_etag() {
let sk = test_utils::bls_sk_for_testing();
Expand Down Expand Up @@ -568,6 +703,41 @@ fun finalize_pending_version_promotes_after_certification() {
destroy_bucket_object_fixture(bucket_object, blob_bucket, blob_bucket_cap, pool_payment, system);
}

#[test]
fun finalize_pending_delete_marker_promotes_without_certification() {
let ctx = &mut tx_context::dummy();
let mut system = system::new_for_testing(ctx);
let encoded_size = encoding::encoded_blob_length(SIZE, RS2, system.n_shards());
let mut pool_payment = test_utils::mint_frost(N_COINS, ctx);
let (blob_bucket, blob_bucket_cap) = blob_bucket::new_for_testing(
&mut system,
encoded_size,
3,
&mut pool_payment,
ctx,
);
let mut bucket_object = bucket_object::new_for_testing(
object::id(&blob_bucket),
b"index.html".to_string(),
ctx,
);
let delete_marker = object_version::new_delete_marker(
object::id(&bucket_object),
1,
b"object-etag-delete".to_string(),
);

bucket_object::stage_pending_version_for_testing(&mut bucket_object, delete_marker);
bucket_object::finalize_pending_version_if_certified_for_testing(&mut bucket_object, &blob_bucket);

assert_eq!(bucket_object::generation(&bucket_object), 1);
assert!(bucket_object::has_current_version(&bucket_object));
assert!(bucket_object::is_deleted(&bucket_object));
assert!(object_version::delete_marker(bucket_object::current_version(&bucket_object)));

destroy_bucket_object_fixture(bucket_object, blob_bucket, blob_bucket_cap, pool_payment, system);
}

fun register_blob_in_bucket(
system: &mut system::System,
blob_bucket: &mut blob_bucket::BlobBucket,
Expand Down
Loading