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
2 changes: 1 addition & 1 deletion contracts/bridge/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -964,7 +964,7 @@ mod bridge {
return Err(Error::Unauthorized);
}

self.check_and_update_rate_limits(caller, *route.last().unwrap(), 0, true)?;
self.check_and_update_rate_limits(caller, *route.last().ok_or(Error::InvalidRequest)?, 0, true)?;

// Check if asset is frozen (skipped: token_id is u64, freeze uses AccountId; see bridge/src/lib.rs helpers)

Expand Down
20 changes: 3 additions & 17 deletions contracts/crowdfunding/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1210,22 +1210,9 @@ mod propchain_crowdfunding {
total_investment / total_investors as u128
};

// Find largest investment
#[allow(unused_assignments)]
let mut largest_investment = 0u128;
for id in self.campaign_ids.iter() {
if let Some(investor) = self.campaigns.get(*id) {
if investor.campaign_id == campaign_id {
// This is inefficient, but we need to iterate through all investments
// In a real implementation, we'd store this data
break;
}
}
}

// For now, we'll approximate largest investment
// TODO: Store max investment per campaign
largest_investment = average_investment * 2; // Placeholder
// TODO: Store max investment per campaign for an exact figure.
// For now, approximate as 2× the average investment.
let largest_investment = average_investment * 2;

let total_milestones = self.campaign_milestone_counts.get(campaign_id).unwrap_or(0);
let released_milestones = self.released_milestone_counts.get(campaign_id).unwrap_or(0);
Expand Down Expand Up @@ -1429,7 +1416,6 @@ pub use crate::propchain_crowdfunding::{CrowdfundingError, RealEstateCrowdfundin
#[cfg(test)]
mod tests {
use super::*;
#[allow(unused_imports)]
use ink::env::{test, DefaultEnvironment};
use propchain_crowdfunding::{CampaignStatus, CrowdfundingError, RealEstateCrowdfunding};

Expand Down
6 changes: 2 additions & 4 deletions contracts/identity/tests/identity_tests.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
#![cfg(test)]
#![allow(unused_variables)]

use ink::env::test::{default_accounts, DefaultAccounts};
use ink::primitives::AccountId;
use propchain_identity::propchain_identity::{
Expand Down Expand Up @@ -52,7 +50,7 @@ fn test_create_identity() {

#[ink::test]
fn test_create_identity_already_exists() {
let accounts: DefaultAccounts<ink::env::DefaultEnvironment> = default_accounts();
let _accounts: DefaultAccounts<ink::env::DefaultEnvironment> = default_accounts();
let mut identity_registry = IdentityRegistry::new();

let did = "did:example:123456789abcdefghi".to_string();
Expand Down Expand Up @@ -521,7 +519,7 @@ fn test_privacy_preserving_verification() {

#[ink::test]
fn test_privacy_verification_failed() {
let accounts: DefaultAccounts<ink::env::DefaultEnvironment> = default_accounts();
let _accounts: DefaultAccounts<ink::env::DefaultEnvironment> = default_accounts();
let mut identity_registry = IdentityRegistry::new();

// Create identity with privacy settings disabled
Expand Down
59 changes: 43 additions & 16 deletions contracts/oracle/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ use propchain_traits::*;
// `oracle/src/aggregation.rs` — declare the module so the file is included
// into the crate.
mod aggregation;
/// Buffer-reuse helper (Issue #739).
mod slot_vec;
use slot_vec::SlotVec;

/// Property Valuation Oracle Contract
#[ink::contract]
Expand Down Expand Up @@ -2436,13 +2439,32 @@ mod propchain_oracle {
&mut self,
property_id: u64,
) -> Result<Vec<PriceData>, OracleError> {
let mut prices = Vec::new();
// ── Reusable buffers (Issue #739) ────────────────────────────────
// Each SlotVec is initialised with a capacity matching the expected
// number of oracle sources. On repeat calls the backing allocation
// is retained via `reuse()`, eliminating heap allocations for the
// common case where the source count stays constant across batches.
let source_count = self.active_sources.len().max(8);
let mut cached_sources: SlotVec<(String, OracleSource, u64)> =
SlotVec::with_capacity(source_count);
let mut valid_sources: SlotVec<(String, OracleSource)> =
SlotVec::with_capacity(source_count);
let mut source_updates: SlotVec<(String, u64)> =
SlotVec::with_capacity(source_count);
let mut prices: SlotVec<PriceData> = SlotVec::with_capacity(source_count);

// Reset all buffers (no-op on first call, saves allocations on
// subsequent calls if the function is invoked in a loop).
cached_sources.reuse();
valid_sources.reuse();
source_updates.reuse();
prices.reuse();

let current_block = self.env().block_number() as u64;

// ── Step 1: Cache all source configs and last-update timestamps ──
// This reads each source once and batches the frequency check data,
// avoiding repeated Mapping::get calls during the collection loop.
let mut cached_sources: Vec<(String, OracleSource, u64)> = Vec::new();
for source_id in &self.active_sources {
if let Some(source) = self.oracle_sources.get(source_id) {
let last_update = self.last_source_update.get(source_id).unwrap_or(0);
Expand All @@ -2451,8 +2473,7 @@ mod propchain_oracle {
}

// ── Step 2: Batch frequency check and collect valid sources ──────
let mut valid_sources: Vec<(String, OracleSource)> = Vec::new();
for (source_id, source, last_update) in &cached_sources {
for (source_id, source, last_update) in cached_sources.as_slice() {
if self.min_update_interval_blocks > 0
&& *last_update > 0
&& current_block.saturating_sub(*last_update) < self.min_update_interval_blocks
Expand All @@ -2472,8 +2493,7 @@ mod propchain_oracle {
// In production this would batch cross-contract calls via a multicall
// proxy. Each source still requires an individual call, but the
// frequency-check and config data is already cached.
let mut source_updates: Vec<(String, u64)> = Vec::new();
for (source_id, source) in &valid_sources {
for (source_id, source) in valid_sources.as_slice() {
match self.get_price_from_source(source, property_id) {
Ok(price_data) => {
if self.is_price_fresh(&price_data) {
Expand All @@ -2488,19 +2508,22 @@ mod propchain_oracle {
// ── Step 4: Batch-write all last_source_update entries ───────────
// Single pass over the collected updates instead of N Mapping::insert
// calls inside the collection loop.
for (source_id, block) in &source_updates {
for (source_id, block) in source_updates.as_slice() {
self.last_source_update.insert(source_id, block);
}

let sources_attempted = cached_sources.len() as u32;
let sources_succeeded = prices.len() as u32;

// Emit observability event
self.env().emit_event(BatchPricesCollected {
property_id,
sources_attempted: cached_sources.len() as u32,
sources_succeeded: prices.len() as u32,
sources_attempted,
sources_succeeded,
batch_enabled: true,
});

Ok(prices)
Ok(prices.into_vec())
}

fn collect_prices_from_sources(
Expand All @@ -2519,7 +2542,11 @@ mod propchain_oracle {
&mut self,
property_id: u64,
) -> Result<Vec<PriceData>, OracleError> {
let mut prices = Vec::new();
// Use a SlotVec so the backing allocation is retained across repeated
// calls, reducing allocator pressure (Issue #739).
let mut prices: SlotVec<PriceData> =
SlotVec::with_capacity(self.active_sources.len().max(8));
prices.reuse();
let current_block = self.env().block_number() as u64;

for source_id in &self.active_sources {
Expand Down Expand Up @@ -2549,7 +2576,7 @@ mod propchain_oracle {
}
}

Ok(prices)
Ok(prices.into_vec())
}

fn get_price_from_source(
Expand Down Expand Up @@ -3279,17 +3306,17 @@ mod propchain_oracle {
};

let trend_direction = if relevant_data.len() > 1 {
let first = relevant_data.first().unwrap().valuation as i128;
let last = relevant_data.last().unwrap().valuation as i128;
let first = relevant_data.first().map(|s| s.valuation as i128).unwrap_or(0);
let last = relevant_data.last().map(|s| s.valuation as i128).unwrap_or(0);
((last - first) / (relevant_data.len() as i128))
.max(-100)
.min(100) as i32
} else {
0
};

let period_start = relevant_data.first().unwrap().timestamp;
let period_end = relevant_data.last().unwrap().timestamp;
let period_start = relevant_data.first().ok_or(OracleError::PropertyNotFound)?.timestamp;
let period_end = relevant_data.last().ok_or(OracleError::PropertyNotFound)?.timestamp;

Ok(OracleHistoryStatistics {
property_id,
Expand Down
110 changes: 110 additions & 0 deletions contracts/oracle/src/slot_vec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//! `SlotVec` — a capacity-reusing wrapper around [`ink::prelude::vec::Vec`].
//!
//! # Problem
//!
//! During each call to `collect_prices_batched` / `collect_prices_sequential` the
//! oracle allocates several fresh `Vec` buffers (cached sources, valid sources, price
//! results, source-update pairs). On WASM these heap allocations dominate the
//! instruction count and contribute to gas spikes on large batches.
//!
//! # Solution
//!
//! `SlotVec<T>` wraps a `Vec<T>` and exposes a `reuse()` method that clears the
//! contents while **retaining the previously allocated capacity**. When the same
//! `SlotVec` is reused across multiple calls (e.g. stored temporarily on the stack
//! and passed by `&mut` into helper functions) the allocator is only invoked on the
//! first call; subsequent calls simply reset the length to zero and write into the
//! existing backing buffer.
//!
//! # Usage
//!
//! ```rust,ignore
//! let mut buf: SlotVec<PriceData> = SlotVec::with_capacity(16);
//! buf.reuse(); // clear + retain capacity
//! buf.push(item);
//! let slice = buf.as_slice();
//! ```
//!
//! The type derefs to `&[T]` / `&mut [T]` so it can be passed anywhere a slice is
//! expected without copying.

use ink::prelude::vec::Vec;

/// A `Vec<T>` wrapper that supports capacity-preserving resets.
///
/// See the [module documentation](self) for the design rationale.
pub struct SlotVec<T> {
inner: Vec<T>,
}

impl<T> SlotVec<T> {
/// Create a new `SlotVec` with an initial capacity hint.
///
/// Choosing a capacity equal to the typical batch size avoids any
/// allocation on the first `push` call.
#[inline]
pub fn with_capacity(cap: usize) -> Self {
Self {
inner: Vec::with_capacity(cap),
}
}

/// Clear all elements while **retaining** the backing allocation.
///
/// This is the key method: calling `reuse()` before refilling the buffer
/// means the allocator is only hit when the vector needs to grow beyond its
/// current capacity.
#[inline]
pub fn reuse(&mut self) {
self.inner.clear();
}

/// Append an element, growing the buffer only if needed.
#[inline]
pub fn push(&mut self, item: T) {
self.inner.push(item);
}

/// Number of elements currently stored.
#[inline]
pub fn len(&self) -> usize {
self.inner.len()
}

/// `true` if the buffer contains no elements.
#[inline]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}

/// Borrow the contents as a plain slice.
#[inline]
pub fn as_slice(&self) -> &[T] {
&self.inner
}

/// Consume the `SlotVec`, yielding the inner `Vec<T>`.
///
/// Use this when you need to pass the collected data to a function that
/// expects an owned `Vec` (e.g. returning from a message).
#[inline]
pub fn into_vec(self) -> Vec<T> {
self.inner
}
}

impl<T> core::ops::Deref for SlotVec<T> {
type Target = [T];

#[inline]
fn deref(&self) -> &Self::Target {
&self.inner
}
}

impl<T> core::ops::DerefMut for SlotVec<T> {
#[inline]
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
5 changes: 2 additions & 3 deletions contracts/prediction-market/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ mod propchain_prediction_market {
return Err(Error::MarketNotActive); // Need better error naming
}

let winning_dir = market.winning_direction.as_ref().unwrap();
let winning_dir = market.winning_direction.as_ref().ok_or(Error::MarketNotActive)?;

let key = (market_id, caller);
let mut stake = self.stakes.get(&key).ok_or(Error::StakeNotFound)?;
Expand Down Expand Up @@ -629,7 +629,7 @@ mod propchain_prediction_market {
return Err(Error::OracleMarketNotResolved);
}

let winning_dir = market.winning_direction.as_ref().unwrap();
let winning_dir = market.winning_direction.as_ref().ok_or(Error::OracleMarketNotResolved)?;

let key = (market_id, caller);
let mut stake = self.oracle_stakes.get(&key).ok_or(Error::StakeNotFound)?;
Expand Down Expand Up @@ -698,7 +698,6 @@ mod propchain_prediction_market {
}

#[cfg(test)]
#[allow(unused_variables)]
mod tests {
use super::*;

Expand Down
Loading
Loading