Improve API-related things#24
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #24 +/- ##
========================================
+ Coverage 70.4% 75.9% +5.5%
========================================
Files 18 18
Lines 1732 2254 +522
========================================
+ Hits 1219 1711 +492
- Misses 513 543 +30
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Signed-off-by: Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
Bring changes from ultrasonic RC2
|
@codecov-ai-reviewer review |
|
On it! We are reviewing the PR and will provide feedback shortly. |
PR DescriptionThis pull request refactors the Sonic API to improve its structure, flexibility, and maintainability. It introduces a new Click to see moreKey Technical Changes
Architecture Decisions
Dependencies and Interactions
Risk Considerations
Notable Implementation Details
|
|
|
||
| //! API defines how a contract can be interfaced by software. | ||
| //! API defines how software can interface a contract. |
There was a problem hiding this comment.
Great improvement to the API documentation in the comment change from 'API defines how a contract can be interfaced by software' to 'API defines how software can interface a contract'. The new phrasing emphasizes the software's role in interfacing with contracts, which better aligns with the purpose of the API.
| /// It is created just for UI, so users can easily visually distinguish different sets of APIs from | ||
| /// each other. | ||
| /// | ||
| /// This type is not - and must not be used in any verification. | ||
| #[derive(Wrapper, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From)] | ||
| #[wrapper(Deref, BorrowSlice, Hex, Index, RangeOps)] | ||
| #[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)] | ||
| #[strict_type(lib = LIB_NAME_SONIC)] | ||
| pub struct ApisChecksum( | ||
| #[from] |
There was a problem hiding this comment.
The ApisChecksum type is marked as 'not a unique identifier' and specifically mentioned as 'not to be used in any verification'. This warning should be more prominent, perhaps with additional validation in code to ensure it's not accidentally used for verification purposes. Consider adding a method that explicitly validates its intended use.
| let lib = lib_map.get(id).ok_or(SemanticError::MissedCodexLib(*id))?; | ||
| lib_ids.extend(lib.libs.iter().copied()); | ||
| count = lib_ids.len(); | ||
| i += 1; | ||
| } | ||
| for id in lib_map.keys() { | ||
| if !lib_ids.contains(id) { | ||
| return Err(SemanticError::ExcessiveCodexLib(*id)); | ||
| } | ||
| } | ||
|
|
||
| // Check API libs for redundancies and completeness | ||
| let lib_map = self | ||
| .api_libs | ||
| .iter() | ||
| .map(|lib| (lib.lib_id(), lib)) | ||
| .collect::<IndexMap<_, _>>(); | ||
|
|
||
| let mut lib_ids = indexset![]; | ||
| for api in self.apis() { | ||
| for agg in api.aggregators.values() { | ||
| if let Aggregator::AluVM(entry) = agg { | ||
| lib_ids.insert(entry.lib_id); | ||
| } | ||
| } | ||
| for glob in api.global.values() { | ||
| if let StateConvertor::AluVM(entry) = glob.convertor { | ||
| lib_ids.insert(entry.lib_id); | ||
| } | ||
| if let StateBuilder::AluVM(entry) = glob.builder { | ||
| lib_ids.insert(entry.lib_id); | ||
| } | ||
| if let RawConvertor::AluVM(entry) = glob.raw_convertor { | ||
| lib_ids.insert(entry.lib_id); | ||
| } | ||
| if let RawBuilder::AluVM(entry) = glob.raw_builder { | ||
| lib_ids.insert(entry.lib_id); | ||
| } | ||
| } | ||
| for owned in api.owned.values() { | ||
| if let StateConvertor::AluVM(entry) = owned.convertor { | ||
| lib_ids.insert(entry.lib_id); | ||
| } | ||
| if let StateBuilder::AluVM(entry) = owned.builder { | ||
| lib_ids.insert(entry.lib_id); | ||
| } | ||
| if let StateBuilder::AluVM(entry) = owned.witness_builder { | ||
| lib_ids.insert(entry.lib_id); | ||
| } | ||
| if let StateArithm::AluVM(entry) = owned.arithmetics { | ||
| lib_ids.insert(entry.lib_id); | ||
| } | ||
| } | ||
| } | ||
| let mut i = 0usize; | ||
| let mut count = lib_ids.len(); | ||
| while i < count { | ||
| let id = lib_ids.get_index(i).expect("index is valid"); | ||
| let lib = lib_map.get(id).ok_or(SemanticError::MissedApiLib(*id))?; |
There was a problem hiding this comment.
The refactoring from 'immutable'/'destructible' to 'global'/'owned' improves semantic clarity, making the code more self-explanatory. This is a good change for maintainability. However, ensure this breaking change is well-documented in the migration guide for existing users.
| e.commit_to_hash(&self.default); | ||
| // We do not commit to the codex_libs since they are not a part of APIs | ||
| // and are commit to inside the codex. | ||
| // The fact that there are no other libs | ||
| // is verified in the Articles and Issuer constructors. | ||
| let apis = SmallOrdMap::from_iter_checked( | ||
| self.custom | ||
| .iter() | ||
| .map(|(name, api)| (name.clone(), api.api_id())), | ||
| ); | ||
| e.commit_to_linear_map(&apis); | ||
| let libs = SmallOrdSet::from_iter_checked(self.api_libs.iter().map(Lib::lib_id)); | ||
| e.commit_to_linear_set(&libs); | ||
| e.commit_to_serialized(&self.types.id()); | ||
| } | ||
| } | ||
|
|
||
| impl Semantics { | ||
| pub fn apis_checksum(&self) -> ApisChecksum { self.commit_id() } | ||
|
|
||
| /// Iterates over all APIs, including default and named ones. | ||
| pub fn apis(&self) -> impl Iterator<Item = &Api> { [&self.default].into_iter().chain(self.custom.values()) } | ||
|
|
||
| /// Check whether this semantics object matches codex and the provided set of libraries for it. | ||
| pub fn check(&self, codex: &Codex) -> Result<(), SemanticError> { | ||
| let codex_id = codex.codex_id(); | ||
|
|
||
| let mut ids = bset![]; | ||
| for api in self.apis() { | ||
| if api.codex_id != codex_id { | ||
| return Err(SemanticError::CodexMismatch); | ||
| } | ||
| let api_id = api.api_id(); | ||
| if !ids.insert(api_id) { | ||
| return Err(SemanticError::DuplicatedApi(api_id)); | ||
| } | ||
| } | ||
|
|
||
| // Check codex libs for redundancies and completeness | ||
| let lib_map = self | ||
| .codex_libs | ||
| .iter() | ||
| .map(|lib| (lib.lib_id(), lib)) | ||
| .collect::<IndexMap<_, _>>(); | ||
|
|
||
| let mut lib_ids = codex | ||
| .verifiers | ||
| .values() | ||
| .map(|entry| entry.lib_id) | ||
| .collect::<IndexSet<_>>(); | ||
| let mut i = 0usize; | ||
| let mut count = lib_ids.len(); | ||
| while i < count { | ||
| let id = lib_ids.get_index(i).expect("index is valid"); | ||
| let lib = lib_map.get(id).ok_or(SemanticError::MissedCodexLib(*id))?; | ||
| lib_ids.extend(lib.libs.iter().copied()); | ||
| count = lib_ids.len(); | ||
| i += 1; | ||
| } | ||
| for id in lib_map.keys() { | ||
| if !lib_ids.contains(id) { | ||
| return Err(SemanticError::ExcessiveCodexLib(*id)); | ||
| } | ||
| } | ||
|
|
||
| // Check API libs for redundancies and completeness | ||
| let lib_map = self | ||
| .api_libs | ||
| .iter() | ||
| .map(|lib| (lib.lib_id(), lib)) | ||
| .collect::<IndexMap<_, _>>(); | ||
|
|
||
| let mut lib_ids = indexset![]; | ||
| for api in self.apis() { | ||
| for agg in api.aggregators.values() { | ||
| if let Aggregator::AluVM(entry) = agg { | ||
| lib_ids.insert(entry.lib_id); |
There was a problem hiding this comment.
The SubAggregator enum has many variants that perform similar mathematical operations but with different error handling (e.g., SumUnwrap vs SumOrDefault). Consider adding shared implementation for these related operations to reduce code duplication and improve maintenance.
| fn commit_encode(&self, e: &mut CommitEngine) { | ||
| e.commit_to_serialized(&self.version); | ||
| e.commit_to_hash(&self.default); | ||
| // We do not commit to the codex_libs since they are not a part of APIs | ||
| // and are commit to inside the codex. | ||
| // The fact that there are no other libs | ||
| // is verified in the Articles and Issuer constructors. | ||
| let apis = SmallOrdMap::from_iter_checked( | ||
| self.custom | ||
| .iter() | ||
| .map(|(name, api)| (name.clone(), api.api_id())), | ||
| ); | ||
| e.commit_to_linear_map(&apis); | ||
| let libs = SmallOrdSet::from_iter_checked(self.api_libs.iter().map(Lib::lib_id)); | ||
| e.commit_to_linear_set(&libs); | ||
| e.commit_to_serialized(&self.types.id()); | ||
| } | ||
| } | ||
|
|
||
| impl Semantics { | ||
| pub fn apis_checksum(&self) -> ApisChecksum { self.commit_id() } | ||
|
|
||
| /// Iterates over all APIs, including default and named ones. | ||
| pub fn apis(&self) -> impl Iterator<Item = &Api> { [&self.default].into_iter().chain(self.custom.values()) } | ||
|
|
||
| /// Check whether this semantics object matches codex and the provided set of libraries for it. | ||
| pub fn check(&self, codex: &Codex) -> Result<(), SemanticError> { | ||
| let codex_id = codex.codex_id(); | ||
|
|
||
| let mut ids = bset![]; | ||
| for api in self.apis() { | ||
| if api.codex_id != codex_id { | ||
| return Err(SemanticError::CodexMismatch); | ||
| } | ||
| let api_id = api.api_id(); | ||
| if !ids.insert(api_id) { | ||
| return Err(SemanticError::DuplicatedApi(api_id)); | ||
| } | ||
| } | ||
|
|
||
| // Check codex libs for redundancies and completeness | ||
| let lib_map = self | ||
| .codex_libs | ||
| .iter() | ||
| .map(|lib| (lib.lib_id(), lib)) | ||
| .collect::<IndexMap<_, _>>(); | ||
|
|
||
| let mut lib_ids = codex | ||
| .verifiers | ||
| .values() | ||
| .map(|entry| entry.lib_id) | ||
| .collect::<IndexSet<_>>(); | ||
| let mut i = 0usize; | ||
| let mut count = lib_ids.len(); | ||
| while i < count { | ||
| let id = lib_ids.get_index(i).expect("index is valid"); | ||
| let lib = lib_map.get(id).ok_or(SemanticError::MissedCodexLib(*id))?; | ||
| lib_ids.extend(lib.libs.iter().copied()); | ||
| count = lib_ids.len(); | ||
| i += 1; | ||
| } | ||
| for id in lib_map.keys() { | ||
| if !lib_ids.contains(id) { | ||
| return Err(SemanticError::ExcessiveCodexLib(*id)); | ||
| } | ||
| } | ||
|
|
||
| // Check API libs for redundancies and completeness | ||
| let lib_map = self | ||
| .api_libs | ||
| .iter() | ||
| .map(|lib| (lib.lib_id(), lib)) | ||
| .collect::<IndexMap<_, _>>(); | ||
|
|
||
| let mut lib_ids = indexset![]; | ||
| for api in self.apis() { | ||
| for agg in api.aggregators.values() { | ||
| if let Aggregator::AluVM(entry) = agg { | ||
| lib_ids.insert(entry.lib_id); | ||
| } | ||
| } | ||
| for glob in api.global.values() { | ||
| if let StateConvertor::AluVM(entry) = glob.convertor { | ||
| lib_ids.insert(entry.lib_id); | ||
| } | ||
| if let StateBuilder::AluVM(entry) = glob.builder { | ||
| lib_ids.insert(entry.lib_id); | ||
| } | ||
| if let RawConvertor::AluVM(entry) = glob.raw_convertor { |
There was a problem hiding this comment.
In the Semantics::check() method, there are two separate code blocks checking libraries that follow a very similar pattern. This is a good candidate for refactoring into a common helper function to reduce duplication and improve readability.
| #[derive(StrictType, StrictDumb, StrictEncode)] | ||
| // We must not derive or implement StrictDecode for Issuer, since we cannot validate signature | ||
| // inside it | ||
| #[strict_type(lib = LIB_NAME_SONIC)] | ||
| pub struct Articles { | ||
| apis: ApiDescriptor, | ||
| /// We can't use [`Issuer`] here since we will duplicate the codex between it and the [`Issue`]. | ||
| /// Thus, a dedicated substructure [`Semantics`] is introduced, which keeps a shared part of | ||
| /// both [`Issuer`] and [`Articles`]. | ||
| semantics: Semantics, | ||
| /// Signature from the contract issuer (`issue.meta.issuer`) over the articles' id. | ||
| /// | ||
| /// NB: it must precede the issue, which contains genesis! | ||
| /// Since genesis is read with a stream-supporting procedure later. | ||
| sig: Option<SigBlob>, | ||
| /// The contract issue. | ||
| issue: Issue, | ||
| } | ||
|
|
||
| impl Articles { | ||
| fn articles_id(&self) -> ArticlesId { | ||
| let custom_api_ids = SmallOrdMap::from_iter_checked( | ||
| self.apis | ||
| .custom | ||
| .iter() | ||
| .map(|(name, api)| (name.clone(), api.api_id())), | ||
| ); | ||
| ArticlesCommitment { | ||
| contract_id: self.contract_id(), | ||
| default_api_id: self.apis.default.api_id(), | ||
| custom_api_ids, | ||
| /// Construct articles from a signed contract semantic and the contract issue under that | ||
| /// semantics. |
There was a problem hiding this comment.
The Articles::with constructor now takes a signature validator, which is an improvement for flexibility. However, the signature validator accepts a generic error type E that gets discarded when mapping to SemanticError::InvalidSignature. This loses specific error details. Consider using a more specific error type or preserving the original error for better debugging.
| pub fn genesis_opid(&self) -> Opid { self.issue.genesis_opid() } | ||
|
|
||
| pub fn apis(&self) -> &ApiDescriptor { &self.apis } | ||
| pub fn default_api(&self) -> &Api { &self.apis.default } | ||
| pub fn custom_apis(&self) -> impl Iterator<Item = (&TypeName, &Api)> { self.apis.custom.iter() } | ||
| pub fn types(&self) -> &TypeSystem { &self.apis.types } | ||
| /// Get a reference to the contract semantic. | ||
| pub fn semantics(&self) -> &Semantics { &self.semantics } | ||
| /// Get a reference to the default API. | ||
| pub fn default_api(&self) -> &Api { &self.semantics.default } | ||
| /// Get an iterator over the custom APIs. | ||
| pub fn custom_apis(&self) -> impl Iterator<Item = (&TypeName, &Api)> { self.semantics.custom.iter() } | ||
| /// Get a reference to the type system. | ||
| pub fn types(&self) -> &TypeSystem { &self.semantics.types } | ||
| /// Iterates over all APIs, including the default and the named ones. | ||
| pub fn apis(&self) -> impl Iterator<Item = &Api> { self.semantics.apis() } | ||
| /// Iterates over all codex libraries. | ||
| pub fn codex_libs(&self) -> impl Iterator<Item = &Lib> { self.semantics.codex_libs.iter() } | ||
|
|
There was a problem hiding this comment.
The Articles::upgrade_apis method now returns a boolean to indicate whether an upgrade occurred. This is good for informing clients, but the commit doesn't add usage examples. Consider adding documentation showing how clients should handle this return value.
No description provided.