From c1583c2e5821ee726afcaa3ca6d5396f5514988f Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Sat, 15 Aug 2026 14:30:35 +0100 Subject: [PATCH 1/7] Make latest_round accept one or many data ids The DON transmitter reads every feed's latest state each round to gate on deviation and heartbeat; single-feed reads would cost one consensus call per feed. latest_round now takes a Vec of ids and returns an Option per id. Page sizing is the caller's concern; the host's own budget and footprint limits bound oversized calls either way. The cache no longer cares about freeze on any read path: it holds state and serves it raw, exposing only the is_frozen flag. The proxy decides: every consumer read is prefixed with an is_frozen check that fails with the same FeedFrozen error as before. The check costs no extra ledger reads since every reader loads the feed_state entry holding the flag anyway. Freezing blocks consumer reads, never DON writes, so the transmitter keeps reading and writing frozen feeds. Self-upgrade wasm fixtures are rebuilt. --- .../data-feeds-cache/src/contract.rs | 27 ++---- .../data-feeds-cache/src/interface/reader.rs | 5 +- .../data-feeds-cache/src/tests/admin.rs | 84 +++++++----------- .../data-feeds-cache/src/tests/contract.rs | 4 +- .../data-feeds-cache/src/tests/harness.rs | 10 ++- .../data-feeds-cache/src/tests/reader.rs | 56 +++++++++++- .../data-feeds-cache/src/tests/writer.rs | 28 +++--- .../test_fixtures/cache_self_upgrade.wasm | Bin 23134 -> 22943 bytes .../data-feeds-proxy/src/contract.rs | 18 +++- .../test_fixtures/proxy_self_upgrade.wasm | Bin 9503 -> 10089 bytes 10 files changed, 135 insertions(+), 97 deletions(-) diff --git a/contracts/data-feeds/data-feeds-cache/src/contract.rs b/contracts/data-feeds/data-feeds-cache/src/contract.rs index e19edcc3..158a061e 100644 --- a/contracts/data-feeds/data-feeds-cache/src/contract.rs +++ b/contracts/data-feeds/data-feeds-cache/src/contract.rs @@ -30,14 +30,15 @@ impl DataFeedsCache { #[contractimpl] impl DataFeedsCacheReader for DataFeedsCache { - fn latest_round(env: Env, data_id: BytesN<16>) -> Result, CacheError> { - let Some(state) = feed::feed_state(&env, &data_id) else { - return Ok(None); - }; - if state.frozen { - return Err(CacheError::FeedFrozen); + fn latest_round( + env: Env, + data_ids: Vec>, + ) -> Result>, CacheError> { + let mut rounds = Vec::new(&env); + for data_id in data_ids.iter() { + rounds.push_back(feed::feed_state(&env, &data_id).map(|state| state.latest_round)); } - Ok(Some(state.latest_round)) + Ok(rounds) } fn get_round( @@ -45,7 +46,6 @@ impl DataFeedsCacheReader for DataFeedsCache { data_id: BytesN<16>, round_id: u64, ) -> Result, CacheError> { - reject_frozen(&env, &data_id)?; Ok(feed::round(&env, &data_id, round_id)) } @@ -55,7 +55,6 @@ impl DataFeedsCacheReader for DataFeedsCache { from: u64, to: u64, ) -> Result, CacheError> { - reject_frozen(&env, &data_id)?; Ok(feed::range(&env, &data_id, from, to)) } @@ -65,17 +64,14 @@ impl DataFeedsCacheReader for DataFeedsCache { timestamp: u64, bound: Bound, ) -> Result, CacheError> { - reject_frozen(&env, &data_id)?; Ok(feed::find_round(&env, &data_id, timestamp, bound)) } fn decimals(env: Env, data_id: BytesN<16>) -> Result, CacheError> { - reject_frozen(&env, &data_id)?; Ok(feed::decimals(&env, &data_id)) } fn description(env: Env, data_id: BytesN<16>) -> Result, CacheError> { - reject_frozen(&env, &data_id)?; Ok(feed::description(&env, &data_id)) } @@ -88,13 +84,6 @@ impl DataFeedsCacheReader for DataFeedsCache { } } -fn reject_frozen(env: &Env, data_id: &DataId) -> Result<(), CacheError> { - if feed::is_frozen(env, data_id) { - return Err(CacheError::FeedFrozen); - } - Ok(()) -} - #[contractimpl] impl DataFeedsCacheWriter for DataFeedsCache { fn on_report( diff --git a/contracts/data-feeds/data-feeds-cache/src/interface/reader.rs b/contracts/data-feeds/data-feeds-cache/src/interface/reader.rs index 6e483fff..b4f6f406 100644 --- a/contracts/data-feeds/data-feeds-cache/src/interface/reader.rs +++ b/contracts/data-feeds/data-feeds-cache/src/interface/reader.rs @@ -5,7 +5,10 @@ use crate::interface::CacheError; #[contractclient(name = "DataFeedsCacheReaderClient")] pub trait DataFeedsCacheReader { - fn latest_round(env: Env, data_id: BytesN<16>) -> Result, CacheError>; + fn latest_round( + env: Env, + data_ids: Vec>, + ) -> Result>, CacheError>; fn get_round( env: Env, diff --git a/contracts/data-feeds/data-feeds-cache/src/tests/admin.rs b/contracts/data-feeds/data-feeds-cache/src/tests/admin.rs index 7366a780..77712efa 100644 --- a/contracts/data-feeds/data-feeds-cache/src/tests/admin.rs +++ b/contracts/data-feeds/data-feeds-cache/src/tests/admin.rs @@ -319,7 +319,7 @@ mod set_feed_configs { cache.write(&feed, 100, 10); cache.write(&feed, 200, 20); cache.write(&feed, 300, 30); - assert_eq!(cache.latest_round(&feed).round_id, 3); + assert_eq!(cache.latest_round(&feed.id).unwrap().round_id, 3); assert_eq!(cache.history(&feed).len(), 3); cache.remove(&feed); @@ -334,7 +334,7 @@ mod set_feed_configs { ); assert_eq!( - cache.client().latest_round(&feed.id).unwrap().round_id, + cache.latest_round(&feed.id).unwrap().round_id, 3, "FeedState survives remove_feed_configs" ); @@ -352,7 +352,7 @@ mod set_feed_configs { "cache still serves version() after the upgrade" ); assert_eq!( - cache.client().latest_round(&feed.id).unwrap().round_id, + cache.latest_round(&feed.id).unwrap().round_id, 3, "FeedState survives the upgrade too" ); @@ -361,7 +361,7 @@ mod set_feed_configs { cache.write(&feed, 400, 40); assert_eq!( - cache.latest_round(&feed).round_id, + cache.latest_round(&feed.id).unwrap().round_id, 4, "round-id counter resumed from the resurrected FeedState, not reset" ); @@ -403,7 +403,7 @@ mod set_feed_configs { let wid = mock_wire_id(env, feed.tag, 0); cache.write(&feed, 111, 10); cache.write(&feed, 222, 20); - assert_eq!(cache.latest_round(&feed).round_id, 2); + assert_eq!(cache.latest_round(&feed.id).unwrap().round_id, 2); cache.remove(&feed); @@ -420,7 +420,7 @@ mod set_feed_configs { .client() .set_feed_configs(&feed.admin, &vec![env, entry]); - let inherited = cache.client().latest_round(&feed.id).unwrap(); + let inherited = cache.latest_round(&feed.id).unwrap(); assert_eq!(inherited.round_id, 2, "W2 inherits W1's counter position"); assert_eq!(inherited.answer, I256::from_i128(env, 222)); assert_eq!(inherited.timestamp, 20); @@ -436,7 +436,7 @@ mod set_feed_configs { let (md, rep) = report_as(env, &w2_owner, &w2_name, &wid, 999, 30); cache.client().on_report(&w2_sender, &md, &rep); - let head = cache.client().latest_round(&feed.id).unwrap(); + let head = cache.latest_round(&feed.id).unwrap(); assert_eq!(head.round_id, 3, "W2 round numbering resumes W1's sequence"); assert_eq!(head.answer, I256::from_i128(env, 999)); @@ -445,7 +445,7 @@ mod set_feed_configs { .client() .on_report(&feed.sender, &mock_metadata(env), &w1_report); assert_eq!( - cache.client().latest_round(&feed.id).unwrap().round_id, + cache.latest_round(&feed.id).unwrap().round_id, 3, "old W1 sender is not authorized under W2's config (skipped)" ); @@ -460,7 +460,7 @@ mod set_feed_configs { let env = &cache.env; cache.write(&feed, 100, 1_000_000); - assert_eq!(cache.latest_round(&feed).timestamp, 1_000_000); + assert_eq!(cache.latest_round(&feed.id).unwrap().timestamp, 1_000_000); cache.remove(&feed); cache.configure_feed(&feed.admin, &feed.sender, feed.tag, "BTC/USD"); @@ -472,19 +472,19 @@ mod set_feed_configs { stored_ts: 1_000_000, }); assert_eq!( - cache.latest_round(&feed).round_id, + cache.latest_round(&feed.id).unwrap().round_id, 1, "re-added feed's first (lower-ts) report was dropped as stale" ); assert_eq!( - cache.latest_round(&feed).answer, + cache.latest_round(&feed.id).unwrap().answer, I256::from_i128(env, 100), "head is still the pre-removal round" ); cache.write(&feed, 300, 1_000_001); - assert_eq!(cache.latest_round(&feed).round_id, 2); - assert_eq!(cache.latest_round(&feed).timestamp, 1_000_001); + assert_eq!(cache.latest_round(&feed.id).unwrap().round_id, 2); + assert_eq!(cache.latest_round(&feed.id).unwrap().timestamp, 1_000_001); } } @@ -883,41 +883,27 @@ mod set_feed_frozen { } #[test] - fn every_read_rejects_while_frozen() { + fn every_read_stays_raw_while_frozen() { let cache = Cache::deploy(); let feed = live_feed(&cache, 3); freeze(&cache, &feed, true); let c = cache.client(); assert!(c.is_frozen(&feed.id)); - assert!(matches!( - c.try_latest_round(&feed.id), - Err(Ok(CacheError::FeedFrozen)) - )); - assert!(matches!( - c.try_get_round(&feed.id, &1), - Err(Ok(CacheError::FeedFrozen)) - )); - assert!(matches!( - c.try_find_round(&feed.id, &10, &Bound::AtOrBefore), - Err(Ok(CacheError::FeedFrozen)) - )); - assert!(matches!( - c.try_round_range(&feed.id, &0, &u64::MAX), - Err(Ok(CacheError::FeedFrozen)) - )); - assert!(matches!( - c.try_decimals(&feed.id), - Err(Ok(CacheError::FeedFrozen)) - )); - assert!(matches!( - c.try_description(&feed.id), - Err(Ok(CacheError::FeedFrozen)) - )); + assert!(c.is_frozen(&feed.id)); + assert_eq!(cache.latest_round(&feed.id).unwrap().round_id, 3); + assert_eq!(cache.round(&feed, 1).unwrap().round_id, 1); + assert_eq!( + cache.find(&feed, 10, Bound::AtOrBefore).unwrap().round_id, + 1 + ); + assert_eq!(cache.history(&feed).len(), 3); + assert_eq!(c.decimals(&feed.id), Some(18)); + assert!(c.description(&feed.id).is_some()); assert!( c.is_configured(&feed.id), - "is_configured is the one read left open, so a frozen feed stays \ - distinguishable from an unknown one" + "freeze is consumer policy enforced at the proxy; the cache serves \ + raw data and only reports the flag" ); } @@ -930,7 +916,7 @@ mod set_feed_frozen { let c = cache.client(); assert!(!c.is_frozen(&feed.id)); - assert_eq!(cache.latest_round(&feed).round_id, 3); + assert_eq!(cache.latest_round(&feed.id).unwrap().round_id, 3); assert_eq!(cache.round(&feed, 1).unwrap().round_id, 1); assert_eq!( cache.find(&feed, 10, Bound::AtOrBefore).unwrap().round_id, @@ -957,7 +943,7 @@ mod set_feed_frozen { ); freeze(&cache, &feed, false); - let latest = cache.latest_round(&feed); + let latest = cache.latest_round(&feed.id).unwrap(); assert_eq!(latest.round_id, 2); assert_eq!(latest.answer, I256::from_i128(&cache.env, 500)); } @@ -972,10 +958,8 @@ mod set_feed_frozen { let c = cache.client(); assert!(!c.is_configured(&feed.id)); assert!(c.is_frozen(&feed.id)); - assert!(matches!( - c.try_latest_round(&feed.id), - Err(Ok(CacheError::FeedFrozen)) - )); + assert!(c.is_frozen(&feed.id)); + assert!(cache.latest_round(&feed.id).is_some()); } #[test] @@ -986,11 +970,9 @@ mod set_feed_frozen { cache.seed(&b, 1); freeze(&cache, &a, true); - assert!(matches!( - cache.client().try_latest_round(&a.id), - Err(Ok(CacheError::FeedFrozen)) - )); - assert_eq!(cache.latest_round(&b).round_id, 1); + assert!(cache.client().is_frozen(&a.id)); + assert!(cache.latest_round(&a.id).is_some()); + assert_eq!(cache.latest_round(&b.id).unwrap().round_id, 1); } #[test] diff --git a/contracts/data-feeds/data-feeds-cache/src/tests/contract.rs b/contracts/data-feeds/data-feeds-cache/src/tests/contract.rs index 620bc6df..143e19d2 100644 --- a/contracts/data-feeds/data-feeds-cache/src/tests/contract.rs +++ b/contracts/data-feeds/data-feeds-cache/src/tests/contract.rs @@ -150,7 +150,7 @@ mod lifecycle { cache.write(&feed, 123_456_789, big_ts); let history_before = cache.history(&feed); - let latest_before = cache.latest_round(&feed); + let latest_before = cache.latest_round(&feed.id).unwrap(); let decimals_before = cache.client().decimals(&feed.id); let description_before = cache.client().description(&feed.id); let perms_before = cache.client().get_feed_permissions(&feed.id); @@ -186,7 +186,7 @@ mod lifecycle { .expect("round lost across upgrade"); assert_same_round(&after, &before, "history round"); } - assert_same_round(&cache.latest_round(&feed), &latest_before, "latest_round"); + assert_same_round(&cache.latest_round(&feed.id).unwrap(), &latest_before, "latest_round"); let r2_before = history_before.iter().find(|r| r.round_id == 2).unwrap(); assert_same_round(&cache.round(&feed, 2).unwrap(), &r2_before, "get_round(2)"); diff --git a/contracts/data-feeds/data-feeds-cache/src/tests/harness.rs b/contracts/data-feeds/data-feeds-cache/src/tests/harness.rs index 44c4dab6..c5ae17fc 100644 --- a/contracts/data-feeds/data-feeds-cache/src/tests/harness.rs +++ b/contracts/data-feeds/data-feeds-cache/src/tests/harness.rs @@ -15,8 +15,8 @@ pub(crate) use crate::events::{ InvalidUpdatePermission, StaleReport, }; pub(crate) use crate::interface::types::{ - Bound, DataId, FeedConfig, ReportEntry, RoundData, WireDataId, WorkflowName, WorkflowOwner, - WorkflowPermission, + Bound, DataId, FeedConfig, ReportEntry, RoundData, WireDataId, WorkflowName, + WorkflowOwner, WorkflowPermission, }; pub(crate) use crate::interface::CacheError; pub(crate) use crate::interface::FeedConfigEntry; @@ -154,8 +154,10 @@ impl Cache { self.client().find_round(&feed.id, &ts, &bound) } - pub(crate) fn latest_round(&self, feed: &Feed) -> RoundData { - self.client().latest_round(&feed.id).unwrap() + pub(crate) fn latest_round(&self, id: &DataId) -> Option { + self.client() + .latest_round(&vec![&self.env, id.clone()]) + .get_unchecked(0) } pub(crate) fn expire_round(&self, feed: &Feed, round_id: u64) { diff --git a/contracts/data-feeds/data-feeds-cache/src/tests/reader.rs b/contracts/data-feeds/data-feeds-cache/src/tests/reader.rs index 74cf561a..14a6d797 100644 --- a/contracts/data-feeds/data-feeds-cache/src/tests/reader.rs +++ b/contracts/data-feeds/data-feeds-cache/src/tests/reader.rs @@ -40,7 +40,7 @@ mod latest_round { fn absent_is_none() { let cache = Cache::deploy(); let missing = mock_feed_id(&cache.env, 1); - assert!(cache.client().latest_round(&missing).is_none()); + assert!(cache.latest_round(&missing).is_none()); } #[test] @@ -48,7 +48,7 @@ mod latest_round { let cache = Cache::deploy(); let feed = cache.add_feed(1); cache.seed(&feed, 3); - let latest = cache.latest_round(&feed); + let latest = cache.latest_round(&feed.id).unwrap(); assert_eq!(latest.round_id, 3); assert_eq!(latest.answer, I256::from_i128(&cache.env, 300)); assert_eq!(latest.timestamp, 30); @@ -60,7 +60,7 @@ mod latest_round { let feed = cache.add_feed(1); cache.seed(&feed, 3); cache.expire_round(&feed, 3); - assert_eq!(cache.latest_round(&feed).round_id, 3); + assert_eq!(cache.latest_round(&feed.id).unwrap().round_id, 3); } } @@ -361,3 +361,53 @@ mod is_configured { assert!(c.description(&id).is_none()); } } + +mod latest_round_batches { + use super::*; + #[test] + fn mixed_batch_preserves_order() { + let cache = Cache::deploy(); + let written = cache.add_feed(1); + cache.seed(&written, 2); + let missing = mock_feed_id(&cache.env, 9); + let ids = vec![&cache.env, missing, written.id.clone()]; + + let rounds = cache.client().latest_round(&ids); + + assert_eq!(rounds.len(), 2); + assert!(rounds.get_unchecked(0).is_none()); + let latest = rounds.get_unchecked(1).unwrap(); + assert_eq!(latest.round_id, 2); + assert_eq!(latest.timestamp, 20); + } + + #[test] + fn frozen_feeds_read_normally() { + let cache = Cache::deploy(); + let feed = cache.add_feed(1); + cache.seed(&feed, 3); + cache + .client() + .set_feed_frozen(&feed.admin, &vec![&cache.env, feed.id.clone()], &true); + + assert!(cache.client().is_frozen(&feed.id)); + assert_eq!(cache.latest_round(&feed.id).unwrap().round_id, 3); + } + + #[test] + fn empty_ids_returns_empty() { + let cache = Cache::deploy(); + assert_eq!(cache.client().latest_round(&Vec::new(&cache.env)).len(), 0); + } + + #[test] + fn reads_large_batches() { + let cache = Cache::deploy(); + let mut ids = Vec::new(&cache.env); + for i in 0..90u32 { + ids.push_back(mock_feed_id(&cache.env, (i % 250) as u8)); + } + assert_eq!(cache.client().latest_round(&ids).len(), ids.len()); + } + +} diff --git a/contracts/data-feeds/data-feeds-cache/src/tests/writer.rs b/contracts/data-feeds/data-feeds-cache/src/tests/writer.rs index f2c9f4f0..0d6e81d5 100644 --- a/contracts/data-feeds/data-feeds-cache/src/tests/writer.rs +++ b/contracts/data-feeds/data-feeds-cache/src/tests/writer.rs @@ -335,7 +335,7 @@ mod on_report { stored_ts: 0, }); cache.write(&feed, 7, 1); - assert_eq!(cache.latest_round(&feed).round_id, 1); + assert_eq!(cache.latest_round(&feed.id).unwrap().round_id, 1); } #[test] @@ -376,7 +376,7 @@ mod on_report { 3, "the ts=12 accept lands after the stale ts=11 skip" ); - assert_eq!(cache.latest_round(&feed).timestamp, 12); + assert_eq!(cache.latest_round(&feed.id).unwrap().timestamp, 12); } #[test] @@ -395,7 +395,7 @@ mod on_report { assert_eq!(history.get(0).unwrap().round_id, 1); assert_eq!(history.get(1).unwrap().round_id, 2); assert_eq!(history.get(1).unwrap().answer, I256::from_i128(env, 200)); - assert_eq!(cache.latest_round(&feed).timestamp, 6); + assert_eq!(cache.latest_round(&feed.id).unwrap().timestamp, 6); } #[test] @@ -404,7 +404,7 @@ mod on_report { let feed = cache.add_feed(1); roll(&cache.env, 4242); cache.write(&feed, 100, 5); - let rd = cache.latest_round(&feed); + let rd = cache.latest_round(&feed.id).unwrap(); assert_eq!(rd.round_id, 1); assert_eq!(rd.answer, I256::from_i128(&cache.env, 100)); assert_eq!(rd.timestamp, 5); @@ -436,8 +436,8 @@ mod on_report { &md, &report(&cache.env, &[(mock_wire_id(&cache.env, 2, 0), 9, 5)]), ); - assert_eq!(c.latest_round(&a).unwrap().round_id, 2); - assert_eq!(c.latest_round(&b).unwrap().round_id, 1); + assert_eq!(cache.latest_round(&a).unwrap().round_id, 2); + assert_eq!(cache.latest_round(&b).unwrap().round_id, 1); } #[test] @@ -446,12 +446,12 @@ mod on_report { let feed = cache.add_feed(1); cache.write(&feed, i128::MAX, 5); assert_eq!( - cache.latest_round(&feed).answer, + cache.latest_round(&feed.id).unwrap().answer, I256::from_i128(&cache.env, i128::MAX) ); cache.write(&feed, i128::MIN, 6); assert_eq!( - cache.latest_round(&feed).answer, + cache.latest_round(&feed.id).unwrap().answer, I256::from_i128(&cache.env, i128::MIN) ); } @@ -477,7 +477,7 @@ mod on_report { .client() .on_report(&feed.sender, &mock_metadata(env), &report); - let got = cache.latest_round(&feed).answer; + let got = cache.latest_round(&feed.id).unwrap().answer; assert_eq!(got, big); assert!(got.to_i128().is_none(), "answer coerced into i128 range"); } @@ -504,7 +504,7 @@ mod on_report { ), ); assert_eq!(cache.history(&feed).len(), 3); - let latest = cache.latest_round(&feed); + let latest = cache.latest_round(&feed.id).unwrap(); assert_eq!(latest.round_id, 3, "skips do not advance the counter"); assert_eq!(latest.timestamp, 12); } @@ -554,11 +554,11 @@ mod on_report { primary: true, }); assert_eq!( - c.latest_round(&a).unwrap().round_id, + cache.latest_round(&a).unwrap().round_id, 2, "feed A's own counter, not a batch-global one" ); - let b_latest = c.latest_round(&b).unwrap(); + let b_latest = cache.latest_round(&b).unwrap(); assert_eq!( b_latest.round_id, 1, "feed B starts its own round sequence at 1" @@ -593,11 +593,11 @@ mod on_report { report_ts: 15, stored_ts: 20, }); - let b_latest = c.latest_round(&b).unwrap(); + let b_latest = cache.latest_round(&b).unwrap(); assert_eq!(b_latest.round_id, 1); assert_eq!(b_latest.answer, I256::from_i128(&cache.env, 222)); assert_eq!(b_latest.timestamp, 5); - let a_latest = c.latest_round(&a).unwrap(); + let a_latest = cache.latest_round(&a).unwrap(); assert_eq!( a_latest.round_id, 1, "A's stale entry skipped; no second round" diff --git a/contracts/data-feeds/data-feeds-cache/test_fixtures/cache_self_upgrade.wasm b/contracts/data-feeds/data-feeds-cache/test_fixtures/cache_self_upgrade.wasm index 7731a0ea5c2cf9948e0e0cad359ac1fcf918ac59..223f317e21e5015feafa4214c79f63a196bfa0d5 100644 GIT binary patch delta 3360 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 B^~wMM diff --git a/contracts/data-feeds/data-feeds-proxy/src/contract.rs b/contracts/data-feeds/data-feeds-proxy/src/contract.rs index 3ec8a6d9..05eaa0e3 100644 --- a/contracts/data-feeds/data-feeds-proxy/src/contract.rs +++ b/contracts/data-feeds/data-feeds-proxy/src/contract.rs @@ -1,6 +1,6 @@ -use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, String}; +use soroban_sdk::{contract, contractimpl, panic_with_error, vec, Address, BytesN, Env, String}; -use data_feeds_cache::DataFeedsCacheReaderClient; +use data_feeds_cache::{CacheError, DataFeedsCacheReaderClient}; use data_feeds_common::{TokenRecoverable, Upgradeable, Versioned}; use stellar_access::ownable::{self, enforce_owner_auth, Ownable}; @@ -20,12 +20,21 @@ impl DataFeedsProxy { } } +fn assert_not_frozen(env: &Env, data_id: &BytesN<16>) { + let cache = DataFeedsCacheReaderClient::new(env, &storage::get_cache(env)); + if cache.is_frozen(data_id) { + panic_with_error!(env, CacheError::FeedFrozen); + } +} + #[contractimpl] impl DataFeedsProxyReader for DataFeedsProxy { fn latest_round(env: Env, data_id: BytesN<16>) -> Result { storage::extend_ttl(&env); + assert_not_frozen(&env, &data_id); DataFeedsCacheReaderClient::new(&env, &storage::get_cache(&env)) - .latest_round(&data_id) + .latest_round(&vec![&env, data_id]) + .get_unchecked(0) .map(|r| Round { round_id: r.round_id, answer: r.answer, @@ -36,6 +45,7 @@ impl DataFeedsProxyReader for DataFeedsProxy { fn get_round(env: Env, data_id: BytesN<16>, round_id: u64) -> Result { storage::extend_ttl(&env); + assert_not_frozen(&env, &data_id); DataFeedsCacheReaderClient::new(&env, &storage::get_cache(&env)) .get_round(&data_id, &round_id) .map(|r| Round { @@ -48,6 +58,7 @@ impl DataFeedsProxyReader for DataFeedsProxy { fn decimals(env: Env, data_id: BytesN<16>) -> Result { storage::extend_ttl(&env); + assert_not_frozen(&env, &data_id); DataFeedsCacheReaderClient::new(&env, &storage::get_cache(&env)) .decimals(&data_id) .ok_or(ProxyReadError::NoDataPresent) @@ -55,6 +66,7 @@ impl DataFeedsProxyReader for DataFeedsProxy { fn description(env: Env, data_id: BytesN<16>) -> Result { storage::extend_ttl(&env); + assert_not_frozen(&env, &data_id); DataFeedsCacheReaderClient::new(&env, &storage::get_cache(&env)) .description(&data_id) .ok_or(ProxyReadError::NoDataPresent) diff --git a/contracts/data-feeds/data-feeds-proxy/test_fixtures/proxy_self_upgrade.wasm b/contracts/data-feeds/data-feeds-proxy/test_fixtures/proxy_self_upgrade.wasm index 5cedfbb14dbeaf2e0325884d550ac893eb2ea8d6..3ffbefcf8488933f6cd7643de1324c82b1a12c61 100644 GIT binary patch delta 3342 zcmb7GU2Igx6`r5FyVt&6d+ar^_$R)1ZQ3=)#sq?agw%J5jVOguq7pT2Wv_NGV0*o5 zcGsZf!Pk_Z@=`10qDHOqgA^*HsHs|^C_GfEQPuXLkxHYqm8vA-Awq3hRj4mj8zuBR zbMF#Jt4hV#GxyAynRCAL%{TMP7yo?FXZhMZXN>Wu)wEbz;!D$<{xJLse~C@YrO3#p z4RpgXA7ef|r0@G|np2mf!ZN|`;}bj*t!*;d`JRu;=e_DD-9U-xSDsqk}0Yt<`MPI}d;dS%Wl z*VxXEs#k_*!Mksl$C_$bEK$f8j(cqP^P)5RxT-(1;7#Sr#VJg#(T-n8EG`_Y=8GO1 zPc&?9@R;aNeqs-9hCSwN+%kTlF9&%zfx7nD)xMRq1 z+i>Exu$g1H+z}H=W??u!CQUI;KXxp)a{I$qK7RlFoe`V6=B&$AzmiwoyZl5e>ccFW z;HudH)@HfOn@_K@VaB?0XO@?jZzk0Mv(_TM*K_vyoN?CXQAee%1k7q(uvxClhT-piHam^Tr4t3^p6XTm|1WcJlSX3x>UAhyY1)m#LvB zU)61M<>W*g;?g|17tO8nLJb6eGI}@u_RK1CThxXYPNBa=Uoo}8LbBLfL#nqYNH@oN z*8hkDLO~GPa~U>(xB)|ktKpy&OZz>x*pG0fGP(fGP>!ismySnj{N3+jd5XbZ1zFu5 zHKI17ViEwm*U5sdFmF)ByRfbe7%6u5LNrU`xEvsIr1OCB$DuKh@pXxz49C89;KavT zeoE;GTe?E!f@1s|K=(%cNOONwBH>j)z)6@B>n8R`@icR}AB(L>!KF(Xq!QBBW?+dsx_fOr~8%+!OA@+eN3#t?fXELQ$VbGpdhL zj10lc$#k&4dCYIOu$X2I*hedv)+Nz6*yFPVq$?arMa>DocQ4Cqn59Ws=EQ^f>Eq3F zclnvzTkJ5>;O=!QB5jRx9#c7r-puf;KAm7>(PZZOvXALgd09ez8 z1P)^O7lNWiLG)|`yJi8Y2&Tj;TFdN8-kQ|V9#HPs+ z?x;fmbvbp?#0tH2ns!2ZAvZX2qeXMW)H{M3oVZ~?XIXHf<;Z#yN-k8BzA$e zZeZ382_a@U?u&R?=-5~O{@QQ;_>1rNLZwkqA_PJu9k~M!=qhNg5$SK+>hE&mL@D~g zxw9{1Vu!S*P+~-Lb0Vn)0E|`Ep@}38MRY;n^s62miP+VQ+^>3Vxvv_If?%Z{KG@EHyErfN-Z;EQHl34Wd$YQKW7c6_~uwh8U+ z;BKm$-wwJvyZL+QufKMvb1fG^=GyT!uUWnUm&U*5J@ky6s8%agZL*?)wgdUn(MomR zD<1Y1D%Cofi_uueSIhau`teG2=2@?}CtoUg;ao2oZ=dwM;=xLNPo;cx=GbD@izXfo z$9-l^%rDfR35UscESlK5uY59Jnkl-)V%4kFwDov2mWsxns#NEWmMW(X=I8Z(GmY^l z78go01w5w@dDZ!untlaiY&IHCM)RM@f4!dH7ezGpACoEcPXjAOW8hNsgZOK%=*5)YV-@&DdPXGV_ delta 2834 zcmb7GTWnlM8J>UUoZYiNyY{RviL-0R=WLv2UB|I=vz;~p&#FlZr8LPyKq!*EvR7?v z$98RptK?%yNO?dZWg>x4t3-$@Q3a?~1myu$?nBj=G*}2C6_6+r7tyK`LP9D66uy6s z6PgMhkYoQd|IB~?<~#ZGC$D@w5%r}rB!r-PaYC-F(8>hyAAw)B;`TCG5fdKPsXoC4 zF`<$q5fhZ~eJo_cD}e=WB@-S_S>YwH5)=Lk?}dDxN;FHO2sTT*-QV$&P*kTOK~U|1 z>htbkipGU{_S@a$Kl|gyPbxY?b>XYNYCe;ZvXBkDoYyRJS=kg4c_GOr;kA&GLK6N- z9r%hwF%x?Unbp-b5hlXi`bsKqo=@ev{R~CP1G+vr*;rT}pa0xkV{z%J(+i?6m}*R( zK2x7v5(D8>V`*~n^uqG#`8hF^d$O@{QS5f>#rd;yQ(|v^w!Yk0S{_fg!_AA0$@$MV z7RQ(8XBu-$Vx)DkF^8LzjSob5D7OS>*~$9kQw{O(H)VVEpkDsMLSwu>H#H99C60I~ zdv@W;#rjl3d?b5U_fdOLZYx~vsqQ10=yEJ{mz|Zny4z~e*RF+u(5-G!C0dbVdTT}0 zr0%h=$bIdx9E(I%io+gcB>GgKEB20zTiR=MJPd{28u4XmZ>ddW>>tz^4cZ61dS`D$ zRT&j1qCQoPy-2cgr|z{^yj`^2-tq>h#s1SfpgP0jwqEG8$NbFh9UQknM(7=>+mbFt zq@$W_2}S6}F(5>#c5Zcb_3aB%SM4)?_k*vUTN5U)<2;2CnV<5`*Kvr z^1G5O>d>1?;SWiwM6PTTCYq^;UT94)T_OEoT5`Y@_bDAKZa`-&bvcrcp&+GI$&lVr z6WS}eR26riWG)~wr)ORQTuhhl*E`WOHKGD@oi)e;cAwg={SsnD)4k|iOK9E)$nI6O zYEczRy-)d4s(gW@T#*S9)-x5=D>zS1q(Vrg$NnTUlK(yzFC%579<%?*oWe*xojq9@ zL}ZP4;dQQAZPZMtnNTy@vCFP!+f_+-+jp}@4eE>?44$BZO@i(AWz|vM9A05d?|)e4 zVQJRCjM$;>x4#QUoQra9jP}@5x!x?N$cTC-S53YvrNlEw73*TAB9VhcFDmG=-i3-} zpNasj0@Mr~^bY{o_hB_@M0BVk-OGn!!-m%g|0v^aM2#5{D>a6G=p$abN!;!zhKn{$ zBlg$~Bpi$=#6mY>1ssJgsmQBc{P~UN|8?>AzuXCh)Ps6|gzO_ZAPyRtIzhmp5!DHI zobZU!=_=x}3x=dLdxoWFC>)k3D%RVeSr?c_i|=x%$@@YJ`(D$*M29h@;1Li6XW?Z@ ziibZoUGaGMXcn;pov8qT$5Ee*eFQ|kzNvC@9uL=_P6PN{8=r@fs@^v5cI$`X4nh`@~K=8lz((O8kdJ7i4Bwc0!Sr;G{IIIo~d=F7(!9vdVXGE0I0K$q`qcsG}s5!*I6lJRZ6 zo^m96gyn}=vhDu7)?HrTYl#k_XG{fn?m9Bh;k)f}zxdY|fA*`_ztar{M(Y_iNEOe_ z`~^2akN~dh2m(upEiIAx_svsfB)m{*Mw`ltNIdS5D(+tSnXTI zL$t+iZv9bDF$CO2jVFV5ok5er@U}-H{SVkp`)=#u;;PUhBZT-CWB_@^nznxX#kOjE z_v8Xjns9OnGJD_6*V~SgvW51|#ZF4SY=xA#6WRUkOSu=YV~fl7<@WyKW$cRBy$P9v pylvlWFVk(C>nPKkSZ}e%%1t*O?Z}gSrRT=UO)rKwPHiiA{{~wyB}D)L From 3adf29f240e2ea11f518e8b05d789084c35321ef Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Sat, 15 Aug 2026 14:31:21 +0100 Subject: [PATCH 2/7] Store the latest round directly in the proxy test mock The mock cache derived its latest round from an injected round list with its own max-by-round-id logic, a transcription of cache behavior that could silently drift from the real contract. Tests now inject the exact RoundData the cache would return for latest_round, and the freeze flag is injected explicitly for is_frozen, so the mock holds answers rather than reimplementing cache logic. --- .../data-feeds-proxy/src/tests/contract.rs | 53 ++++++++++++++++--- .../data-feeds-proxy/src/tests/harness.rs | 9 ++++ .../data-feeds-proxy/src/tests/mock_cache.rs | 35 ++++++++---- 3 files changed, 80 insertions(+), 17 deletions(-) diff --git a/contracts/data-feeds/data-feeds-proxy/src/tests/contract.rs b/contracts/data-feeds/data-feeds-proxy/src/tests/contract.rs index 63bbde16..fb407247 100644 --- a/contracts/data-feeds/data-feeds-proxy/src/tests/contract.rs +++ b/contracts/data-feeds/data-feeds-proxy/src/tests/contract.rs @@ -3,6 +3,7 @@ use super::harness::*; fn assert_read_extends_ttl(read: impl Fn(&Proxy)) { let p = Proxy::deploy(); p.inject(&[(1, 100, 5)]); + p.set_latest((1, 100, 5)); let full = network_max_ttl(&p.env); age_ttl(&p.env); assert!( @@ -23,7 +24,7 @@ mod constructor { #[test] fn constructor_stores_owner_and_routes_reads() { let p = Proxy::deploy(); - p.inject(&[(1, 100, 5)]); + p.set_latest((1, 100, 5)); assert_eq!(p.client().get_owner().unwrap(), p.owner); assert_eq!(p.latest_round().round_id, 1); } @@ -45,7 +46,7 @@ mod latest_round { #[test] fn latest_returns_newest() { let p = Proxy::deploy(); - p.inject(&[(5, 500, 50), (9, 900, 90)]); + p.set_latest((9, 900, 90)); let r = p.latest_round(); assert_eq!(r.round_id, 9); assert_eq!(r.answer, I256::from_i128(&p.env, 900)); @@ -72,12 +73,50 @@ mod latest_round { #[should_panic(expected = "Error(Contract, #100)")] fn cache_error_traps_the_read() { let p = Proxy::deploy(); - p.inject(&[(1, 1, 10)]); + p.set_latest((1, 1, 10)); p.fail_cache(); p.latest_round(); } } +mod frozen { + use super::*; + + #[test] + #[should_panic(expected = "Error(Contract, #109)")] + fn latest_round_rejects_a_frozen_feed() { + let p = Proxy::deploy(); + p.freeze(); + p.latest_round(); + } + + #[test] + #[should_panic(expected = "Error(Contract, #109)")] + fn get_round_rejects_a_frozen_feed() { + let p = Proxy::deploy(); + p.inject(&[(3, 300, 30)]); + p.freeze(); + + p.client().get_round(&p.data_id(), &3); + } + + #[test] + #[should_panic(expected = "Error(Contract, #109)")] + fn decimals_rejects_a_frozen_feed() { + let p = Proxy::deploy(); + p.freeze(); + p.client().decimals(&p.data_id()); + } + + #[test] + #[should_panic(expected = "Error(Contract, #109)")] + fn description_rejects_a_frozen_feed() { + let p = Proxy::deploy(); + p.freeze(); + p.client().description(&p.data_id()); + } +} + mod get_round { use super::*; @@ -168,12 +207,10 @@ mod set_cache { #[test] fn set_cache_swaps_routing_and_emits() { let p = Proxy::deploy(); - p.inject(&[(1, 111, 5)]); + p.set_latest((1, 111, 5)); let mock2 = p.env.register(MockCache, ()); - MockCacheClient::new(&p.env, &mock2).inject( - &p.data_id(), - &vec![&p.env, mock_round_data(&p.env, (7, 222, 9))], - ); + MockCacheClient::new(&p.env, &mock2) + .set_latest(&p.data_id(), &mock_round_data(&p.env, (7, 222, 9))); assert_eq!(p.latest_round().round_id, 1); p.client().set_cache(&mock2); p.assert_event(CacheSet { diff --git a/contracts/data-feeds/data-feeds-proxy/src/tests/harness.rs b/contracts/data-feeds/data-feeds-proxy/src/tests/harness.rs index c14735af..d9309073 100644 --- a/contracts/data-feeds/data-feeds-proxy/src/tests/harness.rs +++ b/contracts/data-feeds/data-feeds-proxy/src/tests/harness.rs @@ -65,6 +65,15 @@ impl Proxy { MockCacheClient::new(&self.env, &self.mock).set_err(&CacheError::MalformedReport); } + pub(crate) fn freeze(&self) { + MockCacheClient::new(&self.env, &self.mock).set_frozen(&self.data_id(), &true); + } + + pub(crate) fn set_latest(&self, row: (u64, i128, u64)) { + MockCacheClient::new(&self.env, &self.mock) + .set_latest(&self.data_id(), &mock_round_data(&self.env, row)); + } + pub(crate) fn data_id(&self) -> DataId { mock_feed_id(&self.env, DUMMY_MOCK_FEED_ID) } diff --git a/contracts/data-feeds/data-feeds-proxy/src/tests/mock_cache.rs b/contracts/data-feeds/data-feeds-proxy/src/tests/mock_cache.rs index 803d7d43..74b3ae33 100644 --- a/contracts/data-feeds/data-feeds-proxy/src/tests/mock_cache.rs +++ b/contracts/data-feeds/data-feeds-proxy/src/tests/mock_cache.rs @@ -10,6 +10,8 @@ pub(crate) struct MockCache; #[contracttype] enum MockKey { Rounds(DataId), + Latest(DataId), + Frozen(DataId), Err, } @@ -24,20 +26,32 @@ impl MockCache { pub fn set_err(env: Env, e: CacheError) { env.storage().instance().set(&MockKey::Err, &e); } + pub fn set_latest(env: Env, data_id: DataId, latest: RoundData) { + env.storage() + .instance() + .set(&MockKey::Latest(data_id), &latest); + } + pub fn set_frozen(env: Env, data_id: DataId, frozen: bool) { + env.storage() + .instance() + .set(&MockKey::Frozen(data_id), &frozen); + } } #[contractimpl] impl DataFeedsCacheReader for MockCache { - fn latest_round(env: Env, data_id: DataId) -> Result, CacheError> { + fn latest_round( + env: Env, + data_ids: Vec, + ) -> Result>, CacheError> { if let Some(e) = env.storage().instance().get(&MockKey::Err) { return Err(e); } - let rounds: Vec = env - .storage() - .instance() - .get(&MockKey::Rounds(data_id)) - .unwrap_or(Vec::new(&env)); - Ok(rounds.iter().max_by_key(|v| v.round_id)) + let mut out = Vec::new(&env); + for data_id in data_ids.iter() { + out.push_back(env.storage().instance().get(&MockKey::Latest(data_id))); + } + Ok(out) } fn get_round( env: Env, @@ -82,8 +96,11 @@ impl DataFeedsCacheReader for MockCache { fn is_configured(_env: Env, _data_id: DataId) -> Result { unimplemented!("MockCache simulates no config reads; add real logic before testing them") } - fn is_frozen(_env: Env, _data_id: DataId) -> bool { - unimplemented!("MockCache simulates no freeze state; add real logic before testing it") + fn is_frozen(env: Env, data_id: DataId) -> bool { + env.storage() + .instance() + .get(&MockKey::Frozen(data_id)) + .unwrap_or(false) } fn description(env: Env, _data_id: DataId) -> Result, CacheError> { if let Some(e) = env.storage().instance().get(&MockKey::Err) { From fc2f1520e990496b75270c83e5f1a2ae5e2e2af1 Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Mon, 17 Aug 2026 14:02:30 +0100 Subject: [PATCH 3/7] Added feed_id(s) param --- .../data-feeds-cache/src/contract.rs | 32 ++++-- .../data-feeds-cache/src/interface/reader.rs | 8 +- .../data-feeds-cache/src/tests/admin.rs | 41 ++++---- .../data-feeds-cache/src/tests/contract.rs | 14 ++- .../data-feeds-cache/src/tests/harness.rs | 28 +++++- .../data-feeds-cache/src/tests/reader.rs | 92 +++++++++++++++--- .../test_fixtures/cache_self_upgrade.wasm | Bin 22943 -> 23399 bytes .../data-feeds-proxy/src/contract.rs | 11 ++- .../data-feeds-proxy/src/tests/mock_cache.rs | 45 +++++---- .../test_fixtures/proxy_self_upgrade.wasm | Bin 10089 -> 10236 bytes 10 files changed, 196 insertions(+), 75 deletions(-) diff --git a/contracts/data-feeds/data-feeds-cache/src/contract.rs b/contracts/data-feeds/data-feeds-cache/src/contract.rs index 158a061e..146c543a 100644 --- a/contracts/data-feeds/data-feeds-cache/src/contract.rs +++ b/contracts/data-feeds/data-feeds-cache/src/contract.rs @@ -67,20 +67,36 @@ impl DataFeedsCacheReader for DataFeedsCache { Ok(feed::find_round(&env, &data_id, timestamp, bound)) } - fn decimals(env: Env, data_id: BytesN<16>) -> Result, CacheError> { - Ok(feed::decimals(&env, &data_id)) + fn decimals(env: Env, data_ids: Vec>) -> Result>, CacheError> { + let mut out = Vec::new(&env); + for data_id in data_ids.iter() { + out.push_back(feed::decimals(&env, &data_id)); + } + Ok(out) } - fn description(env: Env, data_id: BytesN<16>) -> Result, CacheError> { - Ok(feed::description(&env, &data_id)) + fn description(env: Env, data_ids: Vec>) -> Result>, CacheError> { + let mut out = Vec::new(&env); + for data_id in data_ids.iter() { + out.push_back(feed::description(&env, &data_id)); + } + Ok(out) } - fn is_configured(env: Env, data_id: BytesN<16>) -> Result { - Ok(feed::configured(&env, &data_id)) + fn is_configured(env: Env, data_ids: Vec>) -> Result, CacheError> { + let mut out = Vec::new(&env); + for data_id in data_ids.iter() { + out.push_back(feed::configured(&env, &data_id)); + } + Ok(out) } - fn is_frozen(env: Env, data_id: BytesN<16>) -> bool { - feed::is_frozen(&env, &data_id) + fn is_frozen(env: Env, data_ids: Vec>) -> Vec { + let mut out = Vec::new(&env); + for data_id in data_ids.iter() { + out.push_back(feed::is_frozen(&env, &data_id)); + } + out } } diff --git a/contracts/data-feeds/data-feeds-cache/src/interface/reader.rs b/contracts/data-feeds/data-feeds-cache/src/interface/reader.rs index b4f6f406..a861326c 100644 --- a/contracts/data-feeds/data-feeds-cache/src/interface/reader.rs +++ b/contracts/data-feeds/data-feeds-cache/src/interface/reader.rs @@ -30,11 +30,11 @@ pub trait DataFeedsCacheReader { bound: Bound, ) -> Result, CacheError>; - fn decimals(env: Env, data_id: BytesN<16>) -> Result, CacheError>; + fn decimals(env: Env, data_ids: Vec>) -> Result>, CacheError>; - fn description(env: Env, data_id: BytesN<16>) -> Result, CacheError>; + fn description(env: Env, data_ids: Vec>) -> Result>, CacheError>; - fn is_configured(env: Env, data_id: BytesN<16>) -> Result; + fn is_configured(env: Env, data_ids: Vec>) -> Result, CacheError>; - fn is_frozen(env: Env, data_id: BytesN<16>) -> bool; + fn is_frozen(env: Env, data_ids: Vec>) -> Vec; } diff --git a/contracts/data-feeds/data-feeds-cache/src/tests/admin.rs b/contracts/data-feeds/data-feeds-cache/src/tests/admin.rs index 77712efa..141ab0fe 100644 --- a/contracts/data-feeds/data-feeds-cache/src/tests/admin.rs +++ b/contracts/data-feeds/data-feeds-cache/src/tests/admin.rs @@ -328,7 +328,7 @@ mod set_feed_configs { "permissions must be cleared by remove" ); assert_eq!( - cache.client().description(&feed.id), + cache.description(&feed.id), None, "config must be gone after remove" ); @@ -527,7 +527,7 @@ mod remove_feed_configs { &mock_wf_name(&cache.env) )); assert_eq!(c.get_feed_permissions(&feed.id).len(), 0); - assert_eq!(c.description(&feed.id), None); + assert_eq!(cache.description(&feed.id), None); } #[test] @@ -551,7 +551,7 @@ mod remove_feed_configs { )); assert_eq!(c.get_feed_permissions(&feed.id).len(), 1); assert_eq!( - c.description(&feed.id), + cache.description(&feed.id), Some(String::from_str(&cache.env, "BTC/USD")) ); } @@ -576,7 +576,7 @@ mod remove_feed_configs { )); assert_eq!(c.get_feed_permissions(&feed.id).len(), 1); assert_eq!( - c.description(&feed.id), + cache.description(&feed.id), Some(String::from_str(env, "BTC/USD")) ); } @@ -889,8 +889,8 @@ mod set_feed_frozen { freeze(&cache, &feed, true); let c = cache.client(); - assert!(c.is_frozen(&feed.id)); - assert!(c.is_frozen(&feed.id)); + assert!(cache.is_frozen(&feed.id)); + assert!(cache.is_frozen(&feed.id)); assert_eq!(cache.latest_round(&feed.id).unwrap().round_id, 3); assert_eq!(cache.round(&feed, 1).unwrap().round_id, 1); assert_eq!( @@ -898,10 +898,10 @@ mod set_feed_frozen { 1 ); assert_eq!(cache.history(&feed).len(), 3); - assert_eq!(c.decimals(&feed.id), Some(18)); - assert!(c.description(&feed.id).is_some()); + assert_eq!(cache.decimals(&feed.id), Some(18)); + assert!(cache.description(&feed.id).is_some()); assert!( - c.is_configured(&feed.id), + cache.is_configured(&feed.id), "freeze is consumer policy enforced at the proxy; the cache serves \ raw data and only reports the flag" ); @@ -915,7 +915,7 @@ mod set_feed_frozen { freeze(&cache, &feed, false); let c = cache.client(); - assert!(!c.is_frozen(&feed.id)); + assert!(!cache.is_frozen(&feed.id)); assert_eq!(cache.latest_round(&feed.id).unwrap().round_id, 3); assert_eq!(cache.round(&feed, 1).unwrap().round_id, 1); assert_eq!( @@ -923,9 +923,9 @@ mod set_feed_frozen { 1 ); assert_eq!(cache.history(&feed).len(), 3); - assert_eq!(c.decimals(&feed.id), Some(18)); + assert_eq!(cache.decimals(&feed.id), Some(18)); assert_eq!( - c.description(&feed.id), + cache.description(&feed.id), Some(String::from_str(&cache.env, "BTC/USD")) ); } @@ -937,10 +937,7 @@ mod set_feed_frozen { freeze(&cache, &feed, true); cache.write(&feed, 500, 99); - assert!( - cache.client().is_frozen(&feed.id), - "a report must not thaw the feed" - ); + assert!(cache.is_frozen(&feed.id), "a report must not thaw the feed"); freeze(&cache, &feed, false); let latest = cache.latest_round(&feed.id).unwrap(); @@ -956,9 +953,9 @@ mod set_feed_frozen { cache.remove(&feed); let c = cache.client(); - assert!(!c.is_configured(&feed.id)); - assert!(c.is_frozen(&feed.id)); - assert!(c.is_frozen(&feed.id)); + assert!(!cache.is_configured(&feed.id)); + assert!(cache.is_frozen(&feed.id)); + assert!(cache.is_frozen(&feed.id)); assert!(cache.latest_round(&feed.id).is_some()); } @@ -970,7 +967,7 @@ mod set_feed_frozen { cache.seed(&b, 1); freeze(&cache, &a, true); - assert!(cache.client().is_frozen(&a.id)); + assert!(cache.is_frozen(&a.id)); assert!(cache.latest_round(&a.id).is_some()); assert_eq!(cache.latest_round(&b.id).unwrap().round_id, 1); } @@ -1009,7 +1006,7 @@ mod set_feed_frozen { ), Err(Ok(CacheError::NoFeedState)) ); - assert!(!c.is_frozen(&feed.id), "the valid id must not survive"); + assert!(!cache.is_frozen(&feed.id), "the valid id must not survive"); } } @@ -1026,7 +1023,7 @@ mod set_feed_frozen { ), Err(Ok(CacheError::DuplicateFeedConfig)) ); - assert!(!c.is_frozen(&feed.id)); + assert!(!cache.is_frozen(&feed.id)); } #[test] diff --git a/contracts/data-feeds/data-feeds-cache/src/tests/contract.rs b/contracts/data-feeds/data-feeds-cache/src/tests/contract.rs index 143e19d2..653624f1 100644 --- a/contracts/data-feeds/data-feeds-cache/src/tests/contract.rs +++ b/contracts/data-feeds/data-feeds-cache/src/tests/contract.rs @@ -151,8 +151,8 @@ mod lifecycle { let history_before = cache.history(&feed); let latest_before = cache.latest_round(&feed.id).unwrap(); - let decimals_before = cache.client().decimals(&feed.id); - let description_before = cache.client().description(&feed.id); + let decimals_before = cache.decimals(&feed.id); + let description_before = cache.description(&feed.id); let perms_before = cache.client().get_feed_permissions(&feed.id); assert_eq!( @@ -186,18 +186,22 @@ mod lifecycle { .expect("round lost across upgrade"); assert_same_round(&after, &before, "history round"); } - assert_same_round(&cache.latest_round(&feed.id).unwrap(), &latest_before, "latest_round"); + assert_same_round( + &cache.latest_round(&feed.id).unwrap(), + &latest_before, + "latest_round", + ); let r2_before = history_before.iter().find(|r| r.round_id == 2).unwrap(); assert_same_round(&cache.round(&feed, 2).unwrap(), &r2_before, "get_round(2)"); assert_eq!( - cache.client().decimals(&feed.id), + cache.decimals(&feed.id), decimals_before, "decimals lost across upgrade" ); assert_eq!( - cache.client().description(&feed.id), + cache.description(&feed.id), description_before, "description lost across upgrade" ); diff --git a/contracts/data-feeds/data-feeds-cache/src/tests/harness.rs b/contracts/data-feeds/data-feeds-cache/src/tests/harness.rs index c5ae17fc..c3cf9311 100644 --- a/contracts/data-feeds/data-feeds-cache/src/tests/harness.rs +++ b/contracts/data-feeds/data-feeds-cache/src/tests/harness.rs @@ -15,8 +15,8 @@ pub(crate) use crate::events::{ InvalidUpdatePermission, StaleReport, }; pub(crate) use crate::interface::types::{ - Bound, DataId, FeedConfig, ReportEntry, RoundData, WireDataId, WorkflowName, - WorkflowOwner, WorkflowPermission, + Bound, DataId, FeedConfig, ReportEntry, RoundData, WireDataId, WorkflowName, WorkflowOwner, + WorkflowPermission, }; pub(crate) use crate::interface::CacheError; pub(crate) use crate::interface::FeedConfigEntry; @@ -160,6 +160,30 @@ impl Cache { .get_unchecked(0) } + pub(crate) fn decimals(&self, id: &DataId) -> Option { + self.client() + .decimals(&vec![&self.env, id.clone()]) + .get_unchecked(0) + } + + pub(crate) fn description(&self, id: &DataId) -> Option { + self.client() + .description(&vec![&self.env, id.clone()]) + .get_unchecked(0) + } + + pub(crate) fn is_configured(&self, id: &DataId) -> bool { + self.client() + .is_configured(&vec![&self.env, id.clone()]) + .get_unchecked(0) + } + + pub(crate) fn is_frozen(&self, id: &DataId) -> bool { + self.client() + .is_frozen(&vec![&self.env, id.clone()]) + .get_unchecked(0) + } + pub(crate) fn expire_round(&self, feed: &Feed, round_id: u64) { let did = feed.id.clone(); self.env.as_contract(&self.id, || { diff --git a/contracts/data-feeds/data-feeds-cache/src/tests/reader.rs b/contracts/data-feeds/data-feeds-cache/src/tests/reader.rs index 14a6d797..6e71b823 100644 --- a/contracts/data-feeds/data-feeds-cache/src/tests/reader.rs +++ b/contracts/data-feeds/data-feeds-cache/src/tests/reader.rs @@ -298,14 +298,14 @@ mod decimals { fn derived_from_id_byte7() { let cache = Cache::deploy(); let feed = cache.add_feed(1); - assert_eq!(cache.client().decimals(&feed.id), Some(18)); + assert_eq!(cache.decimals(&feed.id), Some(18)); } #[test] fn unconfigured_feed_is_none() { let cache = Cache::deploy(); assert_eq!( - cache.client().decimals(&mock_feed_id(&cache.env, 123)), + cache.decimals(&mock_feed_id(&cache.env, 123)), None, "a derivable id is still None until it is configured" ); @@ -320,7 +320,7 @@ mod description { let cache = Cache::deploy(); let feed = cache.add_feed(1); assert_eq!( - cache.client().description(&feed.id), + cache.description(&feed.id), Some(String::from_str(&cache.env, "BTC/USD")) ); } @@ -329,7 +329,7 @@ mod description { fn unconfigured_feed_is_none() { let cache = Cache::deploy(); let unknown = mock_feed_id(&cache.env, 99); - assert_eq!(cache.client().description(&unknown), None); + assert_eq!(cache.description(&unknown), None); } } @@ -340,8 +340,8 @@ mod is_configured { fn tracks_the_feed_config() { let cache = Cache::deploy(); let feed = cache.add_feed(1); - assert!(cache.client().is_configured(&feed.id)); - assert!(!cache.client().is_configured(&mock_feed_id(&cache.env, 99))); + assert!(cache.is_configured(&feed.id)); + assert!(!cache.is_configured(&mock_feed_id(&cache.env, 99))); } #[test] @@ -351,14 +351,79 @@ mod is_configured { let sender = new_address(&cache.env); let id = cache.configure_feed(&admin, &sender, 2, ""); let c = cache.client(); - assert!(c.is_configured(&id)); - assert!(c.decimals(&id).is_some()); - assert!(c.description(&id).is_some()); + assert!(cache.is_configured(&id)); + assert!(cache.decimals(&id).is_some()); + assert!(cache.description(&id).is_some()); c.remove_feed_configs(&admin, &vec![&cache.env, id.clone()]); - assert!(!c.is_configured(&id)); - assert!(c.decimals(&id).is_none()); - assert!(c.description(&id).is_none()); + assert!(!cache.is_configured(&id)); + assert!(cache.decimals(&id).is_none()); + assert!(cache.description(&id).is_none()); + } +} + +mod metadata_batches { + use super::*; + + #[test] + fn mixed_batch_preserves_order() { + let cache = Cache::deploy(); + let feed = cache.add_feed(1); + let missing = mock_feed_id(&cache.env, 9); + let ids = vec![&cache.env, missing, feed.id.clone()]; + let c = cache.client(); + + let decimals = c.decimals(&ids); + assert_eq!(decimals.len(), 2); + assert_eq!(decimals.get_unchecked(0), None); + assert_eq!(decimals.get_unchecked(1), Some(18)); + + let descriptions = c.description(&ids); + assert_eq!(descriptions.len(), 2); + assert_eq!(descriptions.get_unchecked(0), None); + assert_eq!( + descriptions.get_unchecked(1), + Some(String::from_str(&cache.env, "BTC/USD")) + ); + + let configured = c.is_configured(&ids); + assert_eq!(configured.len(), 2); + assert!(!configured.get_unchecked(0)); + assert!(configured.get_unchecked(1)); + } + + #[test] + fn is_frozen_mixed_batch_preserves_order() { + let cache = Cache::deploy(); + let frozen = cache.add_feed(1); + let thawed = cache.add_feed(2); + cache.seed(&frozen, 1); + cache + .client() + .set_feed_frozen(&frozen.admin, &vec![&cache.env, frozen.id.clone()], &true); + + let flags = cache.client().is_frozen(&vec![ + &cache.env, + thawed.id.clone(), + frozen.id.clone(), + mock_feed_id(&cache.env, 9), + ]); + + assert_eq!(flags.len(), 3); + assert!(!flags.get_unchecked(0)); + assert!(flags.get_unchecked(1)); + assert!(!flags.get_unchecked(2), "unknown feeds are not frozen"); + } + + #[test] + fn empty_ids_returns_empty() { + let cache = Cache::deploy(); + let c = cache.client(); + let none: Vec = Vec::new(&cache.env); + assert_eq!(c.decimals(&none).len(), 0); + assert_eq!(c.description(&none).len(), 0); + assert_eq!(c.is_configured(&none).len(), 0); + assert_eq!(c.is_frozen(&none).len(), 0); } } @@ -390,7 +455,7 @@ mod latest_round_batches { .client() .set_feed_frozen(&feed.admin, &vec![&cache.env, feed.id.clone()], &true); - assert!(cache.client().is_frozen(&feed.id)); + assert!(cache.is_frozen(&feed.id)); assert_eq!(cache.latest_round(&feed.id).unwrap().round_id, 3); } @@ -409,5 +474,4 @@ mod latest_round_batches { } assert_eq!(cache.client().latest_round(&ids).len(), ids.len()); } - } diff --git a/contracts/data-feeds/data-feeds-cache/test_fixtures/cache_self_upgrade.wasm b/contracts/data-feeds/data-feeds-cache/test_fixtures/cache_self_upgrade.wasm index 223f317e21e5015feafa4214c79f63a196bfa0d5..ffc713a78de5877c94258730f0a3aefe79932268 100644 GIT binary patch delta 766 zcmc)IJxc>Y5C-6#*-H>El2ro6XmFBU~PVYje>;% zVIj8ZL~N{W#7gi-sI!-l4-2ugx?%Tr_Tk;RH1OVryH|;~zIl^JiJ%P#0UqW=wOG%> zydbDz35mc%N|DfCaIBxij#E+b43bdMjO1TL3C9RXlqDY^sp)(TW3s|K>tt4_l5XWBW5XmpjzU<_P$L{a^0bi|SnDPAr5OvJb= z+7KjbGvKDR5#VwayB;*(k> z&VlzdpWz#4SS|0k&9leiZqLF_fX&gg(vNfSU=CK} gk!G!yH`jU}xR`|xliygkW!{`_hHE)9zI8GA4fN}OjQ{`u delta 390 zcmaF9jdA{F#trwGnOf2}KV)tcsgGb}s#j)k6v$R$RAEqHRA3M=Qef0%W^iH*VsT)q z1F<=j7zE5*SsWbyH5_2jWnhqUV&Y-s2CB4zi>!50Xmes|;Z|UD6v$Fx;OA!G_JpaXP_0UO9QjLn-v) { let cache = DataFeedsCacheReaderClient::new(env, &storage::get_cache(env)); - if cache.is_frozen(data_id) { + if cache + .is_frozen(&vec![env, data_id.clone()]) + .get_unchecked(0) + { panic_with_error!(env, CacheError::FeedFrozen); } } @@ -60,7 +63,8 @@ impl DataFeedsProxyReader for DataFeedsProxy { storage::extend_ttl(&env); assert_not_frozen(&env, &data_id); DataFeedsCacheReaderClient::new(&env, &storage::get_cache(&env)) - .decimals(&data_id) + .decimals(&vec![&env, data_id]) + .get_unchecked(0) .ok_or(ProxyReadError::NoDataPresent) } @@ -68,7 +72,8 @@ impl DataFeedsProxyReader for DataFeedsProxy { storage::extend_ttl(&env); assert_not_frozen(&env, &data_id); DataFeedsCacheReaderClient::new(&env, &storage::get_cache(&env)) - .description(&data_id) + .description(&vec![&env, data_id]) + .get_unchecked(0) .ok_or(ProxyReadError::NoDataPresent) } } diff --git a/contracts/data-feeds/data-feeds-proxy/src/tests/mock_cache.rs b/contracts/data-feeds/data-feeds-proxy/src/tests/mock_cache.rs index 74b3ae33..96a4131f 100644 --- a/contracts/data-feeds/data-feeds-proxy/src/tests/mock_cache.rs +++ b/contracts/data-feeds/data-feeds-proxy/src/tests/mock_cache.rs @@ -40,10 +40,7 @@ impl MockCache { #[contractimpl] impl DataFeedsCacheReader for MockCache { - fn latest_round( - env: Env, - data_ids: Vec, - ) -> Result>, CacheError> { + fn latest_round(env: Env, data_ids: Vec) -> Result>, CacheError> { if let Some(e) = env.storage().instance().get(&MockKey::Err) { return Err(e); } @@ -84,29 +81,43 @@ impl DataFeedsCacheReader for MockCache { ) -> Result, CacheError> { unimplemented!("MockCache simulates no search reads; add real logic before testing them") } - fn decimals(env: Env, data_id: DataId) -> Result, CacheError> { + fn decimals(env: Env, data_ids: Vec) -> Result>, CacheError> { if let Some(e) = env.storage().instance().get(&MockKey::Err) { return Err(e); } - Ok(Some(match data_id.to_array()[7] { - b @ 0x20..=0x60 => (b - 0x20) as u32, - _ => 0, - })) + let mut out = Vec::new(&env); + for data_id in data_ids.iter() { + out.push_back(Some(match data_id.to_array()[7] { + b @ 0x20..=0x60 => (b - 0x20) as u32, + _ => 0, + })); + } + Ok(out) } - fn is_configured(_env: Env, _data_id: DataId) -> Result { + fn is_configured(_env: Env, _data_ids: Vec) -> Result, CacheError> { unimplemented!("MockCache simulates no config reads; add real logic before testing them") } - fn is_frozen(env: Env, data_id: DataId) -> bool { - env.storage() - .instance() - .get(&MockKey::Frozen(data_id)) - .unwrap_or(false) + fn is_frozen(env: Env, data_ids: Vec) -> Vec { + let mut out = Vec::new(&env); + for data_id in data_ids.iter() { + out.push_back( + env.storage() + .instance() + .get(&MockKey::Frozen(data_id)) + .unwrap_or(false), + ); + } + out } - fn description(env: Env, _data_id: DataId) -> Result, CacheError> { + fn description(env: Env, data_ids: Vec) -> Result>, CacheError> { if let Some(e) = env.storage().instance().get(&MockKey::Err) { return Err(e); } - Ok(Some(String::from_str(&env, "MOCK"))) + let mut out = Vec::new(&env); + for _ in data_ids.iter() { + out.push_back(Some(String::from_str(&env, "MOCK"))); + } + Ok(out) } } diff --git a/contracts/data-feeds/data-feeds-proxy/test_fixtures/proxy_self_upgrade.wasm b/contracts/data-feeds/data-feeds-proxy/test_fixtures/proxy_self_upgrade.wasm index 3ffbefcf8488933f6cd7643de1324c82b1a12c61..1860fe67640cea9b3e4f3252dfee6969f10e6a36 100644 GIT binary patch delta 533 zcmaLT&ntvM7zgm@nVEfed3WtBDXn7O*B<1@hSFZmTXGRma-v)<uh`zCp$de5(tz%)6KY*_q;+KThu7K+(?dt28Vgf5qv}m2NY8gM`(1| zXaa173UWN=m7ZUZzaIBKvcFFycftF{qE~?_F~Vv~fhVLe`@AQ(;H#`LlBQ33J&=~k zTEilmR$yeRFECaH?dq>hnYs9I0Tru}M2@%BNkAlUEFux?56P;LJ}#?ox93&(OA>X` zbOODi@B1efO(a5;sAZ+-fUQerGB#sVa=akIyh2kurw3*u-kE&b zz6NbenqGxGa~AI&53v-&|36M8$X*ZX4(Mb9EnV`jp&uYkbhVfP6^u2UaumUPd=oKh u@=X&MF;`88ww1OuP=t&koK5!A-EMM&c9eOv%(6VDI+3biG zgcsR=z)J`55_sznor0u;mu_WE8$pL41|E-j^WKknZ(GmVN^xQjmjUqrR{-z?Qz{;} zYF#8l;qH;U-2$NndOiy_&&P2Nc+AoS3P@9s#9}lfn57FMz+yI9WIYI`D(h)#kj&Q+ znk+x{>+cU2gC&8sk+YG{a_V;Ig7->~$S8}dn?(s+a}V`Z)V*tc2iKbT!wR4ETLtix znuzn%khsnKj9ZLP3&!I-rd*b$5qHB7$&%wZ4c!7+9<+Kdw>Lfbg~ypBU+qkOU`ev= zAaR?=xval~_iXNbBx9LpkeH$zoZi<&m5?GUw{ekh@85q_#-jptFThU+SAgu7~K2ji8g EPq)8f_5c6? From 3d094b2d108cc2503189402caf14237d98e68faa Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Mon, 17 Aug 2026 14:09:48 +0100 Subject: [PATCH 4/7] Test duplicate and missing ids in batch reads --- .../data-feeds-cache/src/tests/reader.rs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/contracts/data-feeds/data-feeds-cache/src/tests/reader.rs b/contracts/data-feeds/data-feeds-cache/src/tests/reader.rs index 6e71b823..8d90a847 100644 --- a/contracts/data-feeds/data-feeds-cache/src/tests/reader.rs +++ b/contracts/data-feeds/data-feeds-cache/src/tests/reader.rs @@ -392,6 +392,47 @@ mod metadata_batches { assert!(configured.get_unchecked(1)); } + #[test] + fn duplicates_and_missing_ids_keep_input_shape() { + let cache = Cache::deploy(); + let feed = cache.add_feed(1); + cache.seed(&feed, 2); + let missing = mock_feed_id(&cache.env, 9); + let ids = vec![&cache.env, feed.id.clone(), missing, feed.id.clone()]; + let c = cache.client(); + + let rounds = c.latest_round(&ids); + assert_eq!(rounds.len(), ids.len()); + assert_eq!(rounds.get_unchecked(0).unwrap().round_id, 2); + assert!(rounds.get_unchecked(1).is_none()); + assert_eq!(rounds.get_unchecked(2).unwrap().round_id, 2); + + let decimals = c.decimals(&ids); + assert_eq!(decimals.len(), ids.len()); + assert_eq!(decimals.get_unchecked(0), Some(18)); + assert_eq!(decimals.get_unchecked(1), None); + assert_eq!(decimals.get_unchecked(2), Some(18)); + + let expected_desc = Some(String::from_str(&cache.env, "BTC/USD")); + let descriptions = c.description(&ids); + assert_eq!(descriptions.len(), ids.len()); + assert_eq!(descriptions.get_unchecked(0), expected_desc); + assert_eq!(descriptions.get_unchecked(1), None); + assert_eq!(descriptions.get_unchecked(2), expected_desc); + + let configured = c.is_configured(&ids); + assert_eq!(configured.len(), ids.len()); + assert!(configured.get_unchecked(0)); + assert!(!configured.get_unchecked(1)); + assert!(configured.get_unchecked(2)); + + let frozen = c.is_frozen(&ids); + assert_eq!(frozen.len(), ids.len()); + assert!(!frozen.get_unchecked(0)); + assert!(!frozen.get_unchecked(1)); + assert!(!frozen.get_unchecked(2)); + } + #[test] fn is_frozen_mixed_batch_preserves_order() { let cache = Cache::deploy(); From 3c74431e265a0412d0416708b3a6bebc221efee6 Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Mon, 17 Aug 2026 14:41:28 +0100 Subject: [PATCH 5/7] Group batch reader tests by function --- .../data-feeds-cache/src/tests/reader.rs | 232 +++++++++--------- 1 file changed, 111 insertions(+), 121 deletions(-) diff --git a/contracts/data-feeds/data-feeds-cache/src/tests/reader.rs b/contracts/data-feeds/data-feeds-cache/src/tests/reader.rs index 8d90a847..d9d40fa7 100644 --- a/contracts/data-feeds/data-feeds-cache/src/tests/reader.rs +++ b/contracts/data-feeds/data-feeds-cache/src/tests/reader.rs @@ -62,6 +62,51 @@ mod latest_round { cache.expire_round(&feed, 3); assert_eq!(cache.latest_round(&feed.id).unwrap().round_id, 3); } + + #[test] + fn batch_preserves_order_and_handles_duplicates_and_missing() { + let cache = Cache::deploy(); + let feed = cache.add_feed(1); + cache.seed(&feed, 2); + let missing = mock_feed_id(&cache.env, 9); + let ids = vec![&cache.env, feed.id.clone(), missing, feed.id.clone()]; + + let rounds = cache.client().latest_round(&ids); + + assert_eq!(rounds.len(), ids.len()); + assert_eq!(rounds.get_unchecked(0).unwrap().round_id, 2); + assert!(rounds.get_unchecked(1).is_none()); + assert_eq!(rounds.get_unchecked(2).unwrap().round_id, 2); + } + + #[test] + fn frozen_feeds_read_normally() { + let cache = Cache::deploy(); + let feed = cache.add_feed(1); + cache.seed(&feed, 3); + cache + .client() + .set_feed_frozen(&feed.admin, &vec![&cache.env, feed.id.clone()], &true); + + assert!(cache.is_frozen(&feed.id)); + assert_eq!(cache.latest_round(&feed.id).unwrap().round_id, 3); + } + + #[test] + fn empty_ids_returns_empty() { + let cache = Cache::deploy(); + assert_eq!(cache.client().latest_round(&Vec::new(&cache.env)).len(), 0); + } + + #[test] + fn reads_large_batches() { + let cache = Cache::deploy(); + let mut ids = Vec::new(&cache.env); + for i in 0..90u32 { + ids.push_back(mock_feed_id(&cache.env, (i % 250) as u8)); + } + assert_eq!(cache.client().latest_round(&ids).len(), ids.len()); + } } mod get_round { @@ -310,6 +355,27 @@ mod decimals { "a derivable id is still None until it is configured" ); } + + #[test] + fn batch_preserves_order_and_handles_duplicates_and_missing() { + let cache = Cache::deploy(); + let feed = cache.add_feed(1); + let missing = mock_feed_id(&cache.env, 9); + let ids = vec![&cache.env, feed.id.clone(), missing, feed.id.clone()]; + + let decimals = cache.client().decimals(&ids); + + assert_eq!(decimals.len(), ids.len()); + assert_eq!(decimals.get_unchecked(0), Some(18)); + assert_eq!(decimals.get_unchecked(1), None); + assert_eq!(decimals.get_unchecked(2), Some(18)); + } + + #[test] + fn empty_ids_returns_empty() { + let cache = Cache::deploy(); + assert_eq!(cache.client().decimals(&Vec::new(&cache.env)).len(), 0); + } } mod description { @@ -331,6 +397,28 @@ mod description { let unknown = mock_feed_id(&cache.env, 99); assert_eq!(cache.description(&unknown), None); } + + #[test] + fn batch_preserves_order_and_handles_duplicates_and_missing() { + let cache = Cache::deploy(); + let feed = cache.add_feed(1); + let missing = mock_feed_id(&cache.env, 9); + let ids = vec![&cache.env, feed.id.clone(), missing, feed.id.clone()]; + let expected = Some(String::from_str(&cache.env, "BTC/USD")); + + let descriptions = cache.client().description(&ids); + + assert_eq!(descriptions.len(), ids.len()); + assert_eq!(descriptions.get_unchecked(0), expected); + assert_eq!(descriptions.get_unchecked(1), None); + assert_eq!(descriptions.get_unchecked(2), expected); + } + + #[test] + fn empty_ids_returns_empty() { + let cache = Cache::deploy(); + assert_eq!(cache.client().description(&Vec::new(&cache.env)).len(), 0); + } } mod is_configured { @@ -360,81 +448,34 @@ mod is_configured { assert!(cache.decimals(&id).is_none()); assert!(cache.description(&id).is_none()); } -} - -mod metadata_batches { - use super::*; - - #[test] - fn mixed_batch_preserves_order() { - let cache = Cache::deploy(); - let feed = cache.add_feed(1); - let missing = mock_feed_id(&cache.env, 9); - let ids = vec![&cache.env, missing, feed.id.clone()]; - let c = cache.client(); - - let decimals = c.decimals(&ids); - assert_eq!(decimals.len(), 2); - assert_eq!(decimals.get_unchecked(0), None); - assert_eq!(decimals.get_unchecked(1), Some(18)); - - let descriptions = c.description(&ids); - assert_eq!(descriptions.len(), 2); - assert_eq!(descriptions.get_unchecked(0), None); - assert_eq!( - descriptions.get_unchecked(1), - Some(String::from_str(&cache.env, "BTC/USD")) - ); - - let configured = c.is_configured(&ids); - assert_eq!(configured.len(), 2); - assert!(!configured.get_unchecked(0)); - assert!(configured.get_unchecked(1)); - } #[test] - fn duplicates_and_missing_ids_keep_input_shape() { + fn batch_preserves_order_and_handles_duplicates_and_missing() { let cache = Cache::deploy(); let feed = cache.add_feed(1); - cache.seed(&feed, 2); let missing = mock_feed_id(&cache.env, 9); let ids = vec![&cache.env, feed.id.clone(), missing, feed.id.clone()]; - let c = cache.client(); - - let rounds = c.latest_round(&ids); - assert_eq!(rounds.len(), ids.len()); - assert_eq!(rounds.get_unchecked(0).unwrap().round_id, 2); - assert!(rounds.get_unchecked(1).is_none()); - assert_eq!(rounds.get_unchecked(2).unwrap().round_id, 2); - - let decimals = c.decimals(&ids); - assert_eq!(decimals.len(), ids.len()); - assert_eq!(decimals.get_unchecked(0), Some(18)); - assert_eq!(decimals.get_unchecked(1), None); - assert_eq!(decimals.get_unchecked(2), Some(18)); - let expected_desc = Some(String::from_str(&cache.env, "BTC/USD")); - let descriptions = c.description(&ids); - assert_eq!(descriptions.len(), ids.len()); - assert_eq!(descriptions.get_unchecked(0), expected_desc); - assert_eq!(descriptions.get_unchecked(1), None); - assert_eq!(descriptions.get_unchecked(2), expected_desc); + let configured = cache.client().is_configured(&ids); - let configured = c.is_configured(&ids); assert_eq!(configured.len(), ids.len()); assert!(configured.get_unchecked(0)); assert!(!configured.get_unchecked(1)); assert!(configured.get_unchecked(2)); + } - let frozen = c.is_frozen(&ids); - assert_eq!(frozen.len(), ids.len()); - assert!(!frozen.get_unchecked(0)); - assert!(!frozen.get_unchecked(1)); - assert!(!frozen.get_unchecked(2)); + #[test] + fn empty_ids_returns_empty() { + let cache = Cache::deploy(); + assert_eq!(cache.client().is_configured(&Vec::new(&cache.env)).len(), 0); } +} + +mod is_frozen { + use super::*; #[test] - fn is_frozen_mixed_batch_preserves_order() { + fn batch_preserves_order_and_handles_duplicates_and_missing() { let cache = Cache::deploy(); let frozen = cache.add_feed(1); let thawed = cache.add_feed(2); @@ -442,77 +483,26 @@ mod metadata_batches { cache .client() .set_feed_frozen(&frozen.admin, &vec![&cache.env, frozen.id.clone()], &true); - - let flags = cache.client().is_frozen(&vec![ + let ids = vec![ &cache.env, - thawed.id.clone(), frozen.id.clone(), + thawed.id.clone(), mock_feed_id(&cache.env, 9), - ]); - - assert_eq!(flags.len(), 3); - assert!(!flags.get_unchecked(0)); - assert!(flags.get_unchecked(1)); - assert!(!flags.get_unchecked(2), "unknown feeds are not frozen"); - } - - #[test] - fn empty_ids_returns_empty() { - let cache = Cache::deploy(); - let c = cache.client(); - let none: Vec = Vec::new(&cache.env); - assert_eq!(c.decimals(&none).len(), 0); - assert_eq!(c.description(&none).len(), 0); - assert_eq!(c.is_configured(&none).len(), 0); - assert_eq!(c.is_frozen(&none).len(), 0); - } -} - -mod latest_round_batches { - use super::*; - #[test] - fn mixed_batch_preserves_order() { - let cache = Cache::deploy(); - let written = cache.add_feed(1); - cache.seed(&written, 2); - let missing = mock_feed_id(&cache.env, 9); - let ids = vec![&cache.env, missing, written.id.clone()]; - - let rounds = cache.client().latest_round(&ids); - - assert_eq!(rounds.len(), 2); - assert!(rounds.get_unchecked(0).is_none()); - let latest = rounds.get_unchecked(1).unwrap(); - assert_eq!(latest.round_id, 2); - assert_eq!(latest.timestamp, 20); - } + frozen.id.clone(), + ]; - #[test] - fn frozen_feeds_read_normally() { - let cache = Cache::deploy(); - let feed = cache.add_feed(1); - cache.seed(&feed, 3); - cache - .client() - .set_feed_frozen(&feed.admin, &vec![&cache.env, feed.id.clone()], &true); + let flags = cache.client().is_frozen(&ids); - assert!(cache.is_frozen(&feed.id)); - assert_eq!(cache.latest_round(&feed.id).unwrap().round_id, 3); + assert_eq!(flags.len(), ids.len()); + assert!(flags.get_unchecked(0)); + assert!(!flags.get_unchecked(1)); + assert!(!flags.get_unchecked(2), "unknown feeds are not frozen"); + assert!(flags.get_unchecked(3)); } #[test] fn empty_ids_returns_empty() { let cache = Cache::deploy(); - assert_eq!(cache.client().latest_round(&Vec::new(&cache.env)).len(), 0); - } - - #[test] - fn reads_large_batches() { - let cache = Cache::deploy(); - let mut ids = Vec::new(&cache.env); - for i in 0..90u32 { - ids.push_back(mock_feed_id(&cache.env, (i % 250) as u8)); - } - assert_eq!(cache.client().latest_round(&ids).len(), ids.len()); + assert_eq!(cache.client().is_frozen(&Vec::new(&cache.env)).len(), 0); } } From 0a35a193deb473f4cba68774eb6fe53ec0bc0376 Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Mon, 17 Aug 2026 17:58:49 +0100 Subject: [PATCH 6/7] Cover frozen reads for decimals and description --- .../data-feeds-cache/src/tests/reader.rs | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/contracts/data-feeds/data-feeds-cache/src/tests/reader.rs b/contracts/data-feeds/data-feeds-cache/src/tests/reader.rs index d9d40fa7..9a1e5e9b 100644 --- a/contracts/data-feeds/data-feeds-cache/src/tests/reader.rs +++ b/contracts/data-feeds/data-feeds-cache/src/tests/reader.rs @@ -97,16 +97,6 @@ mod latest_round { let cache = Cache::deploy(); assert_eq!(cache.client().latest_round(&Vec::new(&cache.env)).len(), 0); } - - #[test] - fn reads_large_batches() { - let cache = Cache::deploy(); - let mut ids = Vec::new(&cache.env); - for i in 0..90u32 { - ids.push_back(mock_feed_id(&cache.env, (i % 250) as u8)); - } - assert_eq!(cache.client().latest_round(&ids).len(), ids.len()); - } } mod get_round { @@ -376,6 +366,19 @@ mod decimals { let cache = Cache::deploy(); assert_eq!(cache.client().decimals(&Vec::new(&cache.env)).len(), 0); } + + #[test] + fn frozen_feeds_read_normally() { + let cache = Cache::deploy(); + let feed = cache.add_feed(1); + cache.seed(&feed, 1); + cache + .client() + .set_feed_frozen(&feed.admin, &vec![&cache.env, feed.id.clone()], &true); + + assert!(cache.is_frozen(&feed.id)); + assert_eq!(cache.decimals(&feed.id), Some(18)); + } } mod description { @@ -419,6 +422,22 @@ mod description { let cache = Cache::deploy(); assert_eq!(cache.client().description(&Vec::new(&cache.env)).len(), 0); } + + #[test] + fn frozen_feeds_read_normally() { + let cache = Cache::deploy(); + let feed = cache.add_feed(1); + cache.seed(&feed, 1); + cache + .client() + .set_feed_frozen(&feed.admin, &vec![&cache.env, feed.id.clone()], &true); + + assert!(cache.is_frozen(&feed.id)); + assert_eq!( + cache.description(&feed.id), + Some(String::from_str(&cache.env, "BTC/USD")) + ); + } } mod is_configured { From 7ecf55f7e7bcac5755e6e450d36a661afcc1377c Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Mon, 17 Aug 2026 18:44:31 +0100 Subject: [PATCH 7/7] Scope frozen and metadata tests by function --- .../data-feeds-proxy/src/tests/contract.rs | 56 +++++++++---------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/contracts/data-feeds/data-feeds-proxy/src/tests/contract.rs b/contracts/data-feeds/data-feeds-proxy/src/tests/contract.rs index fb407247..62f25b4b 100644 --- a/contracts/data-feeds/data-feeds-proxy/src/tests/contract.rs +++ b/contracts/data-feeds/data-feeds-proxy/src/tests/contract.rs @@ -77,10 +77,6 @@ mod latest_round { p.fail_cache(); p.latest_round(); } -} - -mod frozen { - use super::*; #[test] #[should_panic(expected = "Error(Contract, #109)")] @@ -89,32 +85,6 @@ mod frozen { p.freeze(); p.latest_round(); } - - #[test] - #[should_panic(expected = "Error(Contract, #109)")] - fn get_round_rejects_a_frozen_feed() { - let p = Proxy::deploy(); - p.inject(&[(3, 300, 30)]); - p.freeze(); - - p.client().get_round(&p.data_id(), &3); - } - - #[test] - #[should_panic(expected = "Error(Contract, #109)")] - fn decimals_rejects_a_frozen_feed() { - let p = Proxy::deploy(); - p.freeze(); - p.client().decimals(&p.data_id()); - } - - #[test] - #[should_panic(expected = "Error(Contract, #109)")] - fn description_rejects_a_frozen_feed() { - let p = Proxy::deploy(); - p.freeze(); - p.client().description(&p.data_id()); - } } mod get_round { @@ -146,6 +116,16 @@ mod get_round { p.client().get_round(&p.data_id(), &1u64); }); } + + #[test] + #[should_panic(expected = "Error(Contract, #109)")] + fn get_round_rejects_a_frozen_feed() { + let p = Proxy::deploy(); + p.inject(&[(3, 300, 30)]); + p.freeze(); + + p.client().get_round(&p.data_id(), &3); + } } mod decimals { @@ -170,6 +150,14 @@ mod decimals { p.client().decimals(&p.data_id()); }); } + + #[test] + #[should_panic(expected = "Error(Contract, #109)")] + fn decimals_rejects_a_frozen_feed() { + let p = Proxy::deploy(); + p.freeze(); + p.client().decimals(&p.data_id()); + } } mod description { @@ -190,6 +178,14 @@ mod description { p.client().description(&p.data_id()); }); } + + #[test] + #[should_panic(expected = "Error(Contract, #109)")] + fn description_rejects_a_frozen_feed() { + let p = Proxy::deploy(); + p.freeze(); + p.client().description(&p.data_id()); + } } mod set_cache {