From 91372cb5a59fc27a279da32986beb73eb8e2abd0 Mon Sep 17 00:00:00 2001 From: Andre da Silva Date: Sat, 12 Oct 2024 01:56:30 -0300 Subject: [PATCH] Create ApplicationDescription blob type --- linera-base/src/data_types.rs | 102 +++++++++++- linera-base/src/identifiers.rs | 151 ++++++++++++++++++ linera-base/src/unit_tests.rs | 12 +- linera-core/src/client/mod.rs | 10 +- linera-execution/src/lib.rs | 4 +- .../tests/snapshots/format__format.yaml.snap | 25 +++ 6 files changed, 291 insertions(+), 13 deletions(-) diff --git a/linera-base/src/data_types.rs b/linera-base/src/data_types.rs index c210f74d6303..193e5b5fc8d4 100644 --- a/linera-base/src/data_types.rs +++ b/linera-base/src/data_types.rs @@ -31,11 +31,11 @@ use thiserror::Error; #[cfg(with_metrics)] use crate::prometheus_util::{self, MeasureLatency}; use crate::{ - crypto::BcsHashable, + crypto::{BcsHashable, CryptoHash}, doc_scalar, hex_debug, identifiers::{ - ApplicationId, BlobId, BlobType, BytecodeId, Destination, GenericApplicationId, MessageId, - UserApplicationId, + ApplicationId, BlobId, BlobType, BlobUserApplicationId, BytecodeId, ChainId, Destination, + GenericApplicationId, MessageId, UserApplicationId, }, time::{Duration, SystemTime}, }; @@ -804,6 +804,25 @@ pub struct UserApplicationDescription { pub required_application_ids: Vec, } +/// Description of the necessary information to run a user application used within blobs. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Hash, Serialize, WitType, WitStore)] +pub struct BlobUserApplicationDescription { + /// The unique ID of the bytecode to use for the application. + pub bytecode_id: BytecodeId, + /// The chain ID that created the application. + pub creator_chain_id: ChainId, + /// Height of the block that created this application. + pub block_height: BlockHeight, + /// At what operation index of the block this application was created. + pub operation_index: u32, + /// The parameters of the application. + #[serde(with = "serde_bytes")] + #[debug(with = "hex_debug")] + pub parameters: Vec, + /// Required dependencies. + pub required_application_ids: Vec, +} + impl From<&UserApplicationDescription> for UserApplicationId { fn from(description: &UserApplicationDescription) -> Self { UserApplicationId { @@ -813,6 +832,26 @@ impl From<&UserApplicationDescription> for UserApplicationId { } } +impl TryFrom<&BlobUserApplicationDescription> for BlobUserApplicationId { + type Error = bcs::Error; + + fn try_from(description: &BlobUserApplicationDescription) -> Result { + Ok(BlobUserApplicationId::new( + CryptoHash::new(&description.blob_bytes()?), + description.bytecode_id, + )) + } +} + +impl BcsHashable for BlobUserApplicationDescription {} + +impl BlobUserApplicationDescription { + /// Gets the `BlobBytes` for this `BlobUserApplicationDescription`. + pub fn blob_bytes(&self) -> Result { + Ok(BlobBytes(bcs::to_bytes(self)?)) + } +} + /// A WebAssembly module's bytecode. #[derive(Clone, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct Bytecode { @@ -946,6 +985,8 @@ pub enum BlobContent { ContractBytecode(CompressedBytecode), /// A blob containing service bytecode. ServiceBytecode(CompressedBytecode), + /// A blob containing an application description. + ApplicationDescription(BlobUserApplicationDescription), } impl fmt::Debug for BlobContent { @@ -954,6 +995,13 @@ impl fmt::Debug for BlobContent { BlobContent::Data(_) => write!(f, "BlobContent::Data"), BlobContent::ContractBytecode(_) => write!(f, "BlobContent::ContractBytecode"), BlobContent::ServiceBytecode(_) => write!(f, "BlobContent::ServiceBytecode"), + BlobContent::ApplicationDescription(description) => f + .debug_struct("BlobContent::ApplicationDescription") + .field("bytecode_id", &description.bytecode_id) + .field("creator_chain_id", &description.creator_chain_id) + .field("block_height", &description.block_height) + .field("operation_index", &description.operation_index) + .finish_non_exhaustive(), } } } @@ -969,6 +1017,9 @@ impl BlobContent { BlobType::ServiceBytecode => BlobContent::ServiceBytecode(CompressedBytecode { compressed_bytes: bytes, }), + BlobType::ApplicationDescription => BlobContent::ApplicationDescription( + bcs::from_bytes(&bytes).expect("Deserializing blob bytes should not fail!"), + ), } } @@ -987,6 +1038,13 @@ impl BlobContent { BlobContent::ServiceBytecode(compressed_bytecode) } + /// Creates a new application description [`BlobContent`] from a [`BlobUserApplicationDescription`]. + pub fn new_application_description( + application_description: BlobUserApplicationDescription, + ) -> Self { + BlobContent::ApplicationDescription(application_description) + } + /// Creates a `Blob` without checking that this is the correct `BlobId`. pub fn with_blob_id_unchecked(self, blob_id: BlobId) -> Blob { Blob { @@ -1005,6 +1063,11 @@ impl BlobContent { BlobType::ServiceBytecode if matches!(&self, BlobContent::ServiceBytecode(_)) => { Some(()) } + BlobType::ApplicationDescription + if matches!(&self, BlobContent::ApplicationDescription(_)) => + { + Some(()) + } _ => None, }?; @@ -1020,14 +1083,25 @@ impl BlobContent { /// Gets the inner blob's bytes. pub fn inner_bytes(&self) -> Vec { match self { - BlobContent::Data(bytes) => bytes, + BlobContent::Data(bytes) => bytes.clone(), BlobContent::ContractBytecode(compressed_bytecode) => { - &compressed_bytecode.compressed_bytes + compressed_bytecode.compressed_bytes.clone() } BlobContent::ServiceBytecode(compressed_bytecode) => { - &compressed_bytecode.compressed_bytes + compressed_bytecode.compressed_bytes.clone() + } + BlobContent::ApplicationDescription(description) => { + bcs::to_bytes(description).expect("Deserializing blob bytes should not fail!") } } + } + + /// Moves ownership of the blob's application description. If the `BlobContent` is of the wrong type, returns `None`. + pub fn into_inner_application_description(self) -> Option { + match self { + BlobContent::ApplicationDescription(description) => Some(description), + _ => None, + } .clone() } @@ -1044,7 +1118,7 @@ impl From for BlobContent { } impl From for Blob { - fn from(content: BlobContent) -> Blob { + fn from(content: BlobContent) -> Self { Self { id: BlobId::from_content(&content), content, @@ -1083,6 +1157,13 @@ impl Blob { BlobContent::new_service_bytecode(compressed_bytecode).into() } + /// Creates a new application description [`Blob`] from a [`BlobUserApplicationDescription`]. + pub fn new_application_description( + application_description: BlobUserApplicationDescription, + ) -> Self { + BlobContent::new_application_description(application_description).into() + } + /// A content-addressed blob ID i.e. the hash of the `Blob`. pub fn id(&self) -> BlobId { self.id @@ -1103,8 +1184,13 @@ impl Blob { self.content.inner_bytes() } + /// Moves ownership of the blob's application description. If the `Blob` is of the wrong type, returns `None`. + pub fn into_inner_application_description(self) -> Option { + self.content.into_inner_application_description() + } + /// Loads data blob content from a file. - pub async fn load_data_blob_from_file(path: impl AsRef) -> io::Result { + pub async fn load_data_blob_from_file(path: impl AsRef) -> Result { Ok(Self::new_data(fs::read(path)?)) } } diff --git a/linera-base/src/identifiers.rs b/linera-base/src/identifiers.rs index 3767b7ac6ffe..b165189ec051 100644 --- a/linera-base/src/identifiers.rs +++ b/linera-base/src/identifiers.rs @@ -171,6 +171,8 @@ pub enum BlobType { ContractBytecode, /// A blob containing service bytecode. ServiceBytecode, + /// A blob containing an application description. + ApplicationDescription, } impl Display for BlobType { @@ -196,6 +198,7 @@ impl From<&BlobContent> for BlobType { BlobContent::Data(_) => BlobType::Data, BlobContent::ContractBytecode(_) => BlobType::ContractBytecode, BlobContent::ServiceBytecode(_) => BlobType::ServiceBytecode, + BlobContent::ApplicationDescription(_) => BlobType::ApplicationDescription, } } } @@ -299,10 +302,24 @@ pub struct ApplicationId { pub creation: MessageId, } +/// A unique identifier for a user application from a blob. +#[derive(WitLoad, WitStore, WitType)] +#[cfg_attr(with_testing, derive(Default))] +pub struct BlobApplicationId { + /// The hash of the `UserApplicationDescription` this refers to. + pub application_description_hash: CryptoHash, + /// The bytecode to use for the application. + pub bytecode_id: BytecodeId, +} + /// Alias for `ApplicationId`. Use this alias in the core /// protocol where the distinction with the more general enum `GenericApplicationId` matters. pub type UserApplicationId = ApplicationId; +/// Alias for `BlobApplicationId`. Use this alias in the core +/// protocol where the distinction with the more general enum `GenericApplicationId` matters. +pub type BlobUserApplicationId = BlobApplicationId; + /// A unique identifier for an application. #[derive( Eq, @@ -808,6 +825,140 @@ impl ApplicationId { } } +// Cannot use #[derive(Clone)] because it requires `A: Clone`. +impl Clone for BlobApplicationId { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for BlobApplicationId {} + +impl PartialEq for BlobApplicationId { + fn eq(&self, other: &Self) -> bool { + self.application_description_hash == other.application_description_hash + && self.bytecode_id == other.bytecode_id + } +} + +impl Eq for BlobApplicationId {} + +impl PartialOrd for BlobApplicationId { + fn partial_cmp(&self, other: &Self) -> Option { + (self.application_description_hash, self.bytecode_id) + .partial_cmp(&(other.application_description_hash, other.bytecode_id)) + } +} + +impl Ord for BlobApplicationId { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + (self.application_description_hash, self.bytecode_id) + .cmp(&(other.application_description_hash, other.bytecode_id)) + } +} + +impl Hash for BlobApplicationId { + fn hash(&self, state: &mut H) { + self.application_description_hash.hash(state); + self.bytecode_id.hash(state); + } +} + +impl Debug for BlobApplicationId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("BlobApplicationId") + .field( + "application_description_hash", + &self.application_description_hash, + ) + .field("bytecode_id", &self.bytecode_id) + .finish() + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename = "BlobApplicationId")] +struct SerializableBlobApplicationId { + pub application_description_hash: CryptoHash, + pub bytecode_id: BytecodeId, +} + +impl Serialize for BlobApplicationId { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + if serializer.is_human_readable() { + let bytes = bcs::to_bytes(&SerializableBlobApplicationId { + application_description_hash: self.application_description_hash, + bytecode_id: self.bytecode_id.forget_abi(), + }) + .map_err(serde::ser::Error::custom)?; + serializer.serialize_str(&hex::encode(bytes)) + } else { + SerializableBlobApplicationId::serialize( + &SerializableBlobApplicationId { + application_description_hash: self.application_description_hash, + bytecode_id: self.bytecode_id.forget_abi(), + }, + serializer, + ) + } + } +} + +impl<'de, A> Deserialize<'de> for BlobApplicationId { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + if deserializer.is_human_readable() { + let s = String::deserialize(deserializer)?; + let application_id_bytes = hex::decode(s).map_err(serde::de::Error::custom)?; + let application_id: SerializableBlobApplicationId = + bcs::from_bytes(&application_id_bytes).map_err(serde::de::Error::custom)?; + Ok(BlobApplicationId { + application_description_hash: application_id.application_description_hash, + bytecode_id: application_id.bytecode_id.with_abi(), + }) + } else { + let value = SerializableBlobApplicationId::deserialize(deserializer)?; + Ok(BlobApplicationId { + application_description_hash: value.application_description_hash, + bytecode_id: value.bytecode_id.with_abi(), + }) + } + } +} + +impl BlobApplicationId { + /// Creates an application ID from the application description hash. + pub fn new(application_description_hash: CryptoHash, bytecode_id: BytecodeId) -> Self { + BlobApplicationId { + application_description_hash, + bytecode_id, + } + } + + /// Specializes an application ID for a given ABI. + pub fn with_abi(self) -> BlobApplicationId { + BlobApplicationId { + application_description_hash: self.application_description_hash, + bytecode_id: self.bytecode_id.with_abi(), + } + } +} + +impl BlobApplicationId { + /// Forgets the ABI of a bytecode ID (if any). + pub fn forget_abi(self) -> BlobApplicationId { + BlobApplicationId { + application_description_hash: self.application_description_hash, + bytecode_id: self.bytecode_id.forget_abi(), + } + } +} + impl Display for Owner { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { Display::fmt(&self.0, f) diff --git a/linera-base/src/unit_tests.rs b/linera-base/src/unit_tests.rs index e7e69ece0750..748220c67e95 100644 --- a/linera-base/src/unit_tests.rs +++ b/linera-base/src/unit_tests.rs @@ -12,7 +12,8 @@ use crate::{ crypto::{CryptoHash, PublicKey}, data_types::{Amount, BlockHeight, Resources, SendMessageRequest, TimeDelta, Timestamp}, identifiers::{ - Account, ApplicationId, BytecodeId, ChainId, ChannelName, Destination, MessageId, Owner, + Account, ApplicationId, BlobApplicationId, BytecodeId, ChainId, ChannelName, Destination, + MessageId, Owner, }, ownership::{ChainOwnership, TimeoutConfig}, }; @@ -111,6 +112,15 @@ fn application_id_test_case() -> ApplicationId { } } +/// Creates a dummy [`BlobApplicationId`] instance to use for the WIT roundtrip test. +#[allow(dead_code)] +fn blob_application_id_test_case() -> BlobApplicationId { + BlobApplicationId::new( + CryptoHash::test_hash("application description"), + bytecode_id_test_case(), + ) +} + /// Creates a dummy [`BytecodeId`] instance to use for the WIT roundtrip test. fn bytecode_id_test_case() -> BytecodeId { BytecodeId::new( diff --git a/linera-core/src/client/mod.rs b/linera-core/src/client/mod.rs index 6c7c31e3268f..85047ee08063 100644 --- a/linera-core/src/client/mod.rs +++ b/linera-core/src/client/mod.rs @@ -467,6 +467,9 @@ pub enum ChainClientError { #[error(transparent)] CommunicationError(#[from] CommunicationError), + #[error(transparent)] + BcsError(#[from] bcs::Error), + #[error("Internal error within chain client: {0}")] InternalError(&'static str), @@ -2527,9 +2530,12 @@ where &self, bytes: Vec>, ) -> Result, ChainClientError> { - let blobs = bytes.into_iter().map(Blob::new_data); + let mut blobs = Vec::new(); + for byte_vec in bytes { + blobs.push(Blob::new_data(byte_vec)); + } let publish_blob_operations = blobs - .clone() + .iter() .map(|blob| { Operation::System(SystemOperation::PublishDataBlob { blob_hash: blob.id().hash, diff --git a/linera-execution/src/lib.rs b/linera-execution/src/lib.rs index f7ae725e5f86..599be273fb82 100644 --- a/linera-execution/src/lib.rs +++ b/linera-execution/src/lib.rs @@ -142,6 +142,8 @@ pub enum ExecutionError { JoinError(#[from] linera_base::task::Error), #[error(transparent)] DecompressionError(#[from] DecompressionError), + #[error(transparent)] + BcsError(#[from] bcs::Error), #[error("The given promise is invalid or was polled once already")] InvalidPromise, @@ -184,8 +186,6 @@ pub enum ExecutionError { UnexpectedOracleResponse, #[error("Invalid JSON: {}", .0)] Json(#[from] serde_json::Error), - #[error(transparent)] - Bcs(#[from] bcs::Error), #[error("Recorded response for oracle query has the wrong type")] OracleResponseMismatch, #[error("Assertion failed: local time {local_time} is not earlier than {timestamp}")] diff --git a/linera-rpc/tests/snapshots/format__format.yaml.snap b/linera-rpc/tests/snapshots/format__format.yaml.snap index 84fa57d04323..214c761cd848 100644 --- a/linera-rpc/tests/snapshots/format__format.yaml.snap +++ b/linera-rpc/tests/snapshots/format__format.yaml.snap @@ -43,6 +43,12 @@ ApplicationPermissions: - close_chain: SEQ: TYPENAME: ApplicationId +BlobApplicationId: + STRUCT: + - application_description_hash: + TYPENAME: CryptoHash + - bytecode_id: + TYPENAME: BytecodeId BlobContent: ENUM: 0: @@ -56,6 +62,10 @@ BlobContent: ServiceBytecode: NEWTYPE: TYPENAME: CompressedBytecode + 3: + ApplicationDescription: + NEWTYPE: + TYPENAME: BlobUserApplicationDescription BlobId: STRUCT: - hash: @@ -70,6 +80,21 @@ BlobType: ContractBytecode: UNIT 2: ServiceBytecode: UNIT + 3: + ApplicationDescription: UNIT +BlobUserApplicationDescription: + STRUCT: + - bytecode_id: + TYPENAME: BytecodeId + - creator_chain_id: + TYPENAME: ChainId + - block_height: + TYPENAME: BlockHeight + - operation_index: U32 + - parameters: BYTES + - required_application_ids: + SEQ: + TYPENAME: BlobApplicationId Block: STRUCT: - chain_id: