Skip to content

Improve API-related things#24

Merged
dr-orlovsky merged 41 commits into
masterfrom
v0.12
Jun 1, 2025
Merged

Improve API-related things#24
dr-orlovsky merged 41 commits into
masterfrom
v0.12

Conversation

@dr-orlovsky
Copy link
Copy Markdown
Member

No description provided.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 25, 2025

Codecov Report

Attention: Patch coverage is 76.65505% with 201 lines in your changes missing coverage. Please review.

Project coverage is 75.9%. Comparing base (706089f) to head (c74a154).
Report is 42 commits behind head on master.

Files with missing lines Patch % Lines
api/src/state/aggregators.rs 79.9% 48 Missing ⚠️
api/src/api.rs 75.2% 32 Missing ⚠️
api/src/state/adaptors.rs 72.3% 31 Missing ⚠️
api/src/articles.rs 59.1% 27 Missing ⚠️
api/src/builders.rs 56.1% 25 Missing ⚠️
src/ledger.rs 86.7% 12 Missing ⚠️
api/src/issuer.rs 86.7% 10 Missing ⚠️
src/state.rs 82.6% 8 Missing ⚠️
api/src/state/arithmetics.rs 0.0% 5 Missing ⚠️
api/src/state/raw.rs 0.0% 2 Missing ⚠️
... and 1 more
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     
Flag Coverage Δ
rust 75.9% <76.7%> (+5.5%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@dr-orlovsky
Copy link
Copy Markdown
Member Author

@codecov-ai-reviewer review

@ghost
Copy link
Copy Markdown

ghost commented Jun 1, 2025

On it! We are reviewing the PR and will provide feedback shortly.

@ghost
Copy link
Copy Markdown

ghost commented Jun 1, 2025

PR Description

This pull request refactors the Sonic API to improve its structure, flexibility, and maintainability. It introduces a new Semantics struct to encapsulate contract API definitions, improves versioning and signature validation, and updates state management to align with the new API design.

Click to see more

Key Technical Changes

  1. API Restructuring: Replaces ImmutableApi and DestructibleApi with GlobalApi and OwnedApi, respectively, for clearer semantics. Introduces Semantics struct to group API definitions, codex libraries, and type systems.
  2. Versioning and Checksums: Adds ApisChecksum and ArticlesId for improved API versioning and identification. Introduces ParseVersionedError for handling version parsing errors.
  3. Signature Validation: Implements signature validation in Articles::with and Issuer::with using a provided validator function.
  4. State Management: Updates state management in EffectiveState and ProcessedState to reflect the new API structure, including changes to how state is applied and rolled back.
  5. Aggregator Refactoring: Refactors StateAggregator into Aggregator and SubAggregator for more flexible state aggregation.
  6. Dependency Updates: Updates dependencies in Cargo.toml and Cargo.lock, including adding once_cell_polyfill and indexmap.

Architecture Decisions

  1. Semantics Struct: The decision to introduce the Semantics struct centralizes API-related data, improving code organization and reducing redundancy between Issuer and Articles. This promotes a more modular design.
  2. Signature Validation Abstraction: The use of a signature validator function allows for greater flexibility in signature schemes and validation logic.
  3. Global/Owned State: The renaming of state types to Global and Owned clarifies the intended usage and lifecycle of contract state.

Dependencies and Interactions

  1. AluVM: The AluVM variants in StateConvertor, StateBuilder, and StateArithm are currently unimplemented and marked as 'reserved for the future'. This indicates a planned integration with the AluVM, but it's important to ensure that these placeholders don't introduce unexpected behavior or dependencies.
  2. StrictEncoding/StrictTypes: The changes rely heavily on strict_encoding and strict_types for serialization and type definitions. Any changes in these dependencies could impact the API's stability.
  3. Ultrasonic: The refactoring is tightly coupled with the ultrasonic crate, particularly its data structures for representing contract state and operations. Reviewers should pay close attention to how these interactions are affected.

Risk Considerations

  1. Breaking Changes: The refactoring introduces several breaking changes, particularly in the API structure and naming conventions. A comprehensive migration guide is essential to minimize disruption for existing users.
  2. Signature Validation: The signature validation logic relies on external validator functions. It's crucial to ensure that these functions are thoroughly tested and secure to prevent vulnerabilities.
  3. AluVM Integration: The unimplemented AluVM variants could introduce unexpected behavior or dependencies if not handled carefully. Consider using feature flags to disable these variants until they are fully implemented.
  4. Performance: The changes to state management and aggregation could potentially impact performance. Thorough benchmarking is recommended to identify and address any performance regressions.

Notable Implementation Details

  1. ApisChecksum: The ApisChecksum is explicitly marked as non-unique and not for verification. This is a critical detail that should be carefully validated to prevent misuse.
  2. SemanticError: The SemanticError enum provides a structured way to handle errors related to contract semantics. Reviewers should ensure that all relevant error conditions are covered by this enum.
  3. State Conversion and Building: The typed_convert and typed_build functions in api/src/state/adaptors.rs are complex and require careful attention to detail to ensure correct state handling.

Comment thread api/src/api.rs
Comment on lines 23 to +24

//! API defines how a contract can be interfaced by software.
//! API defines how software can interface a contract.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread api/src/api.rs
Comment on lines +82 to +91
/// 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]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread api/src/api.rs
Comment on lines +251 to +309
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))?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread api/src/api.rs
Comment on lines +197 to +273
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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread api/src/api.rs
Comment on lines +195 to +283
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 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread api/src/articles.rs
Comment on lines 106 to +126
#[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.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread api/src/articles.rs
Comment on lines 157 to 171
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() }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread api/src/builders.rs
@dr-orlovsky dr-orlovsky merged commit 509c3a9 into master Jun 1, 2025
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant