Skip to content

Commit

Permalink
Check if blob exists before minting NFT (#2356)
Browse files Browse the repository at this point in the history
## Motivation

This is a follow up suggested on #2354

## Proposal

Check if the `Blob` exists before actually minting the NFT

## Test Plan

CI
  • Loading branch information
andresilva91 authored Aug 9, 2024
1 parent 4189b55 commit 34c8a1f
Show file tree
Hide file tree
Showing 13 changed files with 128 additions and 5 deletions.
1 change: 1 addition & 0 deletions examples/non-fungible/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ impl NonFungibleTokenContract {
}

async fn mint(&mut self, owner: AccountOwner, name: String, blob_id: BlobId) {
self.runtime.assert_blob_exists(blob_id);
let token_id = Nft::create_token_id(
&self.runtime.chain_id(),
&self.runtime.application_id().forget_abi(),
Expand Down
15 changes: 15 additions & 0 deletions linera-execution/src/execution_state_actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,11 @@ where
let blob = self.system.read_blob_content(blob_id).await?;
callback.respond(blob);
}

AssertBlobExists { blob_id, callback } => {
self.system.assert_blob_exists(blob_id).await?;
callback.respond(())
}
}

Ok(())
Expand Down Expand Up @@ -445,6 +450,11 @@ pub enum ExecutionRequest {
blob_id: BlobId,
callback: Sender<BlobContent>,
},

AssertBlobExists {
blob_id: BlobId,
callback: Sender<()>,
},
}

impl Debug for ExecutionRequest {
Expand Down Expand Up @@ -582,6 +592,11 @@ impl Debug for ExecutionRequest {
.debug_struct("ExecutionRequest::ReadBlob")
.field("blob_id", blob_id)
.finish_non_exhaustive(),

ExecutionRequest::AssertBlobExists { blob_id, .. } => formatter
.debug_struct("ExecutionRequest::AssertBlobExists")
.field("blob_id", blob_id)
.finish_non_exhaustive(),
}
}
}
9 changes: 9 additions & 0 deletions linera-execution/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@ pub trait ExecutionRuntimeContext {
) -> Result<UserServiceCode, ExecutionError>;

async fn get_blob(&self, blob_id: BlobId) -> Result<Blob, ExecutionError>;

async fn contains_blob(&self, blob_id: BlobId) -> Result<bool, ViewError>;
}

#[derive(Clone, Copy, Debug)]
Expand Down Expand Up @@ -492,6 +494,9 @@ pub trait BaseRuntime {

/// Reads a blob content specified by a given `BlobId`.
fn read_blob_content(&mut self, blob_id: &BlobId) -> Result<BlobContent, ExecutionError>;

/// Asserts the existence of a blob with the given `BlobId`.
fn assert_blob_exists(&mut self, blob_id: &BlobId) -> Result<(), ExecutionError>;
}

pub trait ServiceRuntime: BaseRuntime {
Expand Down Expand Up @@ -920,6 +925,10 @@ impl ExecutionRuntimeContext for TestExecutionRuntimeContext {
.ok_or_else(|| SystemExecutionError::BlobNotFoundOnRead(blob_id))?
.clone())
}

async fn contains_blob(&self, blob_id: BlobId) -> Result<bool, ViewError> {
Ok(self.blobs.contains_key(&blob_id))
}
}

impl From<SystemOperation> for Operation {
Expand Down
23 changes: 23 additions & 0 deletions linera-execution/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,10 @@ impl<UserInstance> BaseRuntime for SyncRuntimeHandle<UserInstance> {
fn read_blob_content(&mut self, blob_id: &BlobId) -> Result<BlobContent, ExecutionError> {
self.inner().read_blob_content(blob_id)
}

fn assert_blob_exists(&mut self, blob_id: &BlobId) -> Result<(), ExecutionError> {
self.inner().assert_blob_exists(blob_id)
}
}

impl<UserInstance> BaseRuntime for SyncRuntimeInternal<UserInstance> {
Expand Down Expand Up @@ -1032,6 +1036,25 @@ impl<UserInstance> BaseRuntime for SyncRuntimeInternal<UserInstance> {
.push(OracleResponse::Blob(*blob_id));
Ok(blob_content)
}

fn assert_blob_exists(&mut self, blob_id: &BlobId) -> Result<(), ExecutionError> {
if let Some(responses) = &mut self.replaying_oracle_responses {
match responses.next() {
Some(OracleResponse::Blob(oracle_blob_id)) if oracle_blob_id == *blob_id => {}
Some(_) => return Err(ExecutionError::OracleResponseMismatch),
None => return Err(ExecutionError::MissingOracleResponse),
}
}
self.execution_state_sender
.send_request(|callback| ExecutionRequest::AssertBlobExists {
blob_id: *blob_id,
callback,
})?
.recv_response()?;
self.recorded_oracle_responses
.push(OracleResponse::Blob(*blob_id));
Ok(())
}
}

impl<UserInstance> Clone for SyncRuntimeHandle<UserInstance> {
Expand Down
11 changes: 11 additions & 0 deletions linera-execution/src/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,17 @@ where
.map_err(|_| SystemExecutionError::BlobNotFoundOnRead(blob_id))
.map(Into::into)
}

pub async fn assert_blob_exists(
&mut self,
blob_id: BlobId,
) -> Result<(), SystemExecutionError> {
if self.context().extra().contains_blob(blob_id).await? {
Ok(())
} else {
Err(SystemExecutionError::BlobNotFoundOnRead(blob_id))
}
}
}

#[cfg(test)]
Expand Down
18 changes: 18 additions & 0 deletions linera-execution/src/wasm/system_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,15 @@ where
.map_err(|error| RuntimeError::Custom(error.into()))
}

/// Asserts the existence of a blob with the given `BlobId`.
fn assert_blob_exists(caller: &mut Caller, blob_id: BlobId) -> Result<(), RuntimeError> {
caller
.user_data_mut()
.runtime
.assert_blob_exists(&blob_id)
.map_err(|error| RuntimeError::Custom(error.into()))
}

/// Logs a `message` with the provided information `level`.
fn log(_caller: &mut Caller, message: String, level: log::Level) -> Result<(), RuntimeError> {
match level {
Expand Down Expand Up @@ -545,6 +554,15 @@ where
.map_err(|error| RuntimeError::Custom(error.into()))
}

/// Asserts the existence of a blob with the given `BlobId`.
fn assert_blob_exists(caller: &mut Caller, blob_id: BlobId) -> Result<(), RuntimeError> {
caller
.user_data_mut()
.runtime
.assert_blob_exists(&blob_id)
.map_err(|error| RuntimeError::Custom(error.into()))
}

/// Aborts the query if the current time at block validation is `>= timestamp`. Note that block
/// validation happens at or after the block timestamp, but isn't necessarily the same.
fn assert_before(caller: &mut Caller, timestamp: Timestamp) -> Result<(), RuntimeError> {
Expand Down
5 changes: 5 additions & 0 deletions linera-sdk/src/contract/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,11 @@ where
pub fn read_blob(&mut self, blob_id: BlobId) -> Blob {
BlobContent::from(wit::read_blob_content(blob_id.into())).with_blob_id_unchecked(blob_id)
}

/// Asserts that a blob with the given `BlobId` exists in storage.
pub fn assert_blob_exists(&mut self, blob_id: BlobId) {
wit::assert_blob_exists(blob_id.into())
}
}

/// A helper type that uses the builder pattern to configure how a message is sent, and then
Expand Down
16 changes: 16 additions & 0 deletions linera-sdk/src/contract/test_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ where
expected_service_queries: VecDeque<(ApplicationId, String, String)>,
expected_post_requests: VecDeque<(String, Vec<u8>, Vec<u8>)>,
expected_read_blob_requests: VecDeque<(BlobId, Blob)>,
expected_contains_blob_requests: VecDeque<(BlobId, Option<()>)>,
key_value_store: KeyValueStore,
}

Expand Down Expand Up @@ -91,6 +92,7 @@ where
expected_service_queries: VecDeque::new(),
expected_post_requests: VecDeque::new(),
expected_read_blob_requests: VecDeque::new(),
expected_contains_blob_requests: VecDeque::new(),
key_value_store: KeyValueStore::mock().to_mut(),
}
}
Expand Down Expand Up @@ -619,6 +621,12 @@ where
.push_back((blob_id, response));
}

/// Adds an expected `contains_blob` call, and the response it should return in the test.
pub fn add_expected_contains_blob_requests(&mut self, blob_id: BlobId, response: Option<()>) {
self.expected_contains_blob_requests
.push_back((blob_id, response));
}

/// Queries our application service as an oracle and returns the response.
///
/// Should only be used with queries where it is very likely that all validators will compute
Expand Down Expand Up @@ -672,6 +680,14 @@ where
assert_eq!(*blob_id, expected_blob_id);
response
}

/// Asserts that a blob with the given `BlobId` exists in storage.
pub fn assert_blob_exists(&mut self, blob_id: BlobId) {
let maybe_request = self.expected_contains_blob_requests.pop_front();
let (expected_blob_id, response) = maybe_request.expect("Unexpected contains_blob request");
assert_eq!(blob_id, expected_blob_id);
response.expect("Blob does not exist!");
}
}

/// A type alias for the handler for cross-application calls.
Expand Down
5 changes: 5 additions & 0 deletions linera-sdk/src/service/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,9 @@ where
pub fn read_blob(&mut self, blob_id: BlobId) -> Blob {
BlobContent::from(wit::read_blob_content(blob_id.into())).with_blob_id_unchecked(blob_id)
}

/// Asserts that a blob with the given `BlobId` exists in storage.
pub fn assert_blob_exists(&mut self, blob_id: BlobId) {
wit::assert_blob_exists(blob_id.into())
}
}
18 changes: 16 additions & 2 deletions linera-sdk/src/service/test_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,8 @@ where
/// Fetches a `Blob` from a given `BlobId`.
pub fn read_blob(&mut self, blob_id: BlobId) -> Blob {
self.blobs
.borrow_mut()
.as_mut()
.borrow()
.as_ref()
.and_then(|blobs| blobs.get(&blob_id).cloned())
.unwrap_or_else(|| {
panic!(
Expand All @@ -400,6 +400,20 @@ where
})
}

/// Asserts that a blob with the given `BlobId` exists in storage.
pub fn assert_blob_exists(&mut self, blob_id: BlobId) {
self.blobs
.borrow()
.as_ref()
.map(|blobs| blobs.contains_key(&blob_id))
.unwrap_or_else(|| {
panic!(
"Blob for BlobId {blob_id:?} has not been mocked, \
please call `MockServiceRuntime::set_blob` first"
)
});
}

/// Loads a mocked value from the `cell` cache or panics with a provided `message`.
fn fetch_mocked_value<T>(cell: &Cell<Option<T>>, message: &str) -> T
where
Expand Down
1 change: 1 addition & 0 deletions linera-sdk/wit/contract-system-api.wit
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface contract-system-api {
http-post: func(query: string, content-type: string, payload: list<u8>) -> list<u8>;
assert-before: func(timestamp: timestamp);
read-blob-content: func(blob-id: blob-id) -> blob-content;
assert-blob-exists: func(blob-id: blob-id);
log: func(message: string, level: log-level);
consume-fuel: func(fuel: u64);

Expand Down
1 change: 1 addition & 0 deletions linera-sdk/wit/service-system-api.wit
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface service-system-api {
query-service: func(application-id: application-id, query: list<u8>) -> list<u8>;
http-post: func(query: string, content-type: string, payload: list<u8>) -> list<u8>;
read-blob-content: func(blob-id: blob-id) -> blob-content;
assert-blob-exists: func(blob-id: blob-id);
assert-before: func(timestamp: timestamp);
log: func(message: string, level: log-level);

Expand Down
10 changes: 7 additions & 3 deletions linera-storage/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,13 @@ pub trait Storage: Sized {
hash: Vec<CryptoHash>,
) -> Result<Vec<bool>, ViewError>;

/// Tests existence of a blob with the given hash.
/// Tests the existence of a blob with the given blob ID.
async fn contains_blob(&self, blob_id: BlobId) -> Result<bool, ViewError>;

/// List the missing blobs from the storage.
/// Lists the missing blobs from storage.
async fn missing_blobs(&self, blob_ids: Vec<BlobId>) -> Result<Vec<BlobId>, ViewError>;

/// Tests existence of a blob state with the given hash.
/// Tests existence of a blob state with the given blob ID.
async fn contains_blob_state(&self, blob_id: BlobId) -> Result<bool, ViewError>;

/// Reads the hashed certificate value with the given hash.
Expand Down Expand Up @@ -473,4 +473,8 @@ where
async fn get_blob(&self, blob_id: BlobId) -> Result<Blob, ExecutionError> {
Ok(self.storage.read_blob(blob_id).await?)
}

async fn contains_blob(&self, blob_id: BlobId) -> Result<bool, ViewError> {
self.storage.contains_blob(blob_id).await
}
}

0 comments on commit 34c8a1f

Please sign in to comment.