Skip to main content

sc_statement_store/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
5
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with this program. If not, see <https://www.gnu.org/licenses/>.
18
19#![doc = include_str!("../docs/overview.md")]
20#![doc = include_str!("../docs/usage.md")]
21//! # Implementation notes
22//!
23//! This crate contains a disk-backed implementation of `sp_statement_store::StatementStore`.
24//!
25//! ## Constraint management
26//!
27//! The statement store validates statements using node-side signature verification and
28//! static runtime allowance limits.
29//! The following constraints are then checked:
30//! * For a given account id, there may be at most `max_count` statements with `max_size` total data
31//!   size. To satisfy this, statements for this account ID are removed from the store starting with
32//!   the lowest priority until a constraint is satisfied.
33//! * There may not be more than `MAX_TOTAL_STATEMENTS` total statements with `MAX_TOTAL_SIZE` size.
34//!   To satisfy this, statements are removed from the store starting with the lowest
35//!   `global_priority` until a constraint is satisfied.
36//!
37//! When a new statement is inserted that would not satisfy constraints in the first place, no
38//! statements are deleted and a `Rejected` result is returned.
39//! The order in which statements with the same priority are deleted is unspecified.
40//!
41//! ## Statement expiration
42//!
43//! Each time a statement is removed from the store (Either evicted by higher priority statement or
44//! explicitly with the `remove` function) the statement is marked as expired. Expired statements
45//! can't be added to the store for `Options::purge_after_sec` seconds. This is to prevent old
46//! statements from being propagated on the network.
47
48#![warn(missing_docs)]
49#![warn(unused_extern_crates)]
50
51mod metrics;
52mod subscription;
53
54#[cfg(feature = "test-helpers")]
55pub mod subxt_client;
56#[cfg(feature = "test-helpers")]
57pub mod test_utils;
58
59use crate::subscription::{SubscriptionStatementsStream, SubscriptionsHandle};
60use futures::FutureExt;
61use metrics::MetricsLink as PrometheusMetrics;
62use parking_lot::RwLock;
63use prometheus_endpoint::Registry as PrometheusRegistry;
64use sc_client_api::{backend::StorageProvider, Backend, StorageKey};
65use sc_keystore::LocalKeystore;
66use schnellru::{ByLength, LruMap};
67use sp_blockchain::HeaderBackend;
68use sp_core::{crypto::UncheckedFrom, hexdisplay::HexDisplay, traits::SpawnNamed, Decode, Encode};
69use sp_runtime::traits::Block as BlockT;
70use sp_statement_store::{
71	runtime_api::{StatementSource, StatementStoreExt},
72	AccountId, AdmittedBatch, BlockHash, Channel, DecryptionKey, FilterDecision, Hash,
73	InvalidReason, OptimizedTopicFilter, RejectionReason, Result, SignatureVerificationResult,
74	Statement, StatementAllowance, StatementEvent, SubmitResult, Topic,
75};
76pub use sp_statement_store::{Error, StatementStore, MAX_TOPICS};
77use std::{
78	collections::{BTreeMap, HashMap, HashSet},
79	sync::{
80		atomic::{AtomicUsize, Ordering as AtomicOrdering},
81		Arc, Weak,
82	},
83	time::{Duration, Instant},
84};
85pub use subscription::{
86	AddFilterError, MultiFilterEventStream, MultiFilterSubscriptionApi,
87	MultiFilterSubscriptionEvent, StatementStoreSubscriptionApi, SubscriptionHandle,
88	MAX_FILTERS_PER_SUBSCRIPTION,
89};
90use subscription::{ReplayBatch, ReplaySnapshotProvider, REPLAY_CHUNK_RAW_BYTES};
91
92const KEY_VERSION: &[u8] = b"version".as_slice();
93const CURRENT_VERSION: u32 = 2;
94
95/// Meta column key of the persisted global counters, SCALE-encoded as
96/// `(statement_count: u64, total_size: u64, next_seq: u64)`. The row is rewritten as part of
97/// every mutating commit (all of which happen under the submit-index write lock), so it is always
98/// consistent with the statement data and makes startup independent of the store size.
99const KEY_COUNTERS: &[u8] = b"counters".as_slice();
100
101const MIGRATION_COMMIT_CHUNK: usize = 100_000;
102
103/// A single raw database operation: writes `value` under `key` in `column`, or deletes the key
104/// when `value` is `None`.
105struct DbOperation {
106	column: u8,
107	key: Vec<u8>,
108	value: Option<Vec<u8>>,
109}
110
111impl From<(u8, Vec<u8>, Option<Vec<u8>>)> for DbOperation {
112	fn from((column, key, value): (u8, Vec<u8>, Option<Vec<u8>>)) -> Self {
113		Self { column, key, value }
114	}
115}
116
117struct MigrationBatch<'a> {
118	db: &'a parity_db::Db,
119	operations: Vec<DbOperation>,
120	committed: usize,
121}
122
123impl<'a> MigrationBatch<'a> {
124	fn new(db: &'a parity_db::Db) -> Self {
125		Self { db, operations: Vec::new(), committed: 0 }
126	}
127
128	fn push(&mut self, operation: DbOperation) -> Result<()> {
129		self.operations.push(operation);
130		self.flush_if_full()
131	}
132
133	fn extend(&mut self, operations: impl IntoIterator<Item = DbOperation>) -> Result<()> {
134		self.operations.extend(operations);
135		self.flush_if_full()
136	}
137
138	fn flush_if_full(&mut self) -> Result<()> {
139		if self.operations.len() >= MIGRATION_COMMIT_CHUNK {
140			self.flush()?;
141		}
142		Ok(())
143	}
144
145	fn flush(&mut self) -> Result<()> {
146		if self.operations.is_empty() {
147			return Ok(());
148		}
149		let operations = std::mem::take(&mut self.operations);
150		let count = operations.len();
151		self.db
152			.commit(
153				operations
154					.into_iter()
155					.map(|operation| (operation.column, operation.key, operation.value)),
156			)
157			.map_err(|error| Error::Db(error.to_string()))?;
158		self.committed += count;
159		Ok(())
160	}
161
162	fn finish(mut self) -> Result<usize> {
163		self.flush()?;
164		Ok(self.committed)
165	}
166}
167
168const LOG_TARGET: &str = "statement-store";
169
170/// The amount of time an expired statement is kept before it is removed from the store entirely.
171pub const DEFAULT_PURGE_AFTER_SEC: u64 = 2 * 24 * 60 * 60; // 48h
172/// The maximum number of statements the statement store can hold.
173pub const DEFAULT_MAX_TOTAL_STATEMENTS: usize = 4 * 1024 * 1024; // ~4 million
174/// The maximum amount of data the statement store can hold, regardless of the number of
175/// statements from which the data originates.
176pub const DEFAULT_MAX_TOTAL_SIZE: usize = 2 * 1024 * 1024 * 1024; // 2GiB
177/// The maximum size of a single statement in bytes.
178/// Accounts for the 1-byte vector length prefix when statements are gossiped as `Vec<Statement>`.
179pub const MAX_STATEMENT_SIZE: usize =
180	sc_network_statement::config::MAX_STATEMENT_NOTIFICATION_SIZE as usize - 1;
181
182/// Maximum number of statements to expire in a single iteration.
183const MAX_EXPIRY_STATEMENTS_PER_ITERATION: usize = 10_000;
184/// Maximum number of accounts to check for expiry in a single iteration.
185const MAX_EXPIRY_ACCOUNTS_PER_ITERATION: usize = 10_000;
186/// Maximum time in milliseconds to spend checking for expiry in a single iteration.
187const MAX_EXPIRY_TIME_PER_ITERATION: Duration = Duration::from_millis(100);
188
189/// Number of subscription filter worker tasks.
190const NUM_FILTER_WORKERS: usize = 1;
191
192const MAINTENANCE_PERIOD: std::time::Duration = std::time::Duration::from_secs(29);
193
194/// Specifies which block hash to use when reading statement allowances.
195enum AllowanceBlock {
196	/// Use the best (latest) block hash.
197	Best,
198	/// Use the finalized block hash.
199	Finalized,
200}
201
202// Period between enforcing limits (checking for expired statements and making sure statements stay
203// within allowances). Different from maintenance period to avoid keeping the lock for too long for
204// maintenance tasks.
205const ENFORCE_LIMITS_PERIOD: std::time::Duration = std::time::Duration::from_secs(31);
206
207mod col {
208	pub const META: u8 = 0;
209	pub const STATEMENTS: u8 = 1;
210	pub const EXPIRED: u8 = 2;
211	pub const INDEX_BY_TOPIC: u8 = 3;
212	pub const INDEX_BY_DEC_KEY: u8 = 4;
213	pub const INDEX_EVICTED: u8 = 5;
214	pub const ADMISSION_SEQ: u8 = 6;
215	pub const INDEX_BY_ACCOUNT: u8 = 7;
216	pub const INDEX_BY_EXPIRY: u8 = 8;
217
218	pub const COUNT: u8 = 9;
219}
220
221/// Btree-indexed columns (ordered keys with prefix scans), as opposed to the hash columns.
222const BTREE_COLUMNS: [u8; 6] = [
223	col::INDEX_BY_TOPIC,
224	col::INDEX_BY_DEC_KEY,
225	col::INDEX_EVICTED,
226	col::ADMISSION_SEQ,
227	col::INDEX_BY_ACCOUNT,
228	col::INDEX_BY_EXPIRY,
229];
230
231/// Budget of the per-account details cache, measured in cached statements across all account
232/// records (a record costs at least one unit). Bounds cache memory regardless of how statements
233/// are distributed over accounts.
234#[cfg(not(test))]
235const DETAILS_CACHE_BUDGET: usize = 65_536;
236#[cfg(test)]
237const DETAILS_CACHE_BUDGET: usize = 8;
238
239/// Maximum number of per-account count/size summaries kept in memory.
240#[cfg(not(test))]
241const SUMMARY_CACHE_ACCOUNTS: u32 = 131_072;
242#[cfg(test)]
243const SUMMARY_CACHE_ACCOUNTS: u32 = 4;
244
245const INDEX_EMPTY_VALUE: &[u8] = &[];
246const DEC_KEY_TAG_NONE: u8 = 0;
247const DEC_KEY_TAG_SOME: u8 = 1;
248
249fn topic_index_key(topic: &Topic, hash: &Hash) -> Vec<u8> {
250	let mut key = Vec::with_capacity(topic.len() + hash.len());
251	key.extend_from_slice(&topic[..]);
252	key.extend_from_slice(&hash[..]);
253	key
254}
255
256fn dec_key_index_prefix(dec_key: &Option<DecryptionKey>) -> Vec<u8> {
257	match dec_key {
258		None => vec![DEC_KEY_TAG_NONE],
259		Some(dec_key) => {
260			let mut prefix = Vec::with_capacity(1 + dec_key.len());
261			prefix.push(DEC_KEY_TAG_SOME);
262			prefix.extend_from_slice(&dec_key[..]);
263			prefix
264		},
265	}
266}
267
268fn dec_key_index_key(dec_key: &Option<DecryptionKey>, hash: &Hash) -> Vec<u8> {
269	let mut key = dec_key_index_prefix(dec_key);
270	key.extend_from_slice(&hash[..]);
271	key
272}
273
274/// Reverses [`dec_key_index_prefix`]; `None` if `prefix` does not have that layout.
275fn parse_dec_key_index_prefix(prefix: &[u8]) -> Option<Option<DecryptionKey>> {
276	match prefix.split_first() {
277		Some((&DEC_KEY_TAG_NONE, [])) => Some(None),
278		Some((&DEC_KEY_TAG_SOME, dec_key)) if dec_key.len() == size_of::<DecryptionKey>() => {
279			dec_key.try_into().ok().map(Some)
280		},
281		_ => {
282			log::error!(
283				target: LOG_TARGET,
284				"Corrupt decryption-key index prefix: {:?}",
285				HexDisplay::from(&prefix)
286			);
287			None
288		},
289	}
290}
291
292fn evicted_index_key(purge_at: u64, hash: &Hash) -> Vec<u8> {
293	let mut key = Vec::with_capacity(8 + hash.len());
294	key.extend_from_slice(&purge_at.to_be_bytes());
295	key.extend_from_slice(&hash[..]);
296	key
297}
298
299/// Key of a statement's row in [`col::INDEX_BY_ACCOUNT`]
300fn account_index_key(account: &AccountId, expiry: Expiry, hash: &Hash) -> Vec<u8> {
301	let mut key = Vec::with_capacity(account.len() + size_of::<u64>() + hash.len());
302	key.extend_from_slice(&account[..]);
303	key.extend_from_slice(&expiry.0.to_be_bytes());
304	key.extend_from_slice(&hash[..]);
305	key
306}
307
308/// Reverses [`account_index_key`]; `None` if `key` does not have that layout.
309fn parse_account_index_key(key: &[u8]) -> Option<(AccountId, Expiry, Hash)> {
310	if key.len() != size_of::<AccountId>() + size_of::<u64>() + size_of::<Hash>() {
311		log::error!(
312			target: LOG_TARGET,
313			"Corrupt account index key: {:?}",
314			HexDisplay::from(&key)
315		);
316		return None;
317	}
318	let (account, rest) = key.split_at(size_of::<AccountId>());
319	let (expiry, hash) = rest.split_at(size_of::<u64>());
320	Some((
321		account.try_into().ok()?,
322		Expiry(u64::from_be_bytes(expiry.try_into().ok()?)),
323		hash.try_into().ok()?,
324	))
325}
326
327/// Key of a statement's row in [`col::INDEX_BY_EXPIRY`]: the whole column iterates in expiry
328/// order, so the statements due for expiry form a prefix of it.
329fn expiry_index_key(expiry: Expiry, hash: &Hash) -> Vec<u8> {
330	let mut key = Vec::with_capacity(size_of::<u64>() + hash.len());
331	key.extend_from_slice(&expiry.0.to_be_bytes());
332	key.extend_from_slice(&hash[..]);
333	key
334}
335
336/// Reverses the shared `u64_be ‖ hash` layout of [`expiry_index_key`] and [`evicted_index_key`];
337/// `None` if `key` does not have that layout.
338fn parse_time_index_key(key: &[u8]) -> Option<(u64, Hash)> {
339	if key.len() != size_of::<u64>() + size_of::<Hash>() {
340		log::error!(target: LOG_TARGET, "Corrupt time index key: {:?}", HexDisplay::from(&key));
341		return None;
342	}
343	let (time, hash) = key.split_at(size_of::<u64>());
344	Some((u64::from_be_bytes(time.try_into().ok()?), hash.try_into().ok()?))
345}
346
347/// Per-statement details tracked inside a per-account record; also the (SCALE-encoded) value of
348/// the statement's [`col::INDEX_BY_ACCOUNT`] row.
349#[derive(Clone, Copy, PartialEq, Eq, Debug)]
350struct EntryDetails {
351	channel: Option<Channel>,
352	data_len: usize,
353	admission_seq: u64,
354}
355
356impl Encode for EntryDetails {
357	fn size_hint(&self) -> usize {
358		self.channel.size_hint() +
359			(self.data_len as u32).size_hint() +
360			self.admission_seq.size_hint()
361	}
362
363	fn encode_to<T: codec::Output + ?Sized>(&self, dest: &mut T) {
364		self.channel.encode_to(dest);
365		(self.data_len as u32).encode_to(dest);
366		self.admission_seq.encode_to(dest);
367	}
368}
369
370impl Decode for EntryDetails {
371	fn decode<I: codec::Input>(input: &mut I) -> std::result::Result<Self, codec::Error> {
372		let (channel, data_len, admission_seq) = <(Option<Channel>, u32, u64)>::decode(input)?;
373		Ok(EntryDetails { channel, data_len: data_len as usize, admission_seq })
374	}
375}
376
377/// The two rows tying a statement into the per-account and expiry indexes; written on insert
378/// (`details` carries the account row's value) and deleted with `None`.
379fn account_index_ops(
380	account: &AccountId,
381	expiry: Expiry,
382	hash: &Hash,
383	details: Option<&EntryDetails>,
384) -> [(u8, Vec<u8>, Option<Vec<u8>>); 2] {
385	[
386		(
387			col::INDEX_BY_ACCOUNT,
388			account_index_key(account, expiry, hash),
389			details.map(EntryDetails::encode),
390		),
391		(
392			col::INDEX_BY_EXPIRY,
393			expiry_index_key(expiry, hash),
394			details.map(|_| INDEX_EMPTY_VALUE.to_vec()),
395		),
396	]
397}
398
399/// The account id immediately after `account` in the index ordering, or `None` when `account`
400/// is the maximum id.
401fn next_account_id(mut account: AccountId) -> Option<AccountId> {
402	for byte in account.iter_mut().rev() {
403		match byte.checked_add(1) {
404			Some(incremented) => {
405				*byte = incremented;
406				return Some(account);
407			},
408			None => *byte = 0,
409		}
410	}
411	None
412}
413
414/// Extracts the trailing hash from a composite index key, if it is long enough.
415fn hash_from_index_key(key: &[u8]) -> Option<Hash> {
416	let Some(prefix_len) = key.len().checked_sub(size_of::<Hash>()) else {
417		log::error!(target: LOG_TARGET, "Corrupt index key: {:?}", HexDisplay::from(&key));
418		return None;
419	};
420	key[prefix_len..].try_into().ok()
421}
422
423/// The prefix of a composite index key — everything before the trailing hash — if it is long
424/// enough.
425fn prefix_from_index_key(key: &[u8]) -> Option<&[u8]> {
426	let Some(prefix_len) = key.len().checked_sub(size_of::<Hash>()) else {
427		log::error!(target: LOG_TARGET, "Corrupt index key: {:?}", HexDisplay::from(&key));
428		return None;
429	};
430	Some(&key[..prefix_len])
431}
432
433/// Builds the index-column operations for a statement's topic and decryption-key entries. With
434/// `insert == true` the entries are written; otherwise they are deleted. Designed to be folded
435/// into the same atomic [`parity_db::Db::commit`] as the statement body.
436fn statement_index_ops(
437	hash: &Hash,
438	statement: &Statement,
439	insert: bool,
440) -> Vec<(u8, Vec<u8>, Option<Vec<u8>>)> {
441	let value = insert.then(|| INDEX_EMPTY_VALUE.to_vec());
442	let mut ops = Vec::new();
443	let mut nt = 0;
444	while let Some(topic) = statement.topic(nt) {
445		ops.push((col::INDEX_BY_TOPIC, topic_index_key(&topic, hash), value.clone()));
446		nt += 1;
447	}
448	let dec_key = statement.decryption_key();
449	ops.push((col::INDEX_BY_DEC_KEY, dec_key_index_key(&dec_key, hash), value));
450	ops
451}
452
453#[derive(Eq, PartialEq, Debug, Ord, PartialOrd, Clone, Copy)]
454struct Expiry(u64);
455
456impl Expiry {
457	/// Returns the expiration timestamp in seconds
458	fn get_expiration_timestamp_secs(self) -> u64 {
459		self.0 >> 32
460	}
461}
462
463#[derive(PartialEq, Eq, Clone, Copy)]
464struct PriorityKey {
465	hash: Hash,
466	expiry: Expiry,
467}
468
469impl PartialOrd for PriorityKey {
470	fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
471		Some(self.cmp(other))
472	}
473}
474
475impl Ord for PriorityKey {
476	fn cmp(&self, other: &Self) -> std::cmp::Ordering {
477		self.expiry.cmp(&other.expiry).then_with(|| self.hash.cmp(&other.hash))
478	}
479}
480
481#[derive(PartialEq, Eq)]
482struct ChannelEntry {
483	hash: Hash,
484	expiry: Expiry,
485}
486
487#[derive(Default)]
488struct StatementsForAccount {
489	// Statements ordered by priority.
490	by_priority: BTreeMap<PriorityKey, EntryDetails>,
491	// Channel to statement map. Only one statement per channel is allowed.
492	channels: HashMap<Channel, ChannelEntry>,
493	// Sum of all `Data` field sizes.
494	data_size: usize,
495}
496
497impl StatementsForAccount {
498	fn insert_entry(&mut self, hash: Hash, expiry: Expiry, details: EntryDetails) {
499		self.data_size += details.data_len;
500		if let Some(channel) = details.channel {
501			self.channels.insert(channel, ChannelEntry { hash, expiry });
502		}
503		self.by_priority.insert(PriorityKey { hash, expiry }, details);
504	}
505
506	fn remove_entry(&mut self, key: &PriorityKey) -> Option<EntryDetails> {
507		let details = self.by_priority.remove(key)?;
508		self.data_size -= details.data_len;
509		if let Some(channel) = details.channel {
510			self.channels.remove(&channel);
511		}
512		Some(details)
513	}
514}
515
516/// Default number of concurrent workers for statement validation.
517pub const DEFAULT_NETWORK_WORKERS: usize = 1;
518
519/// Default maximum statements per second per peer before rate limiting kicks in.
520pub use sc_network_statement::config::DEFAULT_STATEMENTS_PER_SECOND as DEFAULT_RATE_LIMIT;
521
522/// Statement store and network handler configuration.
523#[derive(Debug, Clone, Copy)]
524pub struct Config {
525	/// Maximum statements allowed in the store. Once this limit is reached lower-priority
526	/// statements may be evicted.
527	pub max_total_statements: usize,
528	/// Maximum total data size allowed in the store. Once this limit is reached lower-priority
529	/// statements may be evicted.
530	pub max_total_size: usize,
531	/// Number of seconds for which removed statements won't be allowed to be added back in.
532	pub purge_after_sec: u64,
533	/// Number of concurrent workers for statement validation from the network.
534	pub network_workers: usize,
535	/// Maximum statements per second per peer before rate limiting kicks in.
536	pub rate_limit: u32,
537}
538
539impl Config {
540	/// Validate the configuration, returning an error if any values are invalid.
541	pub fn validate(&self) -> Result<()> {
542		if self.max_total_statements == 0 {
543			return Err(Error::InvalidConfig(
544				"max_total_statements must be greater than zero".into(),
545			));
546		}
547		if self.max_total_size == 0 {
548			return Err(Error::InvalidConfig("max_total_size must be greater than zero".into()));
549		}
550		if self.network_workers == 0 {
551			return Err(Error::InvalidConfig("network_workers must be greater than zero".into()));
552		}
553		Ok(())
554	}
555}
556
557impl Default for Config {
558	fn default() -> Self {
559		Config {
560			max_total_statements: DEFAULT_MAX_TOTAL_STATEMENTS,
561			max_total_size: DEFAULT_MAX_TOTAL_SIZE,
562			purge_after_sec: DEFAULT_PURGE_AFTER_SEC,
563			network_workers: DEFAULT_NETWORK_WORKERS,
564			rate_limit: DEFAULT_RATE_LIMIT,
565		}
566	}
567}
568
569/// In-memory part of the read index.
570struct QueryIndex {
571	// TODO: Remove counters; replace them with a merge-join/leapfrog1
572	topic_counts: HashMap<Topic, usize>,
573	dec_key_counts: HashMap<Option<DecryptionKey>, usize>,
574	recent: HashMap<Hash, u64>,
575}
576
577impl QueryIndex {
578	fn new() -> Self {
579		QueryIndex {
580			topic_counts: HashMap::new(),
581			dec_key_counts: HashMap::new(),
582			recent: HashMap::new(),
583		}
584	}
585
586	/// Records a newly inserted statement: bumps cardinalities and marks the hash as recent
587	/// under its admission sequence number.
588	fn note_insert(&mut self, hash: Hash, statement: &Statement, seq: u64) {
589		let mut nt = 0;
590		while let Some(topic) = statement.topic(nt) {
591			*self.topic_counts.entry(topic).or_insert(0) += 1;
592			nt += 1;
593		}
594		let dec_key = statement.decryption_key();
595		*self.dec_key_counts.entry(dec_key).or_insert(0) += 1;
596		self.recent.insert(hash, seq);
597	}
598
599	/// Records a removed statement: decrements cardinalities and drops the hash from `recent`.
600	fn note_remove(&mut self, hash: &Hash, statement: &Statement) {
601		let mut nt = 0;
602		while let Some(topic) = statement.topic(nt) {
603			if let Some(count) = self.topic_counts.get_mut(&topic) {
604				*count = count.saturating_sub(1);
605				if *count == 0 {
606					self.topic_counts.remove(&topic);
607				}
608			}
609			nt += 1;
610		}
611		let dec_key = statement.decryption_key();
612		if let Some(count) = self.dec_key_counts.get_mut(&dec_key) {
613			*count = count.saturating_sub(1);
614			if *count == 0 {
615				self.dec_key_counts.remove(&dec_key);
616			}
617		}
618		self.recent.remove(hash);
619	}
620
621	/// Takes and clears the set of recently added hashes with their admission sequence numbers.
622	fn take_recent(&mut self) -> HashMap<Hash, u64> {
623		std::mem::take(&mut self.recent)
624	}
625}
626
627/// Statement count and total data size of one account.
628#[derive(Clone, Copy)]
629struct AccountSummary {
630	count: usize,
631	data_size: usize,
632}
633
634/// In-memory part of the submit index (constraint checking, quota enforcement).
635///
636/// The authoritative per-statement data lives on disk in [`col::INDEX_BY_ACCOUNT`] /
637/// [`col::INDEX_BY_EXPIRY`]; this structure holds bounded write-through caches over it plus the
638/// global counters.
639struct SubmitIndex {
640	/// Cached per-account detail records. Bounded by [`Self::cached_statement_count`]
641	/// against [`DETAILS_CACHE_BUDGET`].
642	account_statements: LruMap<AccountId, StatementsForAccount>,
643	/// Total number of statements across all cached records in [`Self::account_statements`].
644	cached_statement_count: usize,
645	/// Cached per-account count/size summaries. Cheap enough to cover many more accounts than
646	/// the details cache; lets `submit` skip loading the full record for channel-less statements
647	/// well within their quota, and lets `enforce_limits` skip accounts within their allowance.
648	summaries: LruMap<AccountId, AccountSummary>,
649	/// Resume point of the incremental per-account allowance sweep in `enforce_limits`: the
650	/// account id to continue the scan from, or `None` when a new pass starts from the beginning.
651	allowance_cursor: Option<AccountId>,
652	/// Number of accounts already seen by the current allowance sweep pass.
653	allowance_cycle_seen: usize,
654	/// Store configuration (global limits, purge period).
655	config: Config,
656	/// Number of stored statements.
657	statement_count: usize,
658	/// Running total of data size across all stored statements.
659	total_size: usize,
660	evicted_count: usize,
661	// Monotonic sequence number assigned to each statement as it is inserted.
662	next_seq: u64,
663	// Sequence numbers of recently inserted statements, kept only while at least one subscription
664	// snapshot scan is in progress and only back to the oldest such scan's watermark.
665	recent_seqs: HashMap<Hash, u64>,
666	// Watermarks of the currently running snapshot scans.
667	active_scan_floors: BTreeMap<u64, usize>,
668}
669
670struct ClientWrapper<Block, Client, BE> {
671	client: Arc<Client>,
672	_block: std::marker::PhantomData<Block>,
673	_backend: std::marker::PhantomData<BE>,
674}
675
676impl<Block, Client, BE> ClientWrapper<Block, Client, BE>
677where
678	Block: BlockT,
679	Block::Hash: From<BlockHash>,
680	BE: Backend<Block> + 'static,
681	Client: HeaderBackend<Block> + StorageProvider<Block, BE> + Send + Sync + 'static,
682{
683	fn read_allowance(
684		&self,
685		account_id: &AccountId,
686		allowance_block: AllowanceBlock,
687	) -> Result<Option<StatementAllowance>> {
688		use sp_statement_store::{statement_allowance_key, StatementAllowance};
689
690		let block_hash = match allowance_block {
691			AllowanceBlock::Best => self.client.info().best_hash,
692			AllowanceBlock::Finalized => self.client.info().finalized_hash,
693		};
694		let key = statement_allowance_key(account_id);
695		let storage_key = StorageKey(key);
696		self.client
697			.storage(block_hash, &storage_key)
698			.map_err(|e| Error::Storage(format!("Failed to read allowance: {:?}", e)))?
699			.map(|value| {
700				StatementAllowance::decode(&mut &value.0[..])
701					.map_err(|e| Error::Decode(format!("Failed to decode allowance: {:?}", e)))
702			})
703			.transpose()
704	}
705}
706
707/// Statement store.
708pub struct Store {
709	db: parity_db::Db,
710	submit_index: RwLock<SubmitIndex>,
711	query_index: RwLock<QueryIndex>,
712	read_allowance_fn:
713		Box<dyn Fn(&AccountId, AllowanceBlock) -> Result<Option<StatementAllowance>> + Send + Sync>,
714	subscription_manager: SubscriptionsHandle,
715	keystore: Arc<LocalKeystore>,
716	/// Number of accounts with stored statements. Reported by `maintain`; may lag the
717	/// exact value by up to one sweep cycle.
718	known_accounts_count: AtomicUsize,
719	// Used for testing
720	time_override: Option<u64>,
721	metrics: PrometheusMetrics,
722}
723
724impl ReplaySnapshotProvider for Weak<Store> {
725	fn register_replay(&self, enqueue: &mut dyn FnMut(u64) -> bool) -> Result<Option<u64>> {
726		let Some(store) = self.upgrade() else {
727			return Err(Error::InvalidConfig("statement store is closed".into()));
728		};
729		store.register_replay(enqueue)
730	}
731
732	fn replay_batch(
733		&self,
734		filter: &OptimizedTopicFilter,
735		cursor: u64,
736		watermark: u64,
737	) -> Result<ReplayBatch> {
738		let Some(store) = self.upgrade() else {
739			return Err(Error::InvalidConfig("statement store is closed".into()));
740		};
741		store.replay_batch(filter, cursor, watermark)
742	}
743}
744
745/// What admitting a new statement will claim and evict, as decided by
746/// [`SubmitIndex::plan_insert`].
747struct InsertPlan {
748	/// Admission sequence the new statement will claim.
749	seq: u64,
750	/// The account's own statements that must be evicted to make room. Their bodies and on-disk
751	/// index rows must be deleted.
752	evicted: Vec<(PriorityKey, EntryDetails)>,
753	/// The subset of `evicted` that is banned from re-acceptance, with its purge deadline. These
754	/// must be recorded in the on-disk evicted journal.
755	banned: Vec<(Hash, u64)>,
756}
757
758/// A single on-disk index set referenced during a query: either the set of hashes carrying a
759/// topic, or the set of hashes for a decryption key.
760#[derive(Clone)]
761enum IndexSet {
762	Topic(Topic),
763	DecKey(Option<DecryptionKey>),
764}
765
766impl IndexSet {
767	fn column(&self) -> u8 {
768		match self {
769			IndexSet::Topic(_) => col::INDEX_BY_TOPIC,
770			IndexSet::DecKey(_) => col::INDEX_BY_DEC_KEY,
771		}
772	}
773
774	/// Prefix selecting every entry of this set within its column.
775	fn prefix(&self) -> Vec<u8> {
776		match self {
777			IndexSet::Topic(t) => t[..].to_vec(),
778			IndexSet::DecKey(k) => dec_key_index_prefix(k),
779		}
780	}
781
782	/// Full key of `hash` within this set's column, for point membership lookups.
783	fn member_key(&self, hash: &Hash) -> Vec<u8> {
784		match self {
785			IndexSet::Topic(t) => topic_index_key(t, hash),
786			IndexSet::DecKey(k) => dec_key_index_key(k, hash),
787		}
788	}
789
790	/// Cardinality of this set, read from the in-memory cardinality counters.
791	fn len(&self, read_index: &QueryIndex) -> usize {
792		match self {
793			IndexSet::Topic(t) => read_index.topic_counts.get(t).copied().unwrap_or(0),
794			IndexSet::DecKey(k) => read_index.dec_key_counts.get(k).copied().unwrap_or(0),
795		}
796	}
797}
798
799impl SubmitIndex {
800	fn new(config: Config) -> SubmitIndex {
801		SubmitIndex {
802			// The real bound is [`Self::cached_statement_count`], enforced by
803			// [`Self::cache_record`]; this length limiter must never trigger on its own, and
804			// cannot: every record costs at least one unit, so the cost trimming keeps the
805			// length at or below the budget.
806			account_statements: LruMap::new(ByLength::new(u32::MAX)),
807			summaries: LruMap::new(ByLength::new(SUMMARY_CACHE_ACCOUNTS)),
808			config,
809			statement_count: 0,
810			total_size: 0,
811			next_seq: 0,
812			cached_statement_count: 0,
813			allowance_cursor: None,
814			allowance_cycle_seen: 0,
815			evicted_count: 0,
816			recent_seqs: HashMap::new(),
817			active_scan_floors: BTreeMap::new(),
818		}
819	}
820
821	/// The [`KEY_COUNTERS`] row reflecting the given post-commit totals. Folded into every
822	/// mutating commit.
823	fn counters_op(
824		statement_count: usize,
825		total_size: usize,
826		next_seq: u64,
827	) -> (u8, Vec<u8>, Option<Vec<u8>>) {
828		(
829			col::META,
830			KEY_COUNTERS.to_vec(),
831			Some((statement_count as u64, total_size as u64, next_seq).encode()),
832		)
833	}
834
835	/// Removes the account's record from the details cache, keeping the cost accounting exact.
836	fn uncache_record(&mut self, account: &AccountId) -> Option<StatementsForAccount> {
837		let record = self.account_statements.remove(account)?;
838		self.cached_statement_count -= record.by_priority.len();
839		Some(record)
840	}
841
842	/// Puts a record (back) into the details cache, then evicts the least-recently-used records
843	/// while the total number of cached statements exceeds the budget. A single record larger
844	/// than the whole budget is kept: the cache never trims below one entry.
845	fn cache_record(&mut self, account: AccountId, record: StatementsForAccount) {
846		if record.by_priority.is_empty() {
847			return;
848		}
849		self.cached_statement_count += record.by_priority.len();
850		self.account_statements.insert(account, record);
851		while self.cached_statement_count > DETAILS_CACHE_BUDGET &&
852			self.account_statements.len() > 1
853		{
854			match self.account_statements.pop_oldest() {
855				Some((_, evicted)) => self.cached_statement_count -= evicted.by_priority.len(),
856				None => break,
857			}
858		}
859	}
860
861	/// Puts a record into the details cache together with the summary derived from it. An
862	/// account with no statements is represented in both caches by absence, so an empty record
863	/// refreshes neither.
864	fn cache_record_with_summary(&mut self, account: AccountId, record: StatementsForAccount) {
865		if !record.by_priority.is_empty() {
866			self.summaries.insert(
867				account,
868				AccountSummary { count: record.by_priority.len(), data_size: record.data_size },
869			);
870		}
871		self.cache_record(account, record);
872	}
873
874	/// Records a sequence number in the snapshot window, once its statement and admission entries
875	/// have committed atomically. The number itself is claimed earlier, by [`Self::insert`].
876	fn note_seq(&mut self, hash: Hash, seq: u64) {
877		if !self.active_scan_floors.is_empty() {
878			self.recent_seqs.insert(hash, seq);
879		}
880	}
881
882	/// Registers a subscription snapshot scan and returns its watermark.
883	fn begin_scan(&mut self) -> u64 {
884		let watermark = self.next_seq;
885		*self.active_scan_floors.entry(watermark).or_insert(0) += 1;
886		watermark
887	}
888
889	/// Deregisters a snapshot scan previously registered with [`Self::begin_scan`] and prunes
890	/// `recent_seqs` down to the smallest still-active watermark.
891	fn end_scan(&mut self, watermark: u64) {
892		if let Some(count) = self.active_scan_floors.get_mut(&watermark) {
893			*count -= 1;
894			if *count == 0 {
895				self.active_scan_floors.remove(&watermark);
896			}
897		}
898		match self.active_scan_floors.keys().next() {
899			Some(&floor) => self.recent_seqs.retain(|_, seq| *seq >= floor),
900			None => self.recent_seqs.clear(),
901		}
902	}
903
904	/// Whether the statement `hash` belongs in the snapshot of a scan with the given `watermark`.
905	fn seq_covered_by_snapshot(&self, hash: &Hash, watermark: u64) -> bool {
906		match self.recent_seqs.get(hash) {
907			Some(&seq) => seq < watermark,
908			None => true,
909		}
910	}
911
912	/// Pure constraint check for admitting `statement`: decides, without mutating anything, which
913	/// of the account's own statements must be evicted to make room, or why the statement cannot
914	/// be admitted. `record` is the account's current state; global limits are checked against
915	/// the in-memory counters. The store never evicts other accounts' statements to admit a new
916	/// one — when global limits cannot be met from this account alone, the statement is rejected.
917	fn plan_insert(
918		&self,
919		record: &StatementsForAccount,
920		hash: Hash,
921		statement: &Statement,
922		account: &AccountId,
923		validation: &StatementAllowance,
924		current_time: u64,
925	) -> std::result::Result<InsertPlan, RejectionReason> {
926		let statement_len = statement.data_len();
927		if statement_len > validation.max_size as usize {
928			log::debug!(
929				target: LOG_TARGET,
930				"Ignored oversize message from account {}: {:?} ({} bytes)",
931				HexDisplay::from(account),
932				HexDisplay::from(&hash),
933				statement_len,
934			);
935			return Err(RejectionReason::DataTooLarge {
936				submitted_size: statement_len,
937				available_size: validation.max_size as usize,
938			});
939		}
940
941		let mut evicted: Vec<(PriorityKey, EntryDetails)> = Vec::new();
942		let mut evicted_hashes = HashSet::new();
943		let mut would_free_size = 0;
944		let expiry = Expiry(statement.expiry());
945		let (max_size, max_count) = (validation.max_size as usize, validation.max_count as usize);
946		// It may happen that we can't delete enough lower priority messages
947		// to satisfy size constraints. We check for that before deleting anything,
948		// taking into account channel message replacement.
949		if let Some(channel) = statement.channel() {
950			if let Some(channel_record) = record.channels.get(&channel) {
951				if expiry <= channel_record.expiry {
952					// Trying to replace channel message with lower expiry.
953					log::debug!(
954						target: LOG_TARGET,
955						"Ignored lower priority channel message from account {}: {:?} {:?} <= {:?}",
956						HexDisplay::from(account),
957						HexDisplay::from(&hash),
958						expiry,
959						channel_record.expiry,
960					);
961					return Err(RejectionReason::ChannelPriorityTooLow {
962						submitted_expiry: expiry.0,
963						min_expiry: channel_record.expiry.0,
964					});
965				} else {
966					// Would replace channel message. Still need to check for size constraints
967					// below.
968					log::debug!(
969						target: LOG_TARGET,
970						"Replacing higher priority channel message from account {}: {:?} ({:?}) > {:?} ({:?})",
971						HexDisplay::from(account),
972						HexDisplay::from(&hash),
973						expiry,
974						HexDisplay::from(&channel_record.hash),
975						channel_record.expiry,
976					);
977					let key =
978						PriorityKey { hash: channel_record.hash, expiry: channel_record.expiry };
979					if let Some(details) = record.by_priority.get(&key) {
980						would_free_size += details.data_len;
981						evicted_hashes.insert(key.hash);
982						evicted.push((key, *details));
983					}
984				}
985			}
986		}
987		// Check if we can evict enough lower priority statements to satisfy constraints
988		for (entry, details) in record.by_priority.iter() {
989			if (record.data_size - would_free_size + statement_len <= max_size) &&
990				record.by_priority.len() + 1 - evicted_hashes.len() <= max_count
991			{
992				// Satisfied
993				break;
994			}
995			if evicted_hashes.contains(&entry.hash) {
996				// Already accounted for above
997				continue;
998			}
999			if entry.expiry >= expiry {
1000				log::debug!(
1001					target: LOG_TARGET,
1002					"Ignored message from account {} due to constraints {:?} {:?} < {:?}",
1003					HexDisplay::from(account),
1004					HexDisplay::from(&hash),
1005					expiry,
1006					entry.expiry,
1007				);
1008				let retained_size = record.data_size - would_free_size;
1009				if retained_size + statement_len > max_size {
1010					return Err(RejectionReason::DataTooLarge {
1011						submitted_size: statement_len,
1012						available_size: max_size.saturating_sub(retained_size),
1013					});
1014				}
1015				return Err(RejectionReason::AccountFull {
1016					submitted_expiry: expiry.0,
1017					min_expiry: entry.expiry.0,
1018				});
1019			}
1020			evicted_hashes.insert(entry.hash);
1021			would_free_size += details.data_len;
1022			evicted.push((*entry, *details));
1023		}
1024		// Now check global constraints as well.
1025		if !((self.total_size - would_free_size + statement_len <= self.config.max_total_size) &&
1026			self.statement_count + 1 - evicted.len() <= self.config.max_total_statements)
1027		{
1028			log::debug!(
1029				target: LOG_TARGET,
1030				"Ignored statement {} from account {} because the store is full (size={}, count={})",
1031				HexDisplay::from(&hash),
1032				HexDisplay::from(account),
1033				self.total_size,
1034				self.statement_count,
1035			);
1036			return Err(RejectionReason::StoreFull);
1037		}
1038
1039		let banned = evicted
1040			.iter()
1041			.filter_map(|(key, _)| {
1042				let expiry_ts = key.expiry.get_expiration_timestamp_secs();
1043				(current_time < expiry_ts).then(|| {
1044					(
1045						key.hash,
1046						expiry_ts.min(current_time.saturating_add(self.config.purge_after_sec)),
1047					)
1048				})
1049			})
1050			.collect();
1051		Ok(InsertPlan { seq: self.next_seq, evicted, banned })
1052	}
1053
1054	/// Applies a committed insertion to the caches and counters. `loaded_record` carries the
1055	/// account's record when planning had to load it from disk; with `None` the cached copy (if
1056	/// any) is updated in place, which also covers the summary fast path where no record was
1057	/// materialised at all.
1058	fn apply_insert(
1059		&mut self,
1060		account: &AccountId,
1061		loaded_record: Option<StatementsForAccount>,
1062		hash: Hash,
1063		statement: &Statement,
1064		plan: &InsertPlan,
1065	) {
1066		let statement_len = statement.data_len();
1067		let freed: usize = plan.evicted.iter().map(|(_, details)| details.data_len).sum();
1068		self.statement_count = self.statement_count + 1 - plan.evicted.len();
1069		self.total_size = self.total_size + statement_len - freed;
1070		self.evicted_count += plan.banned.len();
1071		self.next_seq = plan.seq.saturating_add(1);
1072		self.note_seq(hash, plan.seq);
1073		for (key, _) in &plan.evicted {
1074			log::trace!(target: LOG_TARGET, "Expired statement {:?}", HexDisplay::from(&key.hash));
1075		}
1076		let details = EntryDetails {
1077			channel: statement.channel(),
1078			data_len: statement_len,
1079			admission_seq: plan.seq,
1080		};
1081		match loaded_record.or_else(|| self.uncache_record(account)) {
1082			Some(mut record) => {
1083				for (key, _) in &plan.evicted {
1084					record.remove_entry(key);
1085				}
1086				record.insert_entry(hash, Expiry(statement.expiry()), details);
1087				self.cache_record_with_summary(*account, record);
1088			},
1089			None => {
1090				// Summary fast path: no record was materialised, only the summary is maintained.
1091				assert!(plan.evicted.is_empty());
1092				if let Some(summary) = self.summaries.get(account) {
1093					summary.count += 1;
1094					summary.data_size += statement_len;
1095				}
1096			},
1097		}
1098	}
1099
1100	/// Applies the committed removal of one statement to the caches and counters.
1101	fn apply_removal(
1102		&mut self,
1103		account: &AccountId,
1104		key: &PriorityKey,
1105		data_len: usize,
1106		banned: bool,
1107	) {
1108		self.statement_count = self.statement_count.saturating_sub(1);
1109		self.total_size = self.total_size.saturating_sub(data_len);
1110		if banned {
1111			self.evicted_count += 1;
1112		}
1113		if let Some(record) = self.account_statements.peek_mut(account) {
1114			if record.remove_entry(key).is_some() {
1115				self.cached_statement_count -= 1;
1116			}
1117			if record.by_priority.is_empty() {
1118				self.account_statements.remove(account);
1119			}
1120		}
1121		if let Some(summary) = self.summaries.peek_mut(account) {
1122			summary.count = summary.count.saturating_sub(1);
1123			summary.data_size = summary.data_size.saturating_sub(data_len);
1124			if summary.count == 0 {
1125				self.summaries.remove(account);
1126			}
1127		}
1128	}
1129
1130	/// Applies the committed removal of an account's every statement to the caches and counters.
1131	fn apply_account_removal(
1132		&mut self,
1133		account: &AccountId,
1134		removed_count: usize,
1135		freed_size: usize,
1136		banned_count: usize,
1137	) {
1138		self.statement_count = self.statement_count.saturating_sub(removed_count);
1139		self.total_size = self.total_size.saturating_sub(freed_size);
1140		self.evicted_count += banned_count;
1141		if let Some(record) = self.account_statements.remove(account) {
1142			self.cached_statement_count -= record.by_priority.len();
1143		}
1144		self.summaries.remove(account);
1145	}
1146}
1147
1148impl Store {
1149	/// Create a new shared store instance. There should only be one per process.
1150	/// `path` will be used to open a statement database or create a new one if it does not exist.
1151	pub fn new_shared<Block, Client, BE>(
1152		path: &std::path::Path,
1153		config: Config,
1154		client: Arc<Client>,
1155		keystore: Arc<LocalKeystore>,
1156		prometheus: Option<&PrometheusRegistry>,
1157		task_spawner: Box<dyn SpawnNamed>,
1158	) -> Result<Arc<Store>>
1159	where
1160		Block: BlockT,
1161		Block::Hash: From<BlockHash>,
1162		BE: Backend<Block> + 'static,
1163		Client: HeaderBackend<Block> + StorageProvider<Block, BE> + Send + Sync + 'static,
1164	{
1165		let store =
1166			Arc::new(Self::new(path, config, client, keystore, prometheus, task_spawner.clone())?);
1167
1168		// Perform periodic statement store maintenance
1169		let worker_store = store.clone();
1170		task_spawner.spawn(
1171			"statement-store-maintenance",
1172			Some("statement-store"),
1173			Box::pin(async move {
1174				let mut maintenance_interval = tokio::time::interval(MAINTENANCE_PERIOD);
1175				let mut enforce_limits_interval = tokio::time::interval(ENFORCE_LIMITS_PERIOD);
1176				loop {
1177					futures::select! {
1178						_ = maintenance_interval.tick().fuse() => {worker_store.maintain();}
1179						_ = enforce_limits_interval.tick().fuse() => {worker_store.enforce_limits();}
1180					}
1181				}
1182			}),
1183		);
1184
1185		Ok(store)
1186	}
1187
1188	/// Create a new instance.
1189	/// `path` will be used to open a statement database or create a new one if it does not exist.
1190	#[doc(hidden)]
1191	pub fn new<Block, Client, BE>(
1192		path: &std::path::Path,
1193		config: Config,
1194		client: Arc<Client>,
1195		keystore: Arc<LocalKeystore>,
1196		prometheus: Option<&PrometheusRegistry>,
1197		task_spawner: Box<dyn SpawnNamed>,
1198	) -> Result<Store>
1199	where
1200		Block: BlockT,
1201		Block::Hash: From<BlockHash>,
1202		BE: Backend<Block> + 'static,
1203		Client: HeaderBackend<Block> + StorageProvider<Block, BE> + Send + Sync + 'static,
1204	{
1205		config.validate()?;
1206
1207		let mut path: std::path::PathBuf = path.into();
1208		path.push("statements");
1209
1210		Self::migrate_columns(&path)?;
1211		let db = Self::open_db(&path)?;
1212		let needs_index_migration = Self::check_db_version(&db)?;
1213
1214		let storage_reader =
1215			ClientWrapper { client, _block: Default::default(), _backend: Default::default() };
1216		let read_allowance_fn =
1217			Box::new(move |account_id: &AccountId, allowance_block: AllowanceBlock| {
1218				storage_reader.read_allowance(account_id, allowance_block)
1219			});
1220
1221		let store = Store {
1222			db,
1223			submit_index: RwLock::new(SubmitIndex::new(config)),
1224			query_index: RwLock::new(QueryIndex::new()),
1225			read_allowance_fn,
1226			keystore,
1227			known_accounts_count: AtomicUsize::new(0),
1228			time_override: None,
1229			metrics: PrometheusMetrics::new(prometheus),
1230			subscription_manager: SubscriptionsHandle::new(
1231				task_spawner.clone(),
1232				NUM_FILTER_WORKERS,
1233			),
1234		};
1235		store.populate(needs_index_migration)?;
1236		Ok(store)
1237	}
1238
1239	/// Migrate the column layout of an existing database to the current schema.
1240	fn migrate_columns(path: &std::path::Path) -> Result<()> {
1241		let Some(metadata) =
1242			parity_db::Options::load_metadata(path).map_err(|e| Error::Db(e.to_string()))?
1243		else {
1244			return Ok(());
1245		};
1246		if metadata.columns.len() >= col::COUNT as usize {
1247			return Ok(());
1248		}
1249		let mut migrate_config = parity_db::Options::with_columns(path, 0);
1250		migrate_config.columns = metadata.columns;
1251		while migrate_config.columns.len() < col::COUNT as usize {
1252			// `add_column` takes the options by value, so build a fresh one each iteration.
1253			let mut new_column_options = parity_db::ColumnOptions::default();
1254			let column = migrate_config.columns.len() as u8;
1255			new_column_options.btree_index = BTREE_COLUMNS.contains(&column);
1256			parity_db::Db::add_column(&mut migrate_config, new_column_options)
1257				.map_err(|e| Error::Db(e.to_string()))?;
1258		}
1259		Ok(())
1260	}
1261
1262	/// Open (or create) the statement database with the column options expected by the current
1263	/// schema.
1264	fn open_db(path: &std::path::Path) -> Result<parity_db::Db> {
1265		let mut db_config = parity_db::Options::with_columns(path, col::COUNT);
1266		let statement_col = &mut db_config.columns[col::STATEMENTS as usize];
1267		statement_col.ref_counted = false;
1268		statement_col.preimage = true;
1269		statement_col.uniform = true;
1270		for c in BTREE_COLUMNS {
1271			db_config.columns[c as usize].btree_index = true;
1272		}
1273		parity_db::Db::open_or_create(&db_config).map_err(|e| Error::Db(e.to_string()))
1274	}
1275
1276	/// Read the on-disk database version and reconcile it with [`CURRENT_VERSION`].
1277	///
1278	/// A brand new database has its version initialised and needs no migration. An existing
1279	/// database from a newer version is rejected. Returns `true` if the on-disk indexes predate
1280	/// the current version and therefore need to be rebuilt.
1281	fn check_db_version(db: &parity_db::Db) -> Result<bool> {
1282		match db.get(col::META, &KEY_VERSION).map_err(|e| Error::Db(e.to_string()))? {
1283			Some(version) => {
1284				let version = u32::from_le_bytes(
1285					version
1286						.try_into()
1287						.map_err(|_| Error::Db("Error reading database version".into()))?,
1288				);
1289				if version > CURRENT_VERSION {
1290					return Err(Error::Db(format!("Unsupported database version: {version}")));
1291				}
1292				Ok(version < CURRENT_VERSION)
1293			},
1294			None => {
1295				// Brand new database: the index columns start empty, nothing to migrate.
1296				db.commit([(
1297					col::META,
1298					KEY_VERSION.to_vec(),
1299					Some(CURRENT_VERSION.to_le_bytes().to_vec()),
1300				)])
1301				.map_err(|e| Error::Db(e.to_string()))?;
1302				Ok(false)
1303			},
1304		}
1305	}
1306
1307	/// Restore the in-memory state from the database at startup. Statement bodies are never
1308	/// decoded here, so startup cost does not depend on the size of the stored statements.
1309	///
1310	/// A database written by an older version is first migrated with [`Self::migrate_database`].
1311	// This function should only be used on startup. There should be no other DB operations when
1312	// iterating the index.
1313	fn populate(&self, migrate_index: bool) -> Result<()> {
1314		if migrate_index {
1315			self.migrate_database()?;
1316		}
1317
1318		{
1319			let mut submit_index = self.submit_index.write();
1320			let mut query_index = self.query_index.write();
1321
1322			if let Some(counters) =
1323				self.db.get(col::META, KEY_COUNTERS).map_err(|e| Error::Db(e.to_string()))?
1324			{
1325				let (statement_count, total_size, next_seq) =
1326					<(u64, u64, u64)>::decode(&mut counters.as_slice())
1327						.map_err(|_| Error::Db("Error reading the store counters".into()))?;
1328				submit_index.statement_count = statement_count as usize;
1329				submit_index.total_size = total_size as usize;
1330				submit_index.next_seq = next_seq;
1331			}
1332			// While `next_seq` already holds the correct admission sequence number, it costs close
1333			// to nothing to check if the `next_seq` is consistent on the startup, and it can
1334			// catch a bug which could otherwise escape silently.
1335			let mut iter =
1336				self.db.iter(col::ADMISSION_SEQ).map_err(|e| Error::Db(e.to_string()))?;
1337			iter.seek_to_last().map_err(|e| Error::Db(e.to_string()))?;
1338			if let Some((key, _)) = iter.prev().map_err(|e| Error::Db(e.to_string()))? {
1339				let seq = u64::from_be_bytes(
1340					key.try_into()
1341						.map_err(|_| Error::Db("Invalid admission sequence key".into()))?,
1342				);
1343				if submit_index.next_seq <= seq {
1344					log::error!(
1345						target: LOG_TARGET,
1346						"The store counters lag the admission journal (next_seq {} <= last \
1347						 admission {}); recovering. This must be a bug, please report.",
1348						submit_index.next_seq,
1349						seq,
1350					);
1351					submit_index.next_seq = seq.saturating_add(1);
1352				}
1353			}
1354
1355			let mut evicted_count = 0usize;
1356			self.db
1357				.iter_column_while(col::EXPIRED, |_| {
1358					evicted_count += 1;
1359					true
1360				})
1361				.map_err(|e| Error::Db(e.to_string()))?;
1362			submit_index.evicted_count = evicted_count;
1363
1364			// Read-side cardinality counters, rebuilt from the index keys alone.
1365			for (prefix, count) in self.count_index_prefixes(col::INDEX_BY_TOPIC)? {
1366				let Ok(topic) = <[u8; 32]>::try_from(prefix.as_slice()).map(Topic::from) else {
1367					log::error!(
1368						target: LOG_TARGET,
1369						"Corrupt topic index prefix: {:?}",
1370						HexDisplay::from(&prefix)
1371					);
1372					continue;
1373				};
1374				query_index.topic_counts.insert(topic, count);
1375			}
1376			for (prefix, count) in self.count_index_prefixes(col::INDEX_BY_DEC_KEY)? {
1377				let Some(dec_key) = parse_dec_key_index_prefix(&prefix) else { continue };
1378				query_index.dec_key_counts.insert(dec_key, count);
1379			}
1380		}
1381
1382		self.known_accounts_count.store(self.count_accounts()?, AtomicOrdering::Relaxed);
1383		self.maintain();
1384		Ok(())
1385	}
1386
1387	/// Rebuilds every derived column from the authoritative
1388	/// `STATEMENTS` and `EXPIRED` columns. Existing rows are rewritten in place, so the migration
1389	/// is idempotent and safe to re-run after an interruption; the version is bumped only after
1390	/// everything else has been committed.
1391	fn migrate_database(&self) -> Result<()> {
1392		let purge_after_sec = self.submit_index.read().config.purge_after_sec;
1393		// Admission entries persisted by an earlier version keep their sequence numbers;
1394		// statements lacking one (all of them, on a migration from version 1) get fresh numbers.
1395		let mut admission_seqs = HashMap::new();
1396		let mut next_seq = 0u64;
1397		{
1398			let mut iter =
1399				self.db.iter(col::ADMISSION_SEQ).map_err(|e| Error::Db(e.to_string()))?;
1400			iter.seek_to_first().map_err(|e| Error::Db(e.to_string()))?;
1401			while let Some((key, value)) = iter.next().map_err(|e| Error::Db(e.to_string()))? {
1402				let seq = u64::from_be_bytes(
1403					key.try_into()
1404						.map_err(|_| Error::Db("Invalid admission sequence key".into()))?,
1405				);
1406				let hash: Hash = value
1407					.as_slice()
1408					.try_into()
1409					.map_err(|_| Error::Db("Invalid admission sequence hash".into()))?;
1410				admission_seqs.insert(hash, seq);
1411				next_seq = next_seq.max(seq.saturating_add(1));
1412			}
1413		}
1414
1415		let mut migration = MigrationBatch::new(&self.db);
1416		let mut migration_error = None;
1417		let mut statement_count = 0usize;
1418		let mut total_size = 0usize;
1419		self.db
1420			.iter_column_while(col::STATEMENTS, |item| {
1421				let Ok(statement) = Statement::decode(&mut item.value.as_slice()) else {
1422					log::error!(
1423						target: LOG_TARGET,
1424						"Corrupt statement {:?}",
1425						HexDisplay::from(&sp_statement_store::hash_encoded(&item.value))
1426					);
1427					return true;
1428				};
1429				let hash = statement.hash();
1430				let Some(account) = statement.account_id() else {
1431					log::error!(
1432						target: LOG_TARGET,
1433						"Statement without an account id loaded from the DB: {:?}",
1434						HexDisplay::from(&hash)
1435					);
1436					return true;
1437				};
1438				log::trace!(target: LOG_TARGET, "Statement loaded {:?}", HexDisplay::from(&hash));
1439				let persisted_seq = admission_seqs.remove(&hash);
1440				let seq = persisted_seq.unwrap_or_else(|| {
1441					let seq = next_seq;
1442					next_seq = seq.saturating_add(1);
1443					seq
1444				});
1445				statement_count += 1;
1446				total_size += statement.data_len();
1447				let details = EntryDetails {
1448					channel: statement.channel(),
1449					data_len: statement.data_len(),
1450					admission_seq: seq,
1451				};
1452				let admission_op = persisted_seq.is_none().then(|| DbOperation {
1453					column: col::ADMISSION_SEQ,
1454					key: seq.to_be_bytes().to_vec(),
1455					value: Some(hash.to_vec()),
1456				});
1457				let operations = statement_index_ops(&hash, &statement, true)
1458					.into_iter()
1459					.chain(account_index_ops(
1460						&account,
1461						Expiry(statement.expiry()),
1462						&hash,
1463						Some(&details),
1464					))
1465					.map(DbOperation::from)
1466					.chain(admission_op);
1467				if let Err(error) = migration.extend(operations) {
1468					migration_error = Some(error);
1469					return false;
1470				}
1471				true
1472			})
1473			.map_err(|e| Error::Db(e.to_string()))?;
1474		if let Some(error) = migration_error.take() {
1475			return Err(error);
1476		}
1477
1478		self.db
1479			.iter_column_while(col::EXPIRED, |item| {
1480				if let Ok((hash, timestamp)) = <(Hash, u64)>::decode(&mut item.value.as_slice()) {
1481					log::trace!(
1482						target: LOG_TARGET,
1483						"Statement loaded (expired): {:?}",
1484						HexDisplay::from(&hash)
1485					);
1486					let purge_at = timestamp.saturating_add(purge_after_sec);
1487					let operation = DbOperation {
1488						column: col::INDEX_EVICTED,
1489						key: evicted_index_key(purge_at, &hash),
1490						value: Some(INDEX_EMPTY_VALUE.to_vec()),
1491					};
1492					if let Err(error) = migration.push(operation) {
1493						migration_error = Some(error);
1494						return false;
1495					}
1496				}
1497				true
1498			})
1499			.map_err(|e| Error::Db(e.to_string()))?;
1500		if let Some(error) = migration_error {
1501			return Err(error);
1502		}
1503
1504		migration.push(SubmitIndex::counters_op(statement_count, total_size, next_seq).into())?;
1505		let migrated_entries = migration.finish()?;
1506		self.db
1507			.commit([(
1508				col::META,
1509				KEY_VERSION.to_vec(),
1510				Some(CURRENT_VERSION.to_le_bytes().to_vec()),
1511			)])
1512			.map_err(|e| Error::Db(e.to_string()))?;
1513		log::info!(
1514			target: LOG_TARGET,
1515			"Migrated the statement store index to the on-disk format ({} rows)",
1516			migrated_entries
1517		);
1518		Ok(())
1519	}
1520
1521	/// Counts, for every distinct prefix (the key minus its trailing 32-byte hash), the number of
1522	/// entries in a btree index column. The keys iterate in order, so each prefix's entries form
1523	/// one contiguous run.
1524	fn count_index_prefixes(&self, column: u8) -> Result<Vec<(Vec<u8>, usize)>> {
1525		let mut counts: Vec<(Vec<u8>, usize)> = Vec::new();
1526		let mut iter = self.db.iter(column).map_err(|e| Error::Db(e.to_string()))?;
1527		iter.seek_to_first().map_err(|e| Error::Db(e.to_string()))?;
1528		while let Some((key, _)) = iter.next().map_err(|e| Error::Db(e.to_string()))? {
1529			let Some(prefix) = prefix_from_index_key(&key) else { continue };
1530			match counts.last_mut() {
1531				Some((last, count)) if last.as_slice() == prefix => *count += 1,
1532				_ => counts.push((prefix.to_vec(), 1)),
1533			}
1534		}
1535		Ok(counts)
1536	}
1537
1538	/// First account at or after `from` in the on-disk per-account index. Corrupt keys are skipped
1539	/// rather than treated as the end of the index: they must not disable allowance enforcement
1540	/// for the accounts sorted after them.
1541	fn next_account_from(&self, from: &AccountId) -> Result<Option<AccountId>> {
1542		let mut iter = self.db.iter(col::INDEX_BY_ACCOUNT).map_err(|e| Error::Db(e.to_string()))?;
1543		iter.seek(&from[..]).map_err(|e| Error::Db(e.to_string()))?;
1544		while let Some((key, _)) = iter.next().map_err(|e| Error::Db(e.to_string()))? {
1545			if let Some((account, _, _)) = parse_account_index_key(&key) {
1546				return Ok(Some(account));
1547			}
1548		}
1549		Ok(None)
1550	}
1551
1552	/// Number of accounts with at least one stored statement, counted by hopping over the
1553	/// distinct account prefixes of the on-disk per-account index.
1554	fn count_accounts(&self) -> Result<usize> {
1555		let mut count = 0usize;
1556		let mut cursor = [0u8; 32];
1557		while let Some(account) = self.next_account_from(&cursor)? {
1558			count += 1;
1559			match next_account_id(account) {
1560				Some(next) => cursor = next,
1561				None => break,
1562			}
1563		}
1564		Ok(count)
1565	}
1566
1567	/// Reads every per-account index row of `account`, in priority order.
1568	fn load_account_entries(
1569		&self,
1570		account: &AccountId,
1571	) -> Result<Vec<(PriorityKey, EntryDetails)>> {
1572		let mut entries = Vec::new();
1573		let mut iter = self.db.iter(col::INDEX_BY_ACCOUNT).map_err(|e| Error::Db(e.to_string()))?;
1574		iter.seek(&account[..]).map_err(|e| Error::Db(e.to_string()))?;
1575		while let Some((key, value)) = iter.next().map_err(|e| Error::Db(e.to_string()))? {
1576			if !key.starts_with(&account[..]) {
1577				break;
1578			}
1579			let Some((_, expiry, hash)) = parse_account_index_key(&key) else { continue };
1580			let Ok(details) = EntryDetails::decode(&mut value.as_slice()) else {
1581				log::error!(
1582					target: LOG_TARGET,
1583					"Corrupt account index row for statement {:?}",
1584					HexDisplay::from(&hash)
1585				);
1586				continue;
1587			};
1588			entries.push((PriorityKey { hash, expiry }, details));
1589		}
1590		Ok(entries)
1591	}
1592
1593	/// Assembles an account's in-memory record from its on-disk index rows.
1594	fn load_account_record(&self, account: &AccountId) -> Result<StatementsForAccount> {
1595		let mut record = StatementsForAccount::default();
1596		for (key, details) in self.load_account_entries(account)? {
1597			record.insert_entry(key.hash, key.expiry, details);
1598		}
1599		Ok(record)
1600	}
1601
1602	/// Scans an on-disk btree index column for every hash whose key starts with `prefix`.
1603	fn scan_index_prefix(&self, column: u8, prefix: &[u8]) -> Result<HashSet<Hash>> {
1604		let mut set = HashSet::new();
1605		let mut iter = self.db.iter(column).map_err(|e| Error::Db(e.to_string()))?;
1606		iter.seek(prefix).map_err(|e| Error::Db(e.to_string()))?;
1607		while let Some((key, _)) = iter.next().map_err(|e| Error::Db(e.to_string()))? {
1608			if !key.starts_with(prefix) {
1609				break;
1610			}
1611			if let Some(hash) = hash_from_index_key(&key) {
1612				set.insert(hash);
1613			}
1614		}
1615		Ok(set)
1616	}
1617
1618	/// Enumerates the hashes of all active statements. Each statement has exactly one entry in
1619	/// [`col::INDEX_BY_DEC_KEY`], so scanning that column's keys yields every hash exactly once,
1620	/// without decoding any bodies.
1621	fn enumerate_hashes(&self) -> Result<Vec<Hash>> {
1622		let mut hashes = Vec::new();
1623		let mut iter = self.db.iter(col::INDEX_BY_DEC_KEY).map_err(|e| Error::Db(e.to_string()))?;
1624		iter.seek_to_first().map_err(|e| Error::Db(e.to_string()))?;
1625		while let Some((key, _)) = iter.next().map_err(|e| Error::Db(e.to_string()))? {
1626			if let Some(hash) = hash_from_index_key(&key) {
1627				hashes.push(hash);
1628			}
1629		}
1630		Ok(hashes)
1631	}
1632
1633	/// Tests, against the on-disk column, whether `hash` belongs to an index set.
1634	fn index_set_contains(&self, set: &IndexSet, hash: &Hash) -> Result<bool> {
1635		Ok(self
1636			.db
1637			.get_size(set.column(), &set.member_key(hash))
1638			.map_err(|e| Error::Db(e.to_string()))?
1639			.is_some())
1640	}
1641
1642	/// Whether `hash` is present in every one of `sets`, tested against the on-disk index. Used to
1643	/// intersect a materialised candidate set with the remaining topic / decryption-key sets.
1644	fn hash_in_all_sets(&self, hash: &Hash, sets: &[IndexSet]) -> Result<bool> {
1645		sets.iter().try_fold(true, |acc, set| {
1646			Ok::<bool, Error>(acc && self.index_set_contains(set, hash)?)
1647		})
1648	}
1649
1650	/// Enumerates matching hashes for `key` / `topic`, reading candidates directly from the on-disk
1651	/// index for every filter kind. Used both for ad-hoc reads and for subscription snapshots.
1652	fn iterate_with(
1653		&self,
1654		key: Option<DecryptionKey>,
1655		topic_filter: &OptimizedTopicFilter,
1656		f: impl FnMut(&Hash) -> Result<()>,
1657	) -> Result<()> {
1658		match topic_filter {
1659			OptimizedTopicFilter::Any => self.iterate_with_any(key, f),
1660			OptimizedTopicFilter::MatchAll(topics) => self.iterate_with_match_all(key, topics, f),
1661			OptimizedTopicFilter::MatchAny(topics) => self.iterate_with_match_any(key, topics, f),
1662		}
1663	}
1664
1665	/// Streams every hash for a decryption key directly from disk.
1666	fn iterate_with_any(
1667		&self,
1668		key: Option<DecryptionKey>,
1669		mut f: impl FnMut(&Hash) -> Result<()>,
1670	) -> Result<()> {
1671		let prefix = IndexSet::DecKey(key).prefix();
1672		let mut iter = self.db.iter(col::INDEX_BY_DEC_KEY).map_err(|e| Error::Db(e.to_string()))?;
1673		iter.seek(&prefix).map_err(|e| Error::Db(e.to_string()))?;
1674		while let Some((k, _)) = iter.next().map_err(|e| Error::Db(e.to_string()))? {
1675			if !k.starts_with(&prefix) {
1676				break;
1677			}
1678			if let Some(hash) = hash_from_index_key(&k) {
1679				f(&hash)?;
1680			}
1681		}
1682		Ok(())
1683	}
1684
1685	/// For each requested topic, streams its hashes from disk and yields those that also belong to
1686	/// the decryption-key set. A hash carrying several requested topics is yielded once per topic,
1687	/// matching the in-memory behaviour callers already tolerate.
1688	fn iterate_with_match_any(
1689		&self,
1690		key: Option<DecryptionKey>,
1691		topics: &HashSet<Topic>,
1692		mut f: impl FnMut(&Hash) -> Result<()>,
1693	) -> Result<()> {
1694		let key_set = IndexSet::DecKey(key);
1695		for topic in topics {
1696			let prefix = topic[..].to_vec();
1697			let mut iter =
1698				self.db.iter(col::INDEX_BY_TOPIC).map_err(|e| Error::Db(e.to_string()))?;
1699			iter.seek(&prefix).map_err(|e| Error::Db(e.to_string()))?;
1700			while let Some((k, _)) = iter.next().map_err(|e| Error::Db(e.to_string()))? {
1701				if !k.starts_with(&prefix) {
1702					break;
1703				}
1704				if let Some(hash) = hash_from_index_key(&k) {
1705					if self.index_set_contains(&key_set, &hash)? {
1706						f(&hash)?;
1707					}
1708				}
1709			}
1710		}
1711		Ok(())
1712	}
1713
1714	/// Intersects the decryption-key set with all requested topic sets, reading candidates directly
1715	/// from the on-disk index. The lock is taken only briefly to order the sets by cardinality so
1716	/// the smallest is materialised first; this ordering is a best-effort hint (stale counters can
1717	/// only misorder the sets, never drop a statement). Materialising that set and probing the rest
1718	/// against disk then happen without holding the lock.
1719	fn iterate_with_match_all(
1720		&self,
1721		key: Option<DecryptionKey>,
1722		topics: &HashSet<Topic>,
1723		mut f: impl FnMut(&Hash) -> Result<()>,
1724	) -> Result<()> {
1725		if topics.len() > MAX_TOPICS {
1726			return Ok(());
1727		}
1728		let mut sets = Vec::with_capacity(topics.len() + 1);
1729		sets.push(IndexSet::DecKey(key));
1730		for topic in topics {
1731			sets.push(IndexSet::Topic(*topic));
1732		}
1733		{
1734			// Ordering only (best-effort): stale counters can misorder but never drop a statement.
1735			let query_index = self.query_index.read();
1736			sets.sort_by_key(|s| s.len(&query_index));
1737		}
1738		let smallest = self.scan_index_prefix(sets[0].column(), &sets[0].prefix())?;
1739		let others = &sets[1..];
1740		for hash in &smallest {
1741			if self.hash_in_all_sets(hash, others)? {
1742				log::trace!(
1743					target: LOG_TARGET,
1744					"Iterating by topic/key: statement {:?}",
1745					HexDisplay::from(hash)
1746				);
1747				f(hash)?;
1748			}
1749		}
1750		Ok(())
1751	}
1752
1753	/// Reads the raw SCALE-encoded body of `hash` from `col::STATEMENTS`, or `None` if it is absent
1754	/// (a benign DB race: the statement was removed concurrently). The stored value is exactly
1755	/// `statement.encode()`, so it can be forwarded verbatim.
1756	fn read_statement_encoded(&self, hash: &Hash) -> Result<Option<Vec<u8>>> {
1757		match self.db.get(col::STATEMENTS, hash).map_err(|e| Error::Db(e.to_string()))? {
1758			Some(entry) => Ok(Some(entry)),
1759			None => {
1760				log::debug!(target: LOG_TARGET, "Missing statement {:?}", HexDisplay::from(hash));
1761				Ok(None)
1762			},
1763		}
1764	}
1765
1766	/// Reads and decodes the statement `hash`, returning `None` if it is absent or its stored body
1767	/// fails to decode (a corrupt DB row, which is logged and skipped).
1768	fn read_statement(&self, hash: &Hash) -> Result<Option<Statement>> {
1769		let Some(entry) = self.read_statement_encoded(hash)? else { return Ok(None) };
1770		match Statement::decode(&mut entry.as_slice()) {
1771			Ok(statement) => Ok(Some(statement)),
1772			Err(_) => {
1773				log::error!(target: LOG_TARGET, "Corrupt statement {:?}", HexDisplay::from(hash));
1774				Ok(None)
1775			},
1776		}
1777	}
1778
1779	/// Collects statements matching `key` / `topic_filter`. Reads never hold the query-index lock
1780	/// across disk I/O: `Any` / `MatchAny` touch only the (thread-safe) database, and `MatchAll`
1781	/// takes the lock only momentarily to order the candidate sets.
1782	fn collect_statements<R>(
1783		&self,
1784		key: Option<DecryptionKey>,
1785		topic_filter: &OptimizedTopicFilter,
1786		mut f: impl FnMut(Statement) -> Option<R>,
1787	) -> Result<Vec<R>> {
1788		let mut result = Vec::new();
1789		self.iterate_with(key, topic_filter, |hash| {
1790			if let Some(statement) = self.read_statement(hash)? {
1791				if let Some(data) = f(statement) {
1792					result.push(data);
1793				}
1794			}
1795			Ok(())
1796		})?;
1797		Ok(result)
1798	}
1799
1800	/// Evicts the lowest-priority statements of `account` while it exceeds its on-chain
1801	/// allowance, spending at most `budget` evictions. Statements already past their expiry are
1802	/// neither counted against the allowance nor evicted here — the expiry sweep reaps them.
1803	fn enforce_account_allowance(
1804		&self,
1805		account: &AccountId,
1806		current_time: u64,
1807		budget: &mut usize,
1808	) {
1809		let allowance = match (self.read_allowance_fn)(account, AllowanceBlock::Finalized) {
1810			Ok(Some(allowance)) => allowance,
1811			Ok(None) => {
1812				log::debug!(
1813					target: LOG_TARGET,
1814					"No allowance found for account {:?}, treating as zero allowance",
1815					HexDisplay::from(account)
1816				);
1817				StatementAllowance { max_count: 0, max_size: 0 }
1818			},
1819			Err(e) => {
1820				log::error!(target: LOG_TARGET, "Error reading allowance: {:?}", e);
1821				// Skip allowance enforcement for this account on error
1822				return;
1823			},
1824		};
1825		let (max_count, max_size) = (allowance.max_count as usize, allowance.max_size as usize);
1826
1827		// A cached summary proving the account within its allowance saves the disk scan. The
1828		// summary also counts expired-but-unswept statements, so it can only overestimate usage,
1829		// which is fine for a within-limit conclusion; the over-limit path recounts from disk.
1830		if let Some(summary) = self.submit_index.read().summaries.peek(account) {
1831			if summary.count <= max_count && summary.data_size <= max_size {
1832				return;
1833			}
1834		}
1835
1836		let entries = match self.load_account_entries(account) {
1837			Ok(entries) => entries,
1838			Err(e) => {
1839				log::warn!(target: LOG_TARGET, "Error reading the account index: {:?}", e);
1840				return;
1841			},
1842		};
1843		let expiry_bound = Expiry(current_time << 32);
1844		let mut remaining_count = 0usize;
1845		let mut remaining_size = 0usize;
1846		for (key, details) in &entries {
1847			if key.expiry >= expiry_bound {
1848				remaining_count += 1;
1849				remaining_size += details.data_len;
1850			}
1851		}
1852		if remaining_count <= max_count && remaining_size <= max_size {
1853			return;
1854		}
1855		log::debug!(
1856			target: LOG_TARGET,
1857			"Account {:?} exceeds allowance: count={}/{}, size={}/{}",
1858			HexDisplay::from(account),
1859			remaining_count,
1860			allowance.max_count,
1861			remaining_size,
1862			allowance.max_size
1863		);
1864
1865		// Evict lowest priority statements that exceed allowance
1866		for (key, details) in &entries {
1867			if (remaining_count <= max_count && remaining_size <= max_size) || *budget == 0 {
1868				break;
1869			}
1870			if key.expiry < expiry_bound {
1871				continue;
1872			}
1873			log::debug!(
1874				target: LOG_TARGET,
1875				"Evicting statement {:?} due to allowance enforcement",
1876				HexDisplay::from(&key.hash)
1877			);
1878			if let Err(e) = self.remove(&key.hash) {
1879				log::debug!(
1880					target: LOG_TARGET,
1881					"Error evicting statement {:?}: {:?}",
1882					HexDisplay::from(&key.hash),
1883					e
1884				);
1885			} else {
1886				*budget -= 1;
1887				remaining_count -= 1;
1888				remaining_size -= details.data_len;
1889			}
1890		}
1891	}
1892
1893	// Reaps expired statements and enforces per-account allowances against the on-disk index.
1894	//
1895	// Expired statements form a prefix of the global expiry index ([`col::INDEX_BY_EXPIRY`]), so
1896	// the sweep costs time proportional to the number of due statements, not to the store size.
1897	// Allowance enforcement then walks the accounts of [`col::INDEX_BY_ACCOUNT`] incrementally,
1898	// resuming from where the previous call stopped (`allowance_cursor`), so the process
1899	// eventually covers all accounts across multiple invocations.
1900	//
1901	// The two phases are budgeted independently: each gets its own
1902	// `MAX_EXPIRY_STATEMENTS_PER_ITERATION` statement budget and its own
1903	// `MAX_EXPIRY_TIME_PER_ITERATION` time slice, and allowance enforcement is additionally
1904	// bounded by `MAX_EXPIRY_ACCOUNTS_PER_ITERATION` accounts checked. The isolation matters:
1905	// with shared budgets, a store whose expiry inflow persistently exceeds the expiry budget
1906	// (a full store with a short TTL) would never get an allowance pass at all.
1907	//
1908	// Statements are considered expired when their expiry (which encodes the expiration
1909	// timestamp in the upper 32 bits) is less than the current timestamp.
1910	/// Reap expired statements and enforce per-account allowances (one bounded pass); runs
1911	/// periodically from the background task. Hidden: exposed only for the benchmarks.
1912	#[doc(hidden)]
1913	pub fn enforce_limits(&self) {
1914		self.enforce_limits_bounded(
1915			MAX_EXPIRY_STATEMENTS_PER_ITERATION,
1916			MAX_EXPIRY_ACCOUNTS_PER_ITERATION,
1917			MAX_EXPIRY_TIME_PER_ITERATION,
1918		)
1919	}
1920
1921	/// Body of [`Self::enforce_limits`] with the per-call bounds as parameters, letting the
1922	/// tests drive the incremental sweep with budgets small enough to need several calls.
1923	/// `statement_budget` and `time_budget` apply to each phase separately.
1924	fn enforce_limits_bounded(
1925		&self,
1926		statement_budget: usize,
1927		account_budget: usize,
1928		time_budget: Duration,
1929	) {
1930		let _start_check_expiration_timer = self.metrics.start_check_expiration_timer();
1931		let current_time = self.timestamp();
1932		let start = Instant::now();
1933		let mut expired = 0u64;
1934
1935		// Phase 1: reap statements past their expiry, straight off the expiry index.
1936		let mut due = Vec::new();
1937		let scan = (|| -> Result<()> {
1938			let mut iter =
1939				self.db.iter(col::INDEX_BY_EXPIRY).map_err(|e| Error::Db(e.to_string()))?;
1940			iter.seek_to_first().map_err(|e| Error::Db(e.to_string()))?;
1941			while let Some((key, _)) = iter.next().map_err(|e| Error::Db(e.to_string()))? {
1942				let Some((expiry, hash)) = parse_time_index_key(&key) else { continue };
1943				if expiry >= (current_time << 32) {
1944					// Entries are ordered by expiry, so nothing further is due.
1945					break;
1946				}
1947				due.push((expiry, hash));
1948				if due.len() >= statement_budget || start.elapsed() >= time_budget {
1949					break;
1950				}
1951			}
1952			Ok(())
1953		})();
1954		if let Err(e) = scan {
1955			log::warn!(target: LOG_TARGET, "Error scanning the expiry index: {:?}", e);
1956		}
1957		for (expiry, hash) in due {
1958			match self.remove_statement(&hash) {
1959				Ok(true) => {
1960					expired += 1;
1961					log::trace!(
1962						target: LOG_TARGET,
1963						"Marked statement {:?} as expired",
1964						HexDisplay::from(&hash)
1965					);
1966				},
1967				// The row survived its statement: either a concurrent removal won the race (and
1968				// deleted the row along with the body), or the store is inconsistent.
1969				Ok(false) => self.report_orphan_expiry_row(expiry, &hash),
1970				Err(e) => {
1971					log::debug!(
1972						target: LOG_TARGET,
1973						"Error marking statement {:?} as expired: {:?}",
1974						HexDisplay::from(&hash),
1975						e
1976					);
1977				},
1978			}
1979		}
1980
1981		// Phase 2: enforce allowances account by account, resuming from the cursor. The phase
1982		// runs on its own statement and time budgets: with shared ones, a sustained expiry
1983		// backlog exhausting phase 1's budget on every pass would starve allowance enforcement
1984		// indefinitely.
1985		let allowance_start = Instant::now();
1986		let mut allowance_budget = statement_budget;
1987		let mut cursor = self.submit_index.read().allowance_cursor;
1988		let mut checked = 0usize;
1989		let mut wrapped = false;
1990		while checked < account_budget &&
1991			allowance_budget > 0 &&
1992			allowance_start.elapsed() < time_budget
1993		{
1994			let from = cursor.unwrap_or([0u8; 32]);
1995			let account = match self.next_account_from(&from) {
1996				Ok(Some(account)) => account,
1997				Ok(None) => {
1998					wrapped = true;
1999					break;
2000				},
2001				Err(e) => {
2002					log::warn!(target: LOG_TARGET, "Error reading the account index: {:?}", e);
2003					break;
2004				},
2005			};
2006			checked += 1;
2007			let budget_before = allowance_budget;
2008			self.enforce_account_allowance(&account, current_time, &mut allowance_budget);
2009			expired += (budget_before - allowance_budget) as u64;
2010			cursor = match next_account_id(account) {
2011				next @ Some(_) => next,
2012				None => {
2013					wrapped = true;
2014					break;
2015				},
2016			};
2017		}
2018		{
2019			let mut submit_index = self.submit_index.write();
2020			submit_index.allowance_cycle_seen += checked;
2021			if wrapped {
2022				// A full pass over the account index just completed; it is the only place where
2023				// the total number of accounts is (re)counted.
2024				self.known_accounts_count
2025					.store(submit_index.allowance_cycle_seen, AtomicOrdering::Relaxed);
2026				submit_index.allowance_cycle_seen = 0;
2027				submit_index.allowance_cursor = None;
2028			} else {
2029				submit_index.allowance_cursor = cursor;
2030			}
2031		}
2032
2033		drop(_start_check_expiration_timer);
2034
2035		self.metrics.report(|metrics| {
2036			metrics.statements_expired_total.inc_by(expired);
2037		});
2038	}
2039
2040	/// Drains the on-disk evicted journal of entries whose purge deadline has passed. Returns how
2041	/// many entries were drained.
2042	fn drain_due_evicted(&self, current_time: u64) -> Result<usize> {
2043		let mut commit: Vec<(u8, Vec<u8>, Option<Vec<u8>>)> = Vec::new();
2044		let mut drained = 0usize;
2045		{
2046			let mut iter =
2047				self.db.iter(col::INDEX_EVICTED).map_err(|e| Error::Db(e.to_string()))?;
2048			iter.seek_to_first().map_err(|e| Error::Db(e.to_string()))?;
2049			loop {
2050				let Some((key, _)) = iter.next().map_err(|e| Error::Db(e.to_string()))? else {
2051					break;
2052				};
2053				let Some((purge_at, hash)) = parse_time_index_key(&key) else { continue };
2054				if purge_at > current_time {
2055					// Entries are ordered by purge time, so nothing further is due.
2056					break;
2057				}
2058				commit.push((col::EXPIRED, hash.to_vec(), None));
2059				commit.push((col::INDEX_EVICTED, key, None));
2060				drained += 1;
2061			}
2062		}
2063		if !commit.is_empty() {
2064			self.db.commit(commit).map_err(|e| Error::Db(e.to_string()))?;
2065		}
2066		Ok(drained)
2067	}
2068
2069	/// Perform periodic store maintenance: permanently delete statements whose purge period has
2070	/// elapsed and refresh store metrics.
2071	///
2072	/// Expired and evicted statements are not removed from the database immediately; they are kept
2073	/// in the `EXPIRED` column for [`DEFAULT_PURGE_AFTER_SEC`] (default 48h) to prevent
2074	/// re-acceptance while they may still be propagating over gossip. This method removes those
2075	/// whose purge period has passed.
2076	///
2077	/// Runs in a background task on a fixed interval (`MAINTENANCE_PERIOD`, 29s). Enforcing
2078	/// per-account and global limits — expiring over-quota statements — is handled separately by
2079	/// `enforce_limits` on its own interval (`ENFORCE_LIMITS_PERIOD`, 31s), kept distinct to avoid
2080	/// holding the index lock for too long during maintenance.
2081	pub fn maintain(&self) {
2082		log::trace!(target: LOG_TARGET, "Started store maintenance");
2083		let current_time = self.timestamp();
2084		let deleted_count = match self.drain_due_evicted(current_time) {
2085			Ok(count) => count as u64,
2086			Err(e) => {
2087				log::warn!(target: LOG_TARGET, "Error writing to the statement database: {:?}", e);
2088				0
2089			},
2090		};
2091
2092		let (active_count, expired_count, total_size, capacity_statements, capacity_bytes) = {
2093			let mut submit_index = self.submit_index.write();
2094			submit_index.evicted_count =
2095				submit_index.evicted_count.saturating_sub(deleted_count as usize);
2096			(
2097				submit_index.statement_count,
2098				submit_index.evicted_count,
2099				submit_index.total_size,
2100				submit_index.config.max_total_statements,
2101				submit_index.config.max_total_size,
2102			)
2103		};
2104		let accounts_count = self.known_accounts_count.load(AtomicOrdering::Relaxed);
2105
2106		if deleted_count > 0 {
2107			self.metrics.report(|metrics| metrics.statements_pruned.inc_by(deleted_count));
2108		}
2109
2110		self.metrics.report(|metrics| {
2111			metrics.statements_total.set(active_count as u64);
2112			metrics.bytes_total.set(total_size as u64);
2113			metrics.accounts_total.set(accounts_count as u64);
2114			metrics.expired_total.set(expired_count as u64);
2115			metrics.capacity_statements.set(capacity_statements as u64);
2116			metrics.capacity_bytes.set(capacity_bytes as u64);
2117		});
2118
2119		log::trace!(
2120			target: LOG_TARGET,
2121			"Completed store maintenance. Purged: {}, Active: {}, Expired: {}",
2122			deleted_count,
2123			active_count,
2124			expired_count
2125		);
2126	}
2127
2128	fn timestamp(&self) -> u64 {
2129		self.time_override.unwrap_or_else(|| {
2130			std::time::SystemTime::now()
2131				.duration_since(std::time::UNIX_EPOCH)
2132				.unwrap_or_default()
2133				.as_secs()
2134		})
2135	}
2136
2137	#[cfg(test)]
2138	fn set_time(&mut self, time: u64) {
2139		self.time_override = Some(time);
2140	}
2141
2142	/// Returns `self` as [`StatementStoreExt`].
2143	pub fn as_statement_store_ext(self: Arc<Self>) -> StatementStoreExt {
2144		StatementStoreExt::new(self)
2145	}
2146
2147	/// Return information of all known statements whose decryption key is identified as
2148	/// `dest`. The key must be available to the client.
2149	fn posted_clear_inner<R>(
2150		&self,
2151		match_all_topics: &[Topic],
2152		dest: [u8; 32],
2153		// Map the statement and the decrypted data to the desired result.
2154		mut map_f: impl FnMut(Statement, Vec<u8>) -> R,
2155	) -> Result<Vec<R>> {
2156		self.collect_statements(
2157			Some(dest),
2158			&OptimizedTopicFilter::MatchAll(match_all_topics.iter().cloned().collect()),
2159			|statement| {
2160				if let (Some(key), Some(_)) = (statement.decryption_key(), statement.data()) {
2161					let public: sp_core::ed25519::Public = UncheckedFrom::unchecked_from(key);
2162					let public: sp_statement_store::ed25519::Public = public.into();
2163					match self.keystore.key_pair::<sp_statement_store::ed25519::Pair>(&public) {
2164						Err(e) => {
2165							log::debug!(
2166								target: LOG_TARGET,
2167								"Keystore error: {:?}, for statement {:?}",
2168								e,
2169								HexDisplay::from(&statement.hash())
2170							);
2171							None
2172						},
2173						Ok(None) => {
2174							log::debug!(
2175								target: LOG_TARGET,
2176								"Keystore is missing key for statement {:?}",
2177								HexDisplay::from(&statement.hash())
2178							);
2179							None
2180						},
2181						Ok(Some(pair)) => match statement.decrypt_private(&pair.into_inner()) {
2182							Ok(r) => r.map(|data| map_f(statement, data)),
2183							Err(e) => {
2184								log::debug!(
2185									target: LOG_TARGET,
2186									"Decryption error: {:?}, for statement {:?}",
2187									e,
2188									HexDisplay::from(&statement.hash())
2189								);
2190								None
2191							},
2192						},
2193					}
2194				} else {
2195					None
2196				}
2197			},
2198		)
2199	}
2200}
2201
2202impl StatementStore for Store {
2203	/// Return every statement currently in the store.
2204	///
2205	/// Takes a read lock on the query index, iterates all indexed hashes, reads and SCALE-decodes
2206	/// each statement from the `STATEMENTS` database column, and skips any entry that fails to
2207	/// decode.
2208	fn statements(&self) -> Result<Vec<(Hash, Statement)>> {
2209		let hashes = self.enumerate_hashes()?;
2210		let mut result = Vec::with_capacity(hashes.len());
2211		for hash in hashes {
2212			let Some(encoded) =
2213				self.db.get(col::STATEMENTS, &hash).map_err(|e| Error::Db(e.to_string()))?
2214			else {
2215				continue;
2216			};
2217			match Statement::decode(&mut encoded.as_slice()) {
2218				Ok(statement) => result.push((hash, statement)),
2219				Err(_) => log::error!(
2220					target: LOG_TARGET,
2221					"Corrupt statement {:?}",
2222					HexDisplay::from(&hash)
2223				),
2224			}
2225		}
2226		Ok(result)
2227	}
2228
2229	fn take_recent_statements(&self) -> Result<Vec<(u64, Hash, Statement)>> {
2230		let recent = self.query_index.write().take_recent();
2231		let mut result = Vec::with_capacity(recent.len());
2232		for (hash, seq) in recent {
2233			let Some(encoded) =
2234				self.db.get(col::STATEMENTS, &hash).map_err(|e| Error::Db(e.to_string()))?
2235			else {
2236				continue;
2237			};
2238			match Statement::decode(&mut encoded.as_slice()) {
2239				Ok(statement) => result.push((seq, hash, statement)),
2240				Err(_) => log::error!(
2241					target: LOG_TARGET,
2242					"Corrupt statement {:?}",
2243					HexDisplay::from(&hash)
2244				),
2245			}
2246		}
2247		result.sort_unstable_by_key(|(seq, ..)| *seq);
2248		Ok(result)
2249	}
2250
2251	fn admission_watermark(&self) -> Result<u64> {
2252		// Every admission commits its journal row under the submit-index write lock before
2253		// releasing it, so a lock holder observes every sequence number below `next_seq`
2254		// already persisted.
2255		Ok(self.submit_index.read().next_seq)
2256	}
2257
2258	fn admitted_statements(
2259		&self,
2260		mut cursor: u64,
2261		watermark: u64,
2262		scan_limit: usize,
2263		filter: &mut dyn FnMut(&Hash, &[u8], &Statement) -> FilterDecision,
2264	) -> Result<AdmittedBatch> {
2265		// A cursor past the watermark would snap back to it below, moving the caller's
2266		// position backwards.
2267		debug_assert!(
2268			cursor <= watermark,
2269			"admission cursor {cursor} must not exceed the watermark {watermark}"
2270		);
2271		debug_assert!(scan_limit > 0, "a zero scan limit cannot advance the cursor");
2272		let mut statements = Vec::new();
2273		let mut aborted = false;
2274		let mut scanned = 0usize;
2275		let mut iter = self.db.iter(col::ADMISSION_SEQ).map_err(|e| Error::Db(e.to_string()))?;
2276		iter.seek(&cursor.to_be_bytes()).map_err(|e| Error::Db(e.to_string()))?;
2277
2278		while let Some((key, value)) = iter.next().map_err(|e| Error::Db(e.to_string()))? {
2279			let seq = u64::from_be_bytes(
2280				key.try_into().map_err(|_| Error::Db("Invalid admission sequence key".into()))?,
2281			);
2282			if seq >= watermark {
2283				cursor = watermark;
2284				break;
2285			}
2286			if scanned == scan_limit {
2287				aborted = true;
2288				break;
2289			}
2290			scanned += 1;
2291			let hash: Hash = value
2292				.as_slice()
2293				.try_into()
2294				.map_err(|_| Error::Db("Invalid admission sequence hash".into()))?;
2295			let next_cursor = seq.saturating_add(1);
2296			let Some(encoded) =
2297				self.db.get(col::STATEMENTS, &hash).map_err(|e| Error::Db(e.to_string()))?
2298			else {
2299				cursor = next_cursor;
2300				continue;
2301			};
2302			// The admission row is deleted atomically with the statement it admits, so re-reading
2303			// it after the body confirms that this sequence number is still the statement's
2304			// current admission (and not a stale row of an evicted-and-readmitted statement).
2305			let is_current = self
2306				.db
2307				.get(col::ADMISSION_SEQ, &seq.to_be_bytes())
2308				.map_err(|e| Error::Db(e.to_string()))?
2309				.is_some_and(|current| current.as_slice() == hash.as_slice());
2310			if !is_current {
2311				cursor = next_cursor;
2312				continue;
2313			}
2314			let statement = match Statement::decode(&mut encoded.as_slice()) {
2315				Ok(statement) => statement,
2316				Err(e) => {
2317					log::error!(
2318						target: LOG_TARGET,
2319						"Could not decode statement {:?} while walking the admission journal: {:?}",
2320						HexDisplay::from(&hash),
2321						e
2322					);
2323					cursor = next_cursor;
2324					continue;
2325				},
2326			};
2327			match filter(&hash, &encoded, &statement) {
2328				FilterDecision::Skip => cursor = next_cursor,
2329				FilterDecision::Take => {
2330					statements.push((hash, statement));
2331					cursor = next_cursor;
2332				},
2333				FilterDecision::Abort => {
2334					aborted = true;
2335					break;
2336				},
2337			}
2338		}
2339
2340		// The journal can end below the watermark when the newest admissions were removed;
2341		// nothing is left to visit there, so the walk is complete.
2342		if !aborted && cursor < watermark {
2343			cursor = watermark;
2344		}
2345		Ok(AdmittedBatch { statements, cursor, done: cursor >= watermark })
2346	}
2347
2348	/// Read a single statement directly from the `STATEMENTS` database column by hash and decode
2349	/// it. Returns `Ok(None)` if no statement with that hash is stored.
2350	fn statement(&self, hash: &Hash) -> Result<Option<Statement>> {
2351		Ok(
2352			match self
2353				.db
2354				.get(col::STATEMENTS, hash.as_slice())
2355				.map_err(|e| Error::Db(e.to_string()))?
2356			{
2357				Some(entry) => {
2358					log::trace!(
2359						target: LOG_TARGET,
2360						"Queried statement {:?}",
2361						HexDisplay::from(hash)
2362					);
2363					Some(Statement::decode(&mut entry.as_slice()).map_err(|e| {
2364						log::error!(
2365							target: LOG_TARGET,
2366							"Corrupt statement {:?}",
2367							HexDisplay::from(hash)
2368						);
2369						Error::Decode(e.to_string())
2370					})?)
2371				},
2372				None => {
2373					log::trace!(
2374						target: LOG_TARGET,
2375						"Queried missing statement {:?}",
2376						HexDisplay::from(hash)
2377					);
2378					None
2379				},
2380			},
2381		)
2382	}
2383
2384	fn has_statement(&self, hash: &Hash) -> bool {
2385		match self.db.get_size(col::STATEMENTS, hash.as_slice()) {
2386			Ok(size) => size.is_some(),
2387			Err(e) => {
2388				log::debug!(
2389					target: LOG_TARGET,
2390					"Error checking statement presence {:?}: {:?}",
2391					HexDisplay::from(hash),
2392					e
2393				);
2394				false
2395			},
2396		}
2397	}
2398
2399	fn statements_by_hashes(
2400		&self,
2401		hashes: &[Hash],
2402		filter: &mut dyn FnMut(&Hash, &[u8], &Statement) -> FilterDecision,
2403	) -> Result<(Vec<(Hash, Statement)>, usize)> {
2404		let mut result = Vec::new();
2405		let mut processed = 0;
2406		for hash in hashes {
2407			processed += 1;
2408			let Some(encoded) =
2409				self.db.get(col::STATEMENTS, hash).map_err(|e| Error::Db(e.to_string()))?
2410			else {
2411				continue;
2412			};
2413			let Ok(statement) = Statement::decode(&mut encoded.as_slice()) else {
2414				log::error!(target: LOG_TARGET, "Corrupt statement {:?}", HexDisplay::from(hash));
2415				continue;
2416			};
2417			match filter(hash, &encoded, &statement) {
2418				FilterDecision::Skip => {},
2419				FilterDecision::Take => {
2420					result.push((*hash, statement));
2421				},
2422				FilterDecision::Abort => {
2423					// We did not process it :)
2424					processed -= 1;
2425					break;
2426				},
2427			}
2428		}
2429
2430		Ok((result, processed))
2431	}
2432
2433	/// Return the `data` of all statements matching all of `match_all_topics` that have no
2434	/// decryption key (i.e. public broadcasts).
2435	///
2436	/// Filters the query index by topic (intersection; an empty list matches every broadcast),
2437	/// reads and decodes each match from the `STATEMENTS` column, and returns the plaintext data,
2438	/// skipping any inconsistent entries.
2439	fn broadcasts(&self, match_all_topics: &[Topic]) -> Result<Vec<Vec<u8>>> {
2440		self.collect_statements(
2441			None,
2442			&OptimizedTopicFilter::MatchAll(match_all_topics.iter().cloned().collect()),
2443			|statement| statement.into_data(),
2444		)
2445	}
2446
2447	/// Return the (encrypted) `data` of all statements matching all of `match_all_topics` whose
2448	/// decryption key equals `dest`.
2449	///
2450	/// Same filtering and DB read as [`broadcasts`](Self::broadcasts), but keyed on `dest` rather
2451	/// than the absence of a decryption key.
2452	fn posted(&self, match_all_topics: &[Topic], dest: [u8; 32]) -> Result<Vec<Vec<u8>>> {
2453		self.collect_statements(
2454			Some(dest),
2455			&OptimizedTopicFilter::MatchAll(match_all_topics.iter().cloned().collect()),
2456			|statement| statement.into_data(),
2457		)
2458	}
2459
2460	/// Like [`posted`](Self::posted) but returns the decrypted data.
2461	///
2462	/// For each match, looks up the ed25519 key identified by `dest` in the keystore and decrypts
2463	/// the statement data; statements are skipped when the key is unavailable or decryption fails.
2464	fn posted_clear(&self, match_all_topics: &[Topic], dest: [u8; 32]) -> Result<Vec<Vec<u8>>> {
2465		self.posted_clear_inner(match_all_topics, dest, |_statement, data| data)
2466	}
2467
2468	/// Return the full SCALE-encoded statements matching all of `match_all_topics` that have no
2469	/// decryption key (i.e. public broadcasts).
2470	///
2471	/// Takes a read lock on the query index and filters by the absence of a decryption key and
2472	/// topics (intersection / AND — an empty topic list matches every broadcast), then reads,
2473	/// decodes and re-encodes each match from the `STATEMENTS` column, skipping inconsistent
2474	/// entries. Unlike [`broadcasts`](Self::broadcasts), which returns only the data, this returns
2475	/// the whole statement.
2476	fn broadcasts_stmt(&self, match_all_topics: &[Topic]) -> Result<Vec<Vec<u8>>> {
2477		self.collect_statements(
2478			None,
2479			&OptimizedTopicFilter::MatchAll(match_all_topics.iter().cloned().collect()),
2480			|statement| Some(statement.encode()),
2481		)
2482	}
2483
2484	/// Return the full SCALE-encoded statements matching all of `match_all_topics` whose decryption
2485	/// key equals `dest`.
2486	///
2487	/// Takes a read lock on the query index and filters by decryption key (`dest`) and topics
2488	/// (intersection / AND — an empty topic list matches every statement keyed to `dest`), then
2489	/// reads, decodes and re-encodes each match from the `STATEMENTS` column, skipping inconsistent
2490	/// entries. Unlike [`posted`](Self::posted), which returns only the (still-encrypted) data,
2491	/// this returns the whole statement.
2492	fn posted_stmt(&self, match_all_topics: &[Topic], dest: [u8; 32]) -> Result<Vec<Vec<u8>>> {
2493		self.collect_statements(
2494			Some(dest),
2495			&OptimizedTopicFilter::MatchAll(match_all_topics.iter().cloned().collect()),
2496			|statement| Some(statement.encode()),
2497		)
2498	}
2499
2500	/// Return, for each statement matching all of `match_all_topics` whose decryption key equals
2501	/// `dest`, the SCALE-encoded statement concatenated with its decrypted data.
2502	///
2503	/// Filters as [`posted_stmt`](Self::posted_stmt), then for each match looks up the ed25519 key
2504	/// identified by `dest` in the keystore and decrypts the statement data, appending the
2505	/// plaintext to the encoded statement. Statements are skipped when the key is unavailable or
2506	/// decryption fails.
2507	fn posted_clear_stmt(
2508		&self,
2509		match_all_topics: &[Topic],
2510		dest: [u8; 32],
2511	) -> Result<Vec<Vec<u8>>> {
2512		self.posted_clear_inner(match_all_topics, dest, |statement, data| {
2513			let mut res = Vec::with_capacity(statement.size_hint() + data.len());
2514			statement.encode_to(&mut res);
2515			res.extend_from_slice(&data);
2516			res
2517		})
2518	}
2519
2520	/// Submit a statement to the store, validating it and enforcing constraints.
2521	///
2522	/// Runs the following pipeline, short-circuiting on the first failure:
2523	/// 1. **Expiry check** — reject if the statement's expiration timestamp is already in the past
2524	///    (`SubmitResult::Invalid(InvalidReason::AlreadyExpired)`).
2525	/// 2. **Encoding size check** — reject if the encoded statement exceeds [`MAX_STATEMENT_SIZE`]
2526	///    (`InvalidReason::EncodingTooLarge`).
2527	/// 3. **Duplicate check** — a statement already in the store is reported `Known`: the hash
2528	///    covers the whole content, so a duplicate carries nothing to renew. A recently evicted
2529	///    statement may be resubmitted (renewed) by `Chain` and `Local` sources but not by
2530	///    `Network` (`SubmitResult::KnownExpired`).
2531	/// 4. **Proof & signature** — extract the account from the proof and verify the signature
2532	///    (`InvalidReason::NoProof` / `InvalidReason::BadProof`).
2533	/// 5. **Allowance** — read the account's allowance (`StatementAllowance`: max count and size)
2534	///    directly from chain state at the best block (via the `statement_allowance_key` storage
2535	///    key — not a runtime call); reject with `SubmitResult::Rejected(NoAllowance)` if none is
2536	///    set. The best block is used for responsiveness; a statement accepted here may later be
2537	///    evicted when limits are enforced against the finalized block.
2538	/// 6. **Constraint check & eviction** — check the account's record, enforcing per-account
2539	///    limits (count, size, one statement per channel, higher priority replaces lower) and
2540	///    global limits ([`DEFAULT_MAX_TOTAL_STATEMENTS`], [`DEFAULT_MAX_TOTAL_SIZE`]), evicting
2541	///    lower-priority statements of the same account as needed (`SubmitResult::Rejected` if it
2542	///    still does not fit).
2543	/// 7. **Persist** — commit the statement, its index rows, any evictions and the refreshed
2544	///    counters atomically, then update the caches and the in-memory query index.
2545	///
2546	/// Returns `SubmitResult::New` on success.
2547	fn submit(&self, statement: Statement, source: StatementSource) -> SubmitResult {
2548		let _histogram_submit_start_timer = self.metrics.start_submit_timer();
2549		let hash = statement.hash();
2550		// Get unix timestamp
2551		if statement.is_expired(self.timestamp()) {
2552			log::debug!(
2553				target: LOG_TARGET,
2554				"Statement is already expired: {:?}",
2555				HexDisplay::from(&hash),
2556			);
2557			let reason = InvalidReason::AlreadyExpired;
2558			self.metrics.report(|metrics| {
2559				metrics.validations_invalid.with_label_values(&[reason.label()]).inc();
2560			});
2561			return SubmitResult::Invalid(reason);
2562		}
2563		let encoded_size = statement.encoded_size();
2564		if encoded_size > MAX_STATEMENT_SIZE {
2565			log::debug!(
2566				target: LOG_TARGET,
2567				"Statement is too big for propogation: {:?} ({}/{} bytes)",
2568				HexDisplay::from(&hash),
2569				statement.encoded_size(),
2570				MAX_STATEMENT_SIZE
2571			);
2572			let reason = InvalidReason::EncodingTooLarge {
2573				submitted_size: encoded_size,
2574				max_size: MAX_STATEMENT_SIZE,
2575			};
2576			self.metrics.report(|metrics| {
2577				metrics.validations_invalid.with_label_values(&[reason.label()]).inc();
2578			});
2579			return SubmitResult::Invalid(reason);
2580		}
2581
2582		// Deduplicate against statements we already store (in-memory submit index) or have recently
2583		// evicted (on-disk evicted journal). Both checks are lock-free; a duplicate slipping
2584		// past this early check is caught again under the write lock below.
2585		if self.db.get_size(col::STATEMENTS, hash.as_slice()).ok().flatten().is_some() {
2586			if !source.can_be_resubmitted() {
2587				self.metrics.report(|metrics| {
2588					metrics.known_statements.with_label_values(&["known"]).inc();
2589				});
2590				return SubmitResult::Known;
2591			}
2592		} else if self.db.get_size(col::EXPIRED, hash.as_slice()).ok().flatten().is_some() {
2593			if !source.can_be_resubmitted() {
2594				self.metrics.report(|metrics| {
2595					metrics.known_statements.with_label_values(&["known_expired"]).inc();
2596				});
2597				return SubmitResult::KnownExpired;
2598			}
2599		}
2600
2601		let Some(account_id) = statement.account_id() else {
2602			log::debug!(
2603				target: LOG_TARGET,
2604				"Statement validation failed: Missing proof ({:?})",
2605				HexDisplay::from(&hash),
2606			);
2607			let reason = InvalidReason::NoProof;
2608			self.metrics.report(|metrics| {
2609				metrics.validations_invalid.with_label_values(&[reason.label()]).inc();
2610			});
2611			return SubmitResult::Invalid(reason);
2612		};
2613
2614		match statement.verify_signature() {
2615			SignatureVerificationResult::Valid(_) => {},
2616			SignatureVerificationResult::Invalid => {
2617				log::debug!(
2618					target: LOG_TARGET,
2619					"Statement validation failed: BadProof, {:?}",
2620					HexDisplay::from(&hash),
2621				);
2622				let reason = InvalidReason::BadProof;
2623				self.metrics.report(|metrics| {
2624					metrics.validations_invalid.with_label_values(&[reason.label()]).inc();
2625				});
2626				return SubmitResult::Invalid(reason);
2627			},
2628			SignatureVerificationResult::NoSignature => {
2629				log::debug!(
2630					target: LOG_TARGET,
2631					"Statement validation failed: NoProof, {:?}",
2632					HexDisplay::from(&hash),
2633				);
2634				let reason = InvalidReason::NoProof;
2635				self.metrics.report(|metrics| {
2636					metrics.validations_invalid.with_label_values(&[reason.label()]).inc();
2637				});
2638				return SubmitResult::Invalid(reason);
2639			},
2640		};
2641
2642		// Check statement allowance for the account and evict statements if necessary to make room
2643		// for the new statement. We use the best block for allowance checks to allow for more
2644		// up-to-date allowances. This means that in some cases, a statement may be accepted but
2645		// then later evicted when we enforce limits based on the finalized block, if the best_hash
2646		// does not make it into the finalized chain, but this is an acceptable tradeoff for
2647		// better responsiveness to allowance changes.
2648		let validation = match (self.read_allowance_fn)(&account_id, AllowanceBlock::Best) {
2649			Ok(Some(allowance)) if !allowance.is_depleted() => allowance,
2650			Ok(Some(_)) | Ok(None) => {
2651				log::debug!(
2652					target: LOG_TARGET,
2653					"Account {} has no usable statement allowance",
2654					HexDisplay::from(&account_id),
2655				);
2656				let reason = RejectionReason::NoAllowance;
2657				self.metrics.report(|metrics| {
2658					metrics.rejections.with_label_values(&[reason.label()]).inc();
2659				});
2660				return SubmitResult::Rejected(reason);
2661			},
2662			Err(e) => {
2663				log::debug!(
2664					target: LOG_TARGET,
2665					"Reading statement allowance for account {} failed",
2666					HexDisplay::from(&account_id),
2667				);
2668				self.metrics.report(|metrics| {
2669					metrics.internal_errors.with_label_values(&["read_allowance"]).inc();
2670				});
2671				return SubmitResult::InternalError(e);
2672			},
2673		};
2674
2675		let current_time = self.timestamp();
2676		let seq = {
2677			let mut submit_index = self.submit_index.write();
2678
2679			// Re-check for a duplicate under the write lock. Without
2680			// it, a double insertion would double-count the statement in the global counters and
2681			// in the account's quota.
2682			match self.db.get_size(col::STATEMENTS, hash.as_slice()) {
2683				Ok(Some(_)) => {
2684					self.metrics.report(|metrics| {
2685						metrics.known_statements.with_label_values(&["known"]).inc();
2686					});
2687					return SubmitResult::Known;
2688				},
2689				Ok(None) => {},
2690				Err(e) => {
2691					self.metrics.report(|metrics| {
2692						metrics.internal_errors.with_label_values(&["db_read"]).inc();
2693					});
2694					return SubmitResult::InternalError(Error::Db(e.to_string()));
2695				},
2696			}
2697
2698			let statement_len = statement.data_len();
2699			// The account record is materialised only when the constraint check actually needs
2700			// it: a channel-less statement from an account whose cached summary shows headroom
2701			// is admitted without touching the account's on-disk index at all, and an oversize
2702			// statement is rejected by `plan_insert` before it ever looks at the record.
2703			let oversize = statement_len > validation.max_size as usize;
2704			let cached = submit_index.account_statements.peek(&account_id).is_some();
2705			let summary_admits = !cached &&
2706				statement.channel().is_none() &&
2707				submit_index.summaries.peek(&account_id).is_some_and(|summary| {
2708					summary.count < validation.max_count as usize &&
2709						summary.data_size + statement_len <= validation.max_size as usize
2710				}) && submit_index.statement_count <
2711				submit_index.config.max_total_statements &&
2712				submit_index.total_size + statement_len <= submit_index.config.max_total_size;
2713			let loaded_record = if cached || summary_admits || oversize {
2714				None
2715			} else {
2716				match self.load_account_record(&account_id) {
2717					Ok(record) => Some(record),
2718					Err(e) => {
2719						self.metrics.report(|metrics| {
2720							metrics.internal_errors.with_label_values(&["db_read"]).inc();
2721						});
2722						return SubmitResult::InternalError(e);
2723					},
2724				}
2725			};
2726
2727			let empty_record = StatementsForAccount::default();
2728			let record = loaded_record
2729				.as_ref()
2730				.or_else(|| submit_index.account_statements.peek(&account_id))
2731				.unwrap_or(&empty_record);
2732			let plan = match submit_index.plan_insert(
2733				record,
2734				hash,
2735				&statement,
2736				&account_id,
2737				&validation,
2738				current_time,
2739			) {
2740				Ok(plan) => plan,
2741				Err(reason) => {
2742					self.metrics.report(|metrics| {
2743						metrics.rejections.with_label_values(&[reason.label()]).inc();
2744					});
2745					// The rejection left the store untouched, so a record loaded for planning
2746					// still mirrors the disk. Cache it: rejections cost the sender nothing, and
2747					// dropping the record here would let rejected submissions against a large
2748					// account rescan its whole on-disk index, under the write lock, on every
2749					// attempt.
2750					if let Some(record) = loaded_record {
2751						submit_index.cache_record_with_summary(account_id, record);
2752					}
2753					return SubmitResult::Rejected(reason);
2754				},
2755			};
2756
2757			// Build the whole admission as one atomic commit
2758			let mut commit = Vec::new();
2759			commit.push((col::STATEMENTS, hash.to_vec(), Some(statement.encode())));
2760			commit.push((col::ADMISSION_SEQ, plan.seq.to_be_bytes().to_vec(), Some(hash.to_vec())));
2761			commit.extend(statement_index_ops(&hash, &statement, true));
2762			let details = EntryDetails {
2763				channel: statement.channel(),
2764				data_len: statement_len,
2765				admission_seq: plan.seq,
2766			};
2767			commit.extend(account_index_ops(
2768				&account_id,
2769				Expiry(statement.expiry()),
2770				&hash,
2771				Some(&details),
2772			));
2773
2774			let mut evicted_statements = Vec::new();
2775			for (key, evicted_details) in &plan.evicted {
2776				commit.push((col::STATEMENTS, key.hash.to_vec(), None));
2777				commit.push((
2778					col::ADMISSION_SEQ,
2779					evicted_details.admission_seq.to_be_bytes().to_vec(),
2780					None,
2781				));
2782				commit.extend(account_index_ops(&account_id, key.expiry, &key.hash, None));
2783				match self.db.get(col::STATEMENTS, &key.hash) {
2784					Ok(Some(encoded)) => match Statement::decode(&mut encoded.as_slice()) {
2785						Ok(evicted_statement) => {
2786							commit.extend(statement_index_ops(
2787								&key.hash,
2788								&evicted_statement,
2789								false,
2790							));
2791							evicted_statements.push(evicted_statement);
2792						},
2793						Err(_) => log::error!(
2794							target: LOG_TARGET,
2795							"Corrupt statement {:?}",
2796							HexDisplay::from(&key.hash)
2797						),
2798					},
2799					Ok(None) => log::error!(
2800						target: LOG_TARGET,
2801						"Missing body of the indexed statement {:?}",
2802						HexDisplay::from(&key.hash)
2803					),
2804					Err(e) => log::warn!(
2805						target: LOG_TARGET,
2806						"Could not read evicted statement {:?} to clear its index: {:?}",
2807						HexDisplay::from(&key.hash),
2808						e
2809					),
2810				}
2811			}
2812			for (h, purge_at) in &plan.banned {
2813				commit.push((col::EXPIRED, h.to_vec(), Some((h, current_time).encode())));
2814				commit.push((
2815					col::INDEX_EVICTED,
2816					evicted_index_key(*purge_at, h),
2817					Some(INDEX_EMPTY_VALUE.to_vec()),
2818				));
2819			}
2820			let freed: usize = plan.evicted.iter().map(|(_, details)| details.data_len).sum();
2821			commit.push(SubmitIndex::counters_op(
2822				submit_index.statement_count + 1 - plan.evicted.len(),
2823				submit_index.total_size + statement_len - freed,
2824				plan.seq.saturating_add(1),
2825			));
2826
2827			if let Err(e) = self.db.commit(commit) {
2828				log::debug!(
2829					target: LOG_TARGET,
2830					"Statement validation failed: database error {}, {:?}",
2831					e,
2832					statement
2833				);
2834				self.metrics.report(|metrics| {
2835					metrics.internal_errors.with_label_values(&["db_commit"]).inc();
2836				});
2837				return SubmitResult::InternalError(Error::Db(e.to_string()));
2838			}
2839			let seq = plan.seq;
2840			submit_index.apply_insert(&account_id, loaded_record, hash, &statement, &plan);
2841			// The query-index bookkeeping is applied under the same lock that ordered the
2842			// commit: a concurrent removal racing a resubmission of the same statement can then
2843			// never apply its stale update on top of this newer one (#12624). The notification
2844			// stays outside — its ordering is protected by the sequence watermark, not the lock.
2845			{
2846				let mut query_index = self.query_index.write();
2847				for evicted_statement in &evicted_statements {
2848					query_index.note_remove(&evicted_statement.hash(), evicted_statement);
2849				}
2850				query_index.note_insert(hash, &statement, plan.seq);
2851			}
2852			seq
2853		}; // Release submit index lock
2854		self.subscription_manager.notify(seq, statement);
2855		self.metrics.report(|metrics| metrics.submitted_statements.inc());
2856		log::trace!(target: LOG_TARGET, "Statement submitted: {:?}", HexDisplay::from(&hash));
2857		SubmitResult::New
2858	}
2859
2860	/// Soft-delete a statement by hash: drop it and its index rows from the database, and record
2861	/// it in the `EXPIRED` column so it cannot be re-accepted until its purge period elapses (see
2862	/// [`maintain`](Self::maintain)). No-op if the statement is unknown.
2863	fn remove(&self, hash: &Hash) -> Result<()> {
2864		self.remove_statement(hash).map(|_| ())
2865	}
2866
2867	/// Remove every statement authored by `who`, applying the same soft-delete as
2868	/// [`remove`](Self::remove) to each, in a single atomic commit.
2869	fn remove_by(&self, who: [u8; 32]) -> Result<()> {
2870		let current_time = self.timestamp();
2871		{
2872			let mut submit_index = self.submit_index.write();
2873			// The account's statements, from the cached record when there is one, else from the
2874			// on-disk index.
2875			let entries: Vec<(PriorityKey, EntryDetails)> =
2876				match submit_index.account_statements.peek(&who) {
2877					Some(record) => {
2878						record.by_priority.iter().map(|(key, details)| (*key, *details)).collect()
2879					},
2880					None => self.load_account_entries(&who)?,
2881				};
2882			if entries.is_empty() {
2883				return Ok(());
2884			}
2885
2886			let mut commit = Vec::new();
2887			let mut removed_statements = Vec::new();
2888			let mut banned_count = 0usize;
2889			let mut freed_size = 0usize;
2890			for (key, details) in &entries {
2891				commit.push((col::STATEMENTS, key.hash.to_vec(), None));
2892				commit.push((
2893					col::ADMISSION_SEQ,
2894					details.admission_seq.to_be_bytes().to_vec(),
2895					None,
2896				));
2897				commit.extend(account_index_ops(&who, key.expiry, &key.hash, None));
2898				if current_time < key.expiry.get_expiration_timestamp_secs() {
2899					let purge_at = key
2900						.expiry
2901						.get_expiration_timestamp_secs()
2902						.min(current_time.saturating_add(submit_index.config.purge_after_sec));
2903					commit.push((
2904						col::EXPIRED,
2905						key.hash.to_vec(),
2906						Some((key.hash, current_time).encode()),
2907					));
2908					commit.push((
2909						col::INDEX_EVICTED,
2910						evicted_index_key(purge_at, &key.hash),
2911						Some(INDEX_EMPTY_VALUE.to_vec()),
2912					));
2913					banned_count += 1;
2914				}
2915				freed_size += details.data_len;
2916				match self.db.get(col::STATEMENTS, &key.hash) {
2917					Ok(Some(encoded)) => match Statement::decode(&mut encoded.as_slice()) {
2918						Ok(statement) => {
2919							commit.extend(statement_index_ops(&key.hash, &statement, false));
2920							removed_statements.push((key.hash, statement));
2921						},
2922						Err(_) => log::error!(
2923							target: LOG_TARGET,
2924							"Corrupt statement {:?}",
2925							HexDisplay::from(&key.hash)
2926						),
2927					},
2928					Ok(None) => log::error!(
2929						target: LOG_TARGET,
2930						"Missing body of the indexed statement {:?}",
2931						HexDisplay::from(&key.hash)
2932					),
2933					Err(e) => {
2934						log::warn!(
2935							target: LOG_TARGET,
2936							"Could not read statement {:?} to clear its index: {:?}",
2937							HexDisplay::from(&key.hash),
2938							e
2939						);
2940					},
2941				}
2942			}
2943			commit.push(SubmitIndex::counters_op(
2944				submit_index.statement_count.saturating_sub(entries.len()),
2945				submit_index.total_size.saturating_sub(freed_size),
2946				submit_index.next_seq,
2947			));
2948			self.db.commit(commit).map_err(|e| {
2949				log::debug!(
2950					target: LOG_TARGET,
2951					"Error removing statement: database error {}, remove by {:?}",
2952					e,
2953					HexDisplay::from(&who),
2954				);
2955
2956				Error::Db(e.to_string())
2957			})?;
2958			submit_index.apply_account_removal(&who, entries.len(), freed_size, banned_count);
2959			// Applied under the same lock that ordered the commit (#12624).
2960			let mut query_index = self.query_index.write();
2961			for (hash, statement) in &removed_statements {
2962				query_index.note_remove(hash, statement);
2963			}
2964		}
2965		Ok(())
2966	}
2967}
2968
2969/// RAII guard that deregisters a subscription snapshot scan (see [`SubmitIndex::begin_scan`] /
2970/// [`SubmitIndex::end_scan`]) when dropped, so the `recent_seqs` window is always released — on the
2971/// happy path, on an early `?` return, or on a panic during the snapshot.
2972struct ScanGuard<'a> {
2973	store: &'a Store,
2974	watermark: u64,
2975}
2976
2977impl Drop for ScanGuard<'_> {
2978	fn drop(&mut self) {
2979		self.store.submit_index.write().end_scan(self.watermark);
2980	}
2981}
2982
2983impl StatementStoreSubscriptionApi for Store {
2984	fn subscribe_statement(
2985		&self,
2986		topic_filter: OptimizedTopicFilter,
2987	) -> Result<(Vec<Vec<u8>>, async_channel::Sender<StatementEvent>, SubscriptionStatementsStream)>
2988	{
2989		// Avoid overlap between the subscribe-time snapshot and live delivery using a
2990		// sequence-number watermark. Under the submit-index write lock, atomically with respect
2991		// to sequence assignment, capture the current boundary `W` and enqueue the subscription
2992		// registration tagged with `W`.
2993		let (subscription_sender, subscription_stream, watermark) = {
2994			let mut submit_index = self.submit_index.write();
2995			let watermark = submit_index.begin_scan();
2996			let (sender, stream) =
2997				self.subscription_manager.subscribe(topic_filter.clone(), watermark);
2998			(sender, stream, watermark)
2999		};
3000		let _scan_guard = ScanGuard { store: self, watermark };
3001
3002		let mut hashes = HashSet::new();
3003		self.iterate_with(None, &topic_filter, |hash| {
3004			hashes.insert(*hash);
3005			Ok(())
3006		})?;
3007
3008		let hashes: Vec<Hash> = {
3009			let submit_index = self.submit_index.read();
3010			hashes
3011				.into_iter()
3012				.filter(|hash| submit_index.seq_covered_by_snapshot(hash, watermark))
3013				.collect()
3014		};
3015
3016		let mut existing_statements = Vec::with_capacity(hashes.len());
3017		for hash in hashes {
3018			if let Some(entry) = self.read_statement_encoded(&hash)? {
3019				existing_statements.push(entry);
3020			}
3021		}
3022
3023		if existing_statements.is_empty() {
3024			subscription_sender
3025				.send_blocking(StatementEvent::NewStatements {
3026					statements: vec![],
3027					remaining: Some(0),
3028				})
3029				.ok();
3030		}
3031		Ok((existing_statements, subscription_sender, subscription_stream))
3032	}
3033}
3034
3035impl Store {
3036	/// Body of [`StatementStore::remove`], reporting whether a statement was actually removed.
3037	///
3038	/// `Ok(false)` means no (decodable) statement is stored under `hash` — it was already gone,
3039	/// or its body is corrupt. A corrupt body cannot be tied back to its index rows, so nothing
3040	/// is removed at all.
3041	fn remove_statement(&self, hash: &Hash) -> Result<bool> {
3042		let current_time = self.timestamp();
3043		{
3044			let mut submit_index = self.submit_index.write();
3045			// The body is read under the submit-index lock, so it cannot change under our feet
3046			let Some(encoded) =
3047				self.db.get(col::STATEMENTS, hash).map_err(|e| Error::Db(e.to_string()))?
3048			else {
3049				return Ok(false);
3050			};
3051			let Some((statement, account)) = Statement::decode(&mut encoded.as_slice())
3052				.ok()
3053				.and_then(|statement| statement.account_id().map(|account| (statement, account)))
3054			else {
3055				// A corrupt body cannot be tied back to its index rows
3056				log::error!(target: LOG_TARGET, "Corrupt statement {:?}", HexDisplay::from(hash));
3057				return Ok(false);
3058			};
3059			let expiry = Expiry(statement.expiry());
3060			let account_key = account_index_key(&account, expiry, hash);
3061			let details = self
3062				.db
3063				.get(col::INDEX_BY_ACCOUNT, &account_key)
3064				.map_err(|e| Error::Db(e.to_string()))?
3065				.as_deref()
3066				.and_then(|mut value| EntryDetails::decode(&mut value).ok());
3067
3068			let mut commit = vec![(col::STATEMENTS, hash.to_vec(), None)];
3069			commit.extend(statement_index_ops(hash, &statement, false));
3070			commit.extend(account_index_ops(&account, expiry, hash, None));
3071			match &details {
3072				Some(details) => commit.push((
3073					col::ADMISSION_SEQ,
3074					details.admission_seq.to_be_bytes().to_vec(),
3075					None,
3076				)),
3077				// Nothing points at the admission entry any more; replay skips it once the body
3078				// is gone.
3079				None => log::error!(
3080					target: LOG_TARGET,
3081					"Missing or corrupt account index entry for statement {:?}",
3082					HexDisplay::from(hash)
3083				),
3084			}
3085			let banned = current_time < expiry.get_expiration_timestamp_secs();
3086			if banned {
3087				let purge_at = expiry
3088					.get_expiration_timestamp_secs()
3089					.min(current_time.saturating_add(submit_index.config.purge_after_sec));
3090				commit.push((col::EXPIRED, hash.to_vec(), Some((hash, current_time).encode())));
3091				commit.push((
3092					col::INDEX_EVICTED,
3093					evicted_index_key(purge_at, hash),
3094					Some(INDEX_EMPTY_VALUE.to_vec()),
3095				));
3096			}
3097			let data_len = details
3098				.as_ref()
3099				.map_or_else(|| statement.data_len(), |details| details.data_len);
3100			commit.push(SubmitIndex::counters_op(
3101				submit_index.statement_count.saturating_sub(1),
3102				submit_index.total_size.saturating_sub(data_len),
3103				submit_index.next_seq,
3104			));
3105			if let Err(e) = self.db.commit(commit) {
3106				log::debug!(
3107					target: LOG_TARGET,
3108					"Error removing statement: database error {}, {:?}",
3109					e,
3110					HexDisplay::from(hash),
3111				);
3112				return Err(Error::Db(e.to_string()));
3113			}
3114			submit_index.apply_removal(
3115				&account,
3116				&PriorityKey { hash: *hash, expiry },
3117				data_len,
3118				banned,
3119			);
3120			self.query_index.write().note_remove(hash, &statement);
3121			log::trace!(target: LOG_TARGET, "Expired statement {:?}", HexDisplay::from(hash));
3122		}
3123		Ok(true)
3124	}
3125
3126	/// Reports an [`col::INDEX_BY_EXPIRY`] row that survived its statement,
3127	/// re-checking under the submit lock — every commit happens under it — that the row is indeed
3128	/// orphaned: a concurrent removal deletes the row along with its statement, and a concurrent
3129	/// re-admission of the same statement recreates the same content-derived key together with a
3130	/// body.
3131	fn report_orphan_expiry_row(&self, expiry: u64, hash: &Hash) {
3132		let _submit_index = self.submit_index.write();
3133		let body = self.db.get_size(col::STATEMENTS, hash.as_slice());
3134		let row = self.db.get(col::INDEX_BY_EXPIRY, &expiry_index_key(Expiry(expiry), hash));
3135		match (body, row) {
3136			(Ok(None), Ok(Some(_))) => log::error!(
3137				target: LOG_TARGET,
3138				"Orphan expiry index row for statement {:?}",
3139				HexDisplay::from(hash)
3140			),
3141			(Err(e), _) | (_, Err(e)) => {
3142				log::debug!(target: LOG_TARGET, "Error checking statement presence: {:?}", e)
3143			},
3144			_ => {},
3145		}
3146	}
3147
3148	fn register_replay(&self, enqueue: &mut dyn FnMut(u64) -> bool) -> Result<Option<u64>> {
3149		let registered = {
3150			let submit_index = self.submit_index.write();
3151			let watermark = submit_index.next_seq;
3152			enqueue(watermark).then_some(watermark)
3153		}; // Release submit index lock
3154		Ok(registered)
3155	}
3156
3157	fn replay_batch(
3158		&self,
3159		filter: &OptimizedTopicFilter,
3160		cursor: u64,
3161		watermark: u64,
3162	) -> Result<ReplayBatch> {
3163		let mut statements = Vec::new();
3164		let mut chunk_bytes = 0usize;
3165		// Replay batches are pulled by a local subscription task, not a remote peer, so the
3166		// walk is not budgeted.
3167		let batch = StatementStore::admitted_statements(
3168			self,
3169			cursor,
3170			watermark,
3171			usize::MAX,
3172			&mut |_, encoded, statement| {
3173				if !filter.matches(statement) {
3174					return FilterDecision::Skip;
3175				}
3176				if !statements.is_empty() && chunk_bytes + encoded.len() > REPLAY_CHUNK_RAW_BYTES {
3177					return FilterDecision::Abort;
3178				}
3179				chunk_bytes += encoded.len();
3180				statements.push(encoded.to_vec());
3181				// The encoded bytes are already captured above, so the statement is reported
3182				// as skipped to keep the walk from cloning it into the returned batch too.
3183				FilterDecision::Skip
3184			},
3185		)?;
3186		Ok(ReplayBatch { statements, cursor: batch.cursor, done: batch.done })
3187	}
3188}
3189
3190impl MultiFilterSubscriptionApi for Arc<Store> {
3191	fn create_subscription(&self) -> (SubscriptionHandle, MultiFilterEventStream) {
3192		let inner =
3193			Arc::new(parking_lot::Mutex::new(crate::subscription::SubscriptionHandleInner::new()));
3194		let snapshot_provider: Arc<dyn ReplaySnapshotProvider> = Arc::new(Arc::downgrade(self));
3195		let (sub_id, stream) = self.subscription_manager.subscribe_empty(snapshot_provider.clone());
3196
3197		let handle = SubscriptionHandle {
3198			sub_id,
3199			inner,
3200			matchers: self.subscription_manager.matchers(),
3201			snapshot_provider,
3202		};
3203		(handle, stream)
3204	}
3205}
3206
3207#[cfg(test)]
3208impl Store {
3209	/// Number of hashes currently in the on-disk evicted journal (per the in-memory counter).
3210	fn evicted_count(&self) -> usize {
3211		self.submit_index.read().evicted_count
3212	}
3213
3214	/// Whether `hash` is currently banned from re-acceptance (present in the evicted journal).
3215	fn is_evicted(&self, hash: &Hash) -> bool {
3216		self.db.get_size(col::EXPIRED, hash.as_slice()).ok().flatten().is_some()
3217	}
3218
3219	/// Whether the on-disk topic index links `topic` to `hash`.
3220	fn index_has_topic(&self, topic: &Topic, hash: &Hash) -> bool {
3221		self.index_set_contains(&IndexSet::Topic(*topic), hash).unwrap_or(false)
3222	}
3223
3224	/// Whether the on-disk decryption-key index links `key` to `hash`.
3225	fn index_has_dec_key(&self, key: &Option<DecryptionKey>, hash: &Hash) -> bool {
3226		self.index_set_contains(&IndexSet::DecKey(*key), hash).unwrap_or(false)
3227	}
3228
3229	/// Number of stored statements, per the in-memory counter.
3230	fn statement_count(&self) -> usize {
3231		self.submit_index.read().statement_count
3232	}
3233
3234	/// Whether the details cache currently holds `who`'s record.
3235	fn details_cached(&self, who: &AccountId) -> bool {
3236		self.submit_index.read().account_statements.peek(who).is_some()
3237	}
3238
3239	/// Whether the summary cache currently holds `who`'s entry.
3240	fn summary_cached(&self, who: &AccountId) -> bool {
3241		self.submit_index.read().summaries.peek(who).is_some()
3242	}
3243
3244	/// Total stored data size, per the in-memory counter.
3245	fn total_size(&self) -> usize {
3246		self.submit_index.read().total_size
3247	}
3248
3249	/// Whether `who` has at least one statement in the on-disk account index.
3250	fn has_account(&self, who: &AccountId) -> bool {
3251		self.next_account_from(who).expect("failed to read the account index") == Some(*who)
3252	}
3253
3254	/// Number of accounts in the on-disk account index.
3255	fn account_count(&self) -> usize {
3256		self.count_accounts().expect("failed to count the accounts")
3257	}
3258
3259	/// Inserts `statement` bypassing signature and allowance validation — used to seed
3260	/// over-allowance states that `submit` would refuse to create.
3261	fn force_insert(&self, statement: &Statement) {
3262		let hash = statement.hash();
3263		let account = statement.account_id().expect("test statements are signed; qed");
3264		let mut submit_index = self.submit_index.write();
3265		let seq = submit_index.next_seq;
3266		let details = EntryDetails {
3267			channel: statement.channel(),
3268			data_len: statement.data_len(),
3269			admission_seq: seq,
3270		};
3271		let mut commit = vec![
3272			(col::STATEMENTS, hash.to_vec(), Some(statement.encode())),
3273			(col::ADMISSION_SEQ, seq.to_be_bytes().to_vec(), Some(hash.to_vec())),
3274		];
3275		commit.extend(statement_index_ops(&hash, statement, true));
3276		commit.extend(account_index_ops(
3277			&account,
3278			Expiry(statement.expiry()),
3279			&hash,
3280			Some(&details),
3281		));
3282		commit.push(SubmitIndex::counters_op(
3283			submit_index.statement_count + 1,
3284			submit_index.total_size + statement.data_len(),
3285			seq.saturating_add(1),
3286		));
3287		self.db.commit(commit).expect("failed to commit the statement");
3288		let plan = InsertPlan { seq, evicted: Vec::new(), banned: Vec::new() };
3289		submit_index.apply_insert(&account, None, hash, statement, &plan);
3290		self.query_index.write().note_insert(hash, statement, seq);
3291	}
3292}
3293
3294#[cfg(test)]
3295mod tests {
3296
3297	use crate::{col, Store, KEY_VERSION};
3298	use sc_keystore::Keystore;
3299	use sp_core::{Decode, Encode, Pair};
3300	use sp_statement_store::{
3301		AccountId, Channel, DecryptionKey, FilterDecision, InvalidReason, OptimizedTopicFilter,
3302		Proof, RejectionReason, Statement, StatementSource, StatementStore, SubmitResult, Topic,
3303	};
3304
3305	type Extrinsic = sp_runtime::OpaqueExtrinsic;
3306	type Hash = sp_core::H256;
3307	type Hashing = sp_runtime::traits::BlakeTwo256;
3308	type BlockNumber = u64;
3309	type Header = sp_runtime::generic::Header<BlockNumber, Hashing>;
3310	type Block = sp_runtime::generic::Block<Header, Extrinsic>;
3311
3312	const TEST_BEST_BLOCK_HASH: [u8; 32] = [1u8; 32];
3313
3314	/// Maximum seed value used by `account(seed)`/`statement(seed, ...)` in this
3315	/// test module. Increase if you add tests that pass larger seed values to
3316	/// `statement(..)`. The reverse-lookup table in `TestClient::storage` is
3317	/// populated lazily for seeds in `0..=MAX_TEST_ACCOUNT_SEED`.
3318	const MAX_TEST_ACCOUNT_SEED: u64 = 64;
3319
3320	/// Reverse-lookup table from a real sr25519 public key back to the synthetic
3321	/// `u64` seed it was derived from. Populated once with seeds in
3322	/// `0..=MAX_TEST_ACCOUNT_SEED`, then consulted by `TestClient::storage` to
3323	/// figure out which allowance bucket to return for a given account.
3324	fn account_seed_table() -> &'static std::collections::BTreeMap<AccountId, u64> {
3325		use std::sync::OnceLock;
3326		static TABLE: OnceLock<std::collections::BTreeMap<AccountId, u64>> = OnceLock::new();
3327		TABLE.get_or_init(|| {
3328			let mut t = std::collections::BTreeMap::new();
3329			for seed in 0..=MAX_TEST_ACCOUNT_SEED {
3330				t.insert(account_keypair(seed).public().0, seed);
3331			}
3332			t
3333		})
3334	}
3335
3336	#[derive(Clone)]
3337	pub(crate) struct TestClient;
3338
3339	pub(crate) type TestBackend = sc_client_api::in_mem::Backend<Block>;
3340
3341	impl sc_client_api::StorageProvider<Block, TestBackend> for TestClient {
3342		fn storage(
3343			&self,
3344			_hash: Hash,
3345			key: &sc_client_api::StorageKey,
3346		) -> sp_blockchain::Result<Option<sc_client_api::StorageData>> {
3347			use sp_statement_store::StatementAllowance;
3348
3349			assert_eq!(&key.0[0..21], b":statement_allowance:" as &[u8],);
3350
3351			// Recover the synthetic test seed from the account id. Unknown accounts
3352			// (e.g. //Alice for `signed_statement`) fall through to a generic default.
3353			let account_bytes: AccountId = key.0[21..53].try_into().unwrap();
3354			let seed = account_seed_table().get(&account_bytes).copied();
3355			let allowance = match seed {
3356				// Account 0 has no allowance (used to test eviction of all statements)
3357				Some(0) => return Ok(None),
3358				Some(1) => StatementAllowance::new(1, 1000),
3359				Some(2) => StatementAllowance::new(2, 1000),
3360				Some(3) => StatementAllowance::new(3, 1000),
3361				Some(4) => StatementAllowance::new(4, 1000),
3362				Some(42) => StatementAllowance::new(42, (42 * crate::MAX_STATEMENT_SIZE) as u32),
3363				// Depleted on one axis, but still present in state.
3364				Some(50) => StatementAllowance::new(0, 1000),
3365				Some(51) => StatementAllowance::new(5, 0),
3366				Some(_) | None => StatementAllowance::new(100, 1000),
3367			};
3368			Ok(Some(sc_client_api::StorageData(allowance.encode())))
3369		}
3370
3371		fn storage_hash(
3372			&self,
3373			_hash: Hash,
3374			_key: &sc_client_api::StorageKey,
3375		) -> sp_blockchain::Result<Option<Hash>> {
3376			unimplemented!()
3377		}
3378
3379		fn storage_keys(
3380			&self,
3381			_hash: Hash,
3382			_prefix: Option<&sc_client_api::StorageKey>,
3383			_start_key: Option<&sc_client_api::StorageKey>,
3384		) -> sp_blockchain::Result<
3385			sc_client_api::backend::KeysIter<
3386				<TestBackend as sc_client_api::Backend<Block>>::State,
3387				Block,
3388			>,
3389		> {
3390			unimplemented!()
3391		}
3392
3393		fn storage_pairs(
3394			&self,
3395			_hash: Hash,
3396			_prefix: Option<&sc_client_api::StorageKey>,
3397			_start_key: Option<&sc_client_api::StorageKey>,
3398		) -> sp_blockchain::Result<
3399			sc_client_api::backend::PairsIter<
3400				<TestBackend as sc_client_api::Backend<Block>>::State,
3401				Block,
3402			>,
3403		> {
3404			unimplemented!()
3405		}
3406
3407		fn child_storage(
3408			&self,
3409			_hash: Hash,
3410			_child_info: &sc_client_api::ChildInfo,
3411			_key: &sc_client_api::StorageKey,
3412		) -> sp_blockchain::Result<Option<sc_client_api::StorageData>> {
3413			unimplemented!()
3414		}
3415
3416		fn child_storage_keys(
3417			&self,
3418			_hash: Hash,
3419			_child_info: sc_client_api::ChildInfo,
3420			_prefix: Option<&sc_client_api::StorageKey>,
3421			_start_key: Option<&sc_client_api::StorageKey>,
3422		) -> sp_blockchain::Result<
3423			sc_client_api::backend::KeysIter<
3424				<TestBackend as sc_client_api::Backend<Block>>::State,
3425				Block,
3426			>,
3427		> {
3428			unimplemented!()
3429		}
3430
3431		fn child_storage_hash(
3432			&self,
3433			_hash: Hash,
3434			_child_info: &sc_client_api::ChildInfo,
3435			_key: &sc_client_api::StorageKey,
3436		) -> sp_blockchain::Result<Option<Hash>> {
3437			unimplemented!()
3438		}
3439
3440		fn closest_merkle_value(
3441			&self,
3442			_hash: Hash,
3443			_key: &sc_client_api::StorageKey,
3444		) -> sp_blockchain::Result<Option<sc_client_api::MerkleValue<Hash>>> {
3445			unimplemented!()
3446		}
3447
3448		fn child_closest_merkle_value(
3449			&self,
3450			_hash: Hash,
3451			_child_info: &sc_client_api::ChildInfo,
3452			_key: &sc_client_api::StorageKey,
3453		) -> sp_blockchain::Result<Option<sc_client_api::MerkleValue<Hash>>> {
3454			unimplemented!()
3455		}
3456	}
3457
3458	impl sp_blockchain::HeaderBackend<Block> for TestClient {
3459		fn header(&self, _hash: Hash) -> sp_blockchain::Result<Option<Header>> {
3460			unimplemented!()
3461		}
3462		fn info(&self) -> sp_blockchain::Info<Block> {
3463			sp_blockchain::Info {
3464				best_hash: TEST_BEST_BLOCK_HASH.into(),
3465				best_number: 0,
3466				genesis_hash: Default::default(),
3467				finalized_hash: TEST_BEST_BLOCK_HASH.into(),
3468				finalized_number: 1,
3469				finalized_state: None,
3470				number_leaves: 0,
3471				block_gap: None,
3472			}
3473		}
3474		fn status(&self, _hash: Hash) -> sp_blockchain::Result<sp_blockchain::BlockStatus> {
3475			unimplemented!()
3476		}
3477		fn number(&self, _hash: Hash) -> sp_blockchain::Result<Option<BlockNumber>> {
3478			unimplemented!()
3479		}
3480		fn hash(&self, _number: BlockNumber) -> sp_blockchain::Result<Option<Hash>> {
3481			unimplemented!()
3482		}
3483	}
3484
3485	fn test_store() -> (Store, tempfile::TempDir) {
3486		sp_tracing::init_for_tests();
3487		let temp_dir = tempfile::Builder::new().tempdir().expect("Error creating test dir");
3488
3489		let client = std::sync::Arc::new(TestClient);
3490		let mut path: std::path::PathBuf = temp_dir.path().into();
3491		path.push("db");
3492		let keystore = std::sync::Arc::new(sc_keystore::LocalKeystore::in_memory());
3493		let store = Store::new::<Block, TestClient, TestBackend>(
3494			&path,
3495			Default::default(),
3496			client,
3497			keystore,
3498			None,
3499			Box::new(sp_core::testing::TaskExecutor::new()),
3500		)
3501		.unwrap();
3502		(store, temp_dir) // return order is important. Store must be dropped before TempDir
3503	}
3504
3505	pub fn signed_statement(data: u8) -> Statement {
3506		signed_statement_with_topics(data, &[], None)
3507	}
3508
3509	fn signed_statement_with_topics(
3510		data: u8,
3511		topics: &[Topic],
3512		dec_key: Option<DecryptionKey>,
3513	) -> Statement {
3514		let mut statement = Statement::new();
3515		statement.set_plain_data(vec![data]);
3516		statement.set_expiry(u64::MAX);
3517
3518		for i in 0..topics.len() {
3519			statement.set_topic(i, topics[i]);
3520		}
3521		if let Some(key) = dec_key {
3522			statement.set_decryption_key(key);
3523		}
3524		let kp = sp_core::ed25519::Pair::from_string("//Alice", None).unwrap();
3525		statement.sign_ed25519_private(&kp);
3526		statement
3527	}
3528
3529	fn topic(data: u64) -> Topic {
3530		let mut bytes = [0u8; 32];
3531		bytes[0..8].copy_from_slice(&data.to_le_bytes());
3532		Topic::from(bytes)
3533	}
3534
3535	fn dec_key(data: u64) -> DecryptionKey {
3536		let mut dec_key: DecryptionKey = Default::default();
3537		dec_key[0..8].copy_from_slice(&data.to_le_bytes());
3538		dec_key
3539	}
3540
3541	/// Returns the deterministic ed25519 keypair used to author statements for the
3542	/// synthetic test account `seed`.
3543	///
3544	/// Uses ed25519 rather than sr25519 because schnorrkel signing is non-deterministic
3545	/// (the signature depends on RNG state), so calling `statement(id, prio, ch, len)`
3546	/// twice would produce different hashes. Several tests compare statement hashes
3547	/// against pre-computed values; ed25519 keeps those comparisons stable.
3548	fn account_keypair(seed: u64) -> sp_core::ed25519::Pair {
3549		sp_core::ed25519::Pair::from_string(&format!("//StatementAccount{seed}"), None)
3550			.expect("Derivation path is valid; qed")
3551	}
3552
3553	fn account(id: u64) -> AccountId {
3554		account_keypair(id).public().0
3555	}
3556
3557	/// Signs `stmt` with `account_id`'s test keypair. Tests that build a statement via
3558	/// `unsigned_statement(..)` and then mutate it call this exactly once at the end.
3559	fn sign_with(stmt: &mut Statement, account_id: u64) {
3560		stmt.sign_ed25519_private(&account_keypair(account_id));
3561	}
3562
3563	fn channel(id: u64) -> Channel {
3564		let mut channel: Channel = Default::default();
3565		channel[0..8].copy_from_slice(&id.to_le_bytes());
3566		channel
3567	}
3568
3569	/// Builds a test statement without signing it. Use this when a test needs to mutate
3570	/// the statement (encryption, expiry change, topic update, etc.) before submission —
3571	/// call `sign_with(&mut stmt, account_id)` once after all mutations.
3572	fn unsigned_statement(
3573		account_id: u64,
3574		priority: u32,
3575		c: Option<u64>,
3576		data_len: usize,
3577	) -> Statement {
3578		assert!(
3579			account_id <= MAX_TEST_ACCOUNT_SEED,
3580			"account_id {account_id} exceeds MAX_TEST_ACCOUNT_SEED ({MAX_TEST_ACCOUNT_SEED}); \
3581			 raise the constant if you need a wider range",
3582		);
3583		let mut statement = Statement::new();
3584		let mut data = Vec::new();
3585		data.resize(data_len, 0);
3586		statement.set_plain_data(data);
3587		statement.set_expiry_from_parts(u32::MAX, priority);
3588		if let Some(c) = c {
3589			statement.set_channel(channel(c));
3590		}
3591		statement
3592	}
3593
3594	fn statement(account_id: u64, priority: u32, c: Option<u64>, data_len: usize) -> Statement {
3595		let mut statement = unsigned_statement(account_id, priority, c, data_len);
3596		sign_with(&mut statement, account_id);
3597		statement
3598	}
3599
3600	#[test]
3601	fn submit_one() {
3602		let (store, _temp) = test_store();
3603		let statement0 = signed_statement(0);
3604		assert_eq!(store.submit(statement0, StatementSource::Network), SubmitResult::New);
3605		let statement1 = statement(1, 1, None, 0);
3606		assert_eq!(store.submit(statement1, StatementSource::Network), SubmitResult::New);
3607	}
3608
3609	#[test]
3610	fn depleted_count_allowance_admits_nothing() {
3611		let (store, _temp) = test_store();
3612		// Account 50 has `max_count: 0`.
3613		let result = store.submit(statement(50, 1, None, 100), StatementSource::Network);
3614		assert_eq!(result, SubmitResult::Rejected(RejectionReason::NoAllowance));
3615		assert_eq!(store.statement_count(), 0);
3616		assert_eq!(store.total_size(), 0);
3617	}
3618
3619	#[test]
3620	fn depleted_size_allowance_admits_nothing() {
3621		let (store, _temp) = test_store();
3622		// Account 51 has `max_size: 0`; a zero-length statement passes the length gate.
3623		let result = store.submit(statement(51, 1, None, 0), StatementSource::Network);
3624		assert_eq!(result, SubmitResult::Rejected(RejectionReason::NoAllowance));
3625		assert_eq!(store.statement_count(), 0);
3626		assert_eq!(store.total_size(), 0);
3627	}
3628
3629	#[test]
3630	fn save_and_load_statements() {
3631		let (store, temp) = test_store();
3632		let statement0 = signed_statement(0);
3633		let statement1 = signed_statement(1);
3634		let statement2 = signed_statement(2);
3635		assert_eq!(store.submit(statement0.clone(), StatementSource::Network), SubmitResult::New);
3636		assert_eq!(store.submit(statement1.clone(), StatementSource::Network), SubmitResult::New);
3637		assert_eq!(store.submit(statement2.clone(), StatementSource::Network), SubmitResult::New);
3638		assert_eq!(store.statements().unwrap().len(), 3);
3639		assert_eq!(store.broadcasts(&[]).unwrap().len(), 3);
3640		assert_eq!(store.statement(&statement1.hash()).unwrap(), Some(statement1.clone()));
3641		let keystore = store.keystore.clone();
3642		drop(store);
3643
3644		let client = std::sync::Arc::new(TestClient);
3645		let mut path: std::path::PathBuf = temp.path().into();
3646		path.push("db");
3647		let store = Store::new::<Block, TestClient, TestBackend>(
3648			&path,
3649			Default::default(),
3650			client,
3651			keystore,
3652			None,
3653			Box::new(sp_core::testing::TaskExecutor::new()),
3654		)
3655		.unwrap();
3656		assert_eq!(store.statements().unwrap().len(), 3);
3657		assert_eq!(store.broadcasts(&[]).unwrap().len(), 3);
3658		assert_eq!(store.statement(&statement1.hash()).unwrap(), Some(statement1));
3659	}
3660
3661	#[test]
3662	fn admission_seq_is_monotonic_across_restarts() {
3663		// Sequence numbers must never be reused, even when the highest-numbered statements were
3664		// removed before a restart: a replay cursor as high as a dead sequence number would
3665		// silently skip any statement re-admitted under it.
3666		let (store, temp) = test_store();
3667		let first = signed_statement(10);
3668		let second = signed_statement(11);
3669		assert_eq!(store.submit(first.clone(), StatementSource::Network), SubmitResult::New);
3670		assert_eq!(store.submit(second.clone(), StatementSource::Network), SubmitResult::New);
3671		assert_eq!(
3672			store.db.get(col::ADMISSION_SEQ, &0u64.to_be_bytes()).unwrap(),
3673			Some(first.hash().to_vec())
3674		);
3675		assert_eq!(
3676			store.db.get(col::ADMISSION_SEQ, &1u64.to_be_bytes()).unwrap(),
3677			Some(second.hash().to_vec())
3678		);
3679		store.remove(&second.hash()).unwrap();
3680		assert_eq!(
3681			store.db.get(col::ADMISSION_SEQ, &0u64.to_be_bytes()).unwrap(),
3682			Some(first.hash().to_vec())
3683		);
3684		assert_eq!(store.db.get(col::ADMISSION_SEQ, &1u64.to_be_bytes()).unwrap(), None);
3685		let keystore = store.keystore.clone();
3686		drop(store);
3687
3688		let mut path: std::path::PathBuf = temp.path().into();
3689		path.push("db");
3690		let store = Store::new::<Block, TestClient, TestBackend>(
3691			&path,
3692			Default::default(),
3693			std::sync::Arc::new(TestClient),
3694			keystore,
3695			None,
3696			Box::new(sp_core::testing::TaskExecutor::new()),
3697		)
3698		.unwrap();
3699		assert_eq!(store.submit_index.read().next_seq, 2);
3700
3701		let replay = store.replay_batch(&OptimizedTopicFilter::Any, 0, 2).unwrap();
3702		assert_eq!(replay.statements, vec![first.encode()]);
3703		assert!(replay.done);
3704
3705		// The removed statement's sequence number stays dead; the next admission claims a fresh
3706		// one.
3707		let third = signed_statement(12);
3708		assert_eq!(store.submit(third.clone(), StatementSource::Network), SubmitResult::New);
3709		assert_eq!(store.db.get(col::ADMISSION_SEQ, &1u64.to_be_bytes()).unwrap(), None);
3710		assert_eq!(
3711			store.db.get(col::ADMISSION_SEQ, &2u64.to_be_bytes()).unwrap(),
3712			Some(third.hash().to_vec())
3713		);
3714	}
3715
3716	#[test]
3717	fn lagging_counters_row_does_not_reuse_admission_seqs() {
3718		// A counters row lagging the admission journal is impossible on a database written by
3719		// this version (they are committed atomically); should one appear anyway, startup must
3720		// recover `next_seq` from the journal instead of reusing live sequence numbers.
3721		let (store, temp) = test_store();
3722		let first = signed_statement(1);
3723		let second = signed_statement(2);
3724		assert_eq!(store.submit(first.clone(), StatementSource::Network), SubmitResult::New);
3725		assert_eq!(store.submit(second.clone(), StatementSource::Network), SubmitResult::New);
3726		// Corrupt the counters row: keep the totals, rewind the sequence counter.
3727		let (statement_count, total_size, _) = <(u64, u64, u64)>::decode(
3728			&mut store.db.get(col::META, crate::KEY_COUNTERS).unwrap().unwrap().as_slice(),
3729		)
3730		.unwrap();
3731		store
3732			.db
3733			.commit([(
3734				col::META,
3735				crate::KEY_COUNTERS.to_vec(),
3736				Some((statement_count, total_size, 0u64).encode()),
3737			)])
3738			.unwrap();
3739		let keystore = store.keystore.clone();
3740		drop(store);
3741
3742		let mut path: std::path::PathBuf = temp.path().into();
3743		path.push("db");
3744		let store = Store::new::<Block, TestClient, TestBackend>(
3745			&path,
3746			Default::default(),
3747			std::sync::Arc::new(TestClient),
3748			keystore,
3749			None,
3750			Box::new(sp_core::testing::TaskExecutor::new()),
3751		)
3752		.unwrap();
3753		assert_eq!(store.submit_index.read().next_seq, 2);
3754
3755		// A new admission claims a fresh sequence number; the existing entries are untouched.
3756		let third = signed_statement(3);
3757		assert_eq!(store.submit(third.clone(), StatementSource::Network), SubmitResult::New);
3758		assert_eq!(
3759			store.db.get(col::ADMISSION_SEQ, &0u64.to_be_bytes()).unwrap(),
3760			Some(first.hash().to_vec())
3761		);
3762		assert_eq!(
3763			store.db.get(col::ADMISSION_SEQ, &1u64.to_be_bytes()).unwrap(),
3764			Some(second.hash().to_vec())
3765		);
3766		assert_eq!(
3767			store.db.get(col::ADMISSION_SEQ, &2u64.to_be_bytes()).unwrap(),
3768			Some(third.hash().to_vec())
3769		);
3770	}
3771
3772	#[test]
3773	fn statement_without_admission_entry_is_restored_by_migration() {
3774		// Admission entries are committed atomically with statement bodies, so a
3775		// current-version database cannot lose one short of external tampering. Statements
3776		// missing an admission entry (a database written before the entries existed) get one
3777		// from the version migration, which rebuilds every derived column from the bodies.
3778		let (store, temp) = test_store();
3779		let statement = signed_statement_with_topics(20, &[topic(1)], None);
3780		assert_eq!(store.submit(statement.clone(), StatementSource::Network), SubmitResult::New);
3781		// Drop the admission entry, leaving the body behind, and rewind the database version so
3782		// the next start migrates.
3783		store
3784			.db
3785			.commit([
3786				(col::ADMISSION_SEQ, 0u64.to_be_bytes().to_vec(), None),
3787				(col::META, KEY_VERSION.to_vec(), Some(1u32.to_le_bytes().to_vec())),
3788			])
3789			.unwrap();
3790		let filter = OptimizedTopicFilter::MatchAny([topic(1)].into_iter().collect());
3791		assert!(store.replay_batch(&filter, 0, 1).unwrap().statements.is_empty());
3792		let keystore = store.keystore.clone();
3793		drop(store);
3794
3795		let mut path: std::path::PathBuf = temp.path().into();
3796		path.push("db");
3797		let store = Store::new::<Block, TestClient, TestBackend>(
3798			&path,
3799			Default::default(),
3800			std::sync::Arc::new(TestClient),
3801			keystore,
3802			None,
3803			Box::new(sp_core::testing::TaskExecutor::new()),
3804		)
3805		.unwrap();
3806
3807		let watermark = store.submit_index.read().next_seq;
3808		assert_eq!(watermark, 1);
3809		assert_eq!(
3810			store.replay_batch(&filter, 0, watermark).unwrap().statements,
3811			vec![statement.encode()]
3812		);
3813	}
3814
3815	#[test]
3816	fn admission_cursor_resumes_from_the_middle_of_the_range() {
3817		let (store, _temp) = test_store();
3818		let first = signed_statement(1);
3819		let second = signed_statement(2);
3820		let third = signed_statement(3);
3821		for statement in [&first, &second, &third] {
3822			assert_eq!(
3823				store.submit(statement.clone(), StatementSource::Network),
3824				SubmitResult::New
3825			);
3826		}
3827		let batch = store.replay_batch(&OptimizedTopicFilter::Any, 1, 3).unwrap();
3828		assert_eq!(batch.statements, vec![second.encode(), third.encode()]);
3829		assert_eq!(batch.cursor, 3);
3830		assert!(batch.done);
3831	}
3832
3833	#[test]
3834	fn admission_cursor_replays_only_current_matching_admissions() {
3835		let (store, _temp) = test_store();
3836		let matching = signed_statement_with_topics(20, &[topic(1)], None);
3837		let non_matching = signed_statement_with_topics(21, &[topic(2)], None);
3838		let removed = signed_statement_with_topics(22, &[topic(1)], None);
3839		assert_eq!(store.submit(matching.clone(), StatementSource::Network), SubmitResult::New);
3840		assert_eq!(store.submit(non_matching, StatementSource::Network), SubmitResult::New);
3841		assert_eq!(store.submit(removed.clone(), StatementSource::Network), SubmitResult::New);
3842		store.remove(&removed.hash()).unwrap();
3843
3844		let replay = store
3845			.replay_batch(&OptimizedTopicFilter::MatchAny([topic(1)].into_iter().collect()), 0, 3)
3846			.unwrap();
3847		assert_eq!(replay.statements, vec![matching.encode()]);
3848		assert_eq!(replay.cursor, 3);
3849		assert!(replay.done);
3850	}
3851
3852	#[test]
3853	fn admission_watermark_is_one_past_the_newest_admission() {
3854		let (store, _temp) = test_store();
3855		assert_eq!(store.admission_watermark().unwrap(), 0);
3856
3857		let first = signed_statement(1);
3858		let second = signed_statement(2);
3859		assert_eq!(store.submit(first.clone(), StatementSource::Network), SubmitResult::New);
3860		assert_eq!(store.submit(second, StatementSource::Network), SubmitResult::New);
3861		assert_eq!(store.admission_watermark().unwrap(), 2);
3862
3863		// Sequence numbers are never reused, so a removal leaves the boundary in place.
3864		store.remove(&first.hash()).unwrap();
3865		assert_eq!(store.admission_watermark().unwrap(), 2);
3866	}
3867
3868	#[test]
3869	fn admitted_statements_walk_applies_filter_decisions_and_resumes() {
3870		let (store, _temp) = test_store();
3871		let statements: Vec<_> = (1..=4).map(signed_statement).collect();
3872		for statement in &statements {
3873			assert_eq!(
3874				store.submit(statement.clone(), StatementSource::Network),
3875				SubmitResult::New
3876			);
3877		}
3878		// Seq 1 goes dead, the walk must pass over it.
3879		store.remove(&statements[1].hash()).unwrap();
3880
3881		let batch = store
3882			.admitted_statements(0, 4, usize::MAX, &mut |hash, _encoded, _statement| {
3883				if *hash == statements[0].hash() {
3884					FilterDecision::Skip
3885				} else if *hash == statements[3].hash() {
3886					FilterDecision::Abort
3887				} else {
3888					FilterDecision::Take
3889				}
3890			})
3891			.unwrap();
3892		assert_eq!(batch.statements, vec![(statements[2].hash(), statements[2].clone())]);
3893		// The aborted statement sits at seq 3; the cursor points back at it.
3894		assert_eq!(batch.cursor, 3);
3895		assert!(!batch.done);
3896
3897		// Resuming from the cursor visits the aborted statement first.
3898		let batch = store
3899			.admitted_statements(batch.cursor, 4, usize::MAX, &mut |_, _, _| FilterDecision::Take)
3900			.unwrap();
3901		assert_eq!(batch.statements, vec![(statements[3].hash(), statements[3].clone())]);
3902		assert_eq!(batch.cursor, 4);
3903		assert!(batch.done);
3904	}
3905
3906	#[test]
3907	fn admitted_statements_walk_stops_at_the_scan_limit() {
3908		let (store, _temp) = test_store();
3909		let statements: Vec<_> = (1..=4).map(signed_statement).collect();
3910		for statement in &statements {
3911			assert_eq!(
3912				store.submit(statement.clone(), StatementSource::Network),
3913				SubmitResult::New
3914			);
3915		}
3916
3917		// A filter that takes nothing must still visit no more than `scan_limit` entries.
3918		let mut visited = 0;
3919		let batch = store
3920			.admitted_statements(0, 4, 3, &mut |_, _, _| {
3921				visited += 1;
3922				FilterDecision::Skip
3923			})
3924			.unwrap();
3925		assert!(batch.statements.is_empty());
3926		assert_eq!(visited, 3);
3927		assert_eq!(batch.cursor, 3, "the cursor must sit after the last visited entry");
3928		assert!(!batch.done);
3929
3930		// Resuming from the partial cursor completes the walk.
3931		let batch = store
3932			.admitted_statements(batch.cursor, 4, 3, &mut |_, _, _| FilterDecision::Skip)
3933			.unwrap();
3934		assert!(batch.statements.is_empty());
3935		assert_eq!(batch.cursor, 4);
3936		assert!(batch.done);
3937	}
3938
3939	#[test]
3940	fn take_recent_statements_are_ordered_by_admission() {
3941		let (store, _temp) = test_store();
3942		let statements: Vec<_> = (1..=3).map(signed_statement).collect();
3943		for statement in &statements {
3944			assert_eq!(
3945				store.submit(statement.clone(), StatementSource::Network),
3946				SubmitResult::New
3947			);
3948		}
3949
3950		let recent = store.take_recent_statements().unwrap();
3951		let expected: Vec<_> = statements
3952			.into_iter()
3953			.enumerate()
3954			.map(|(seq, statement)| (seq as u64, statement.hash(), statement))
3955			.collect();
3956		assert_eq!(recent, expected);
3957	}
3958
3959	#[test]
3960	fn migrates_v1_database_to_on_disk_index() {
3961		sp_tracing::init_for_tests();
3962		let temp = tempfile::Builder::new().tempdir().expect("Error creating test dir");
3963		let mut path: std::path::PathBuf = temp.path().into();
3964		path.push("db");
3965		// The store appends `statements` to the path it is given.
3966		let mut db_path = path.clone();
3967		db_path.push("statements");
3968
3969		// One addressed statement (topics 1 & 2, decryption key 9) and one broadcast (topic 1, no
3970		// key).
3971		let addressed = signed_statement_with_topics(1, &[topic(1), topic(2)], Some(dec_key(9)));
3972		let broadcast = signed_statement_with_topics(2, &[topic(1)], None);
3973		let h_addressed = addressed.hash();
3974		let h_broadcast = broadcast.hash();
3975
3976		// A hash seeded into the legacy EXPIRED column, with a deadline far in the future so it
3977		// survives the maintenance pass triggered during migration.
3978		let expired_hash = topic(999);
3979		let expired_ts = 10_000_000_000u64;
3980
3981		// Build a version-1 database by hand: three columns and no on-disk read index.
3982		{
3983			let mut cfg = parity_db::Options::with_columns(&db_path, 3);
3984			let statement_col = &mut cfg.columns[1];
3985			statement_col.ref_counted = false;
3986			statement_col.preimage = true;
3987			statement_col.uniform = true;
3988			let db = parity_db::Db::open_or_create(&cfg).unwrap();
3989			db.commit([
3990				(0u8, b"version".to_vec(), Some(1u32.to_le_bytes().to_vec())),
3991				(1u8, h_addressed.to_vec(), Some(addressed.encode())),
3992				(1u8, h_broadcast.to_vec(), Some(broadcast.encode())),
3993				(2u8, expired_hash.to_vec(), Some((expired_hash, expired_ts).encode())),
3994			])
3995			.unwrap();
3996		}
3997
3998		let open = |path: &std::path::Path| {
3999			Store::new::<Block, TestClient, TestBackend>(
4000				path,
4001				Default::default(),
4002				std::sync::Arc::new(TestClient),
4003				std::sync::Arc::new(sc_keystore::LocalKeystore::in_memory()),
4004				None,
4005				Box::new(sp_core::testing::TaskExecutor::new()),
4006			)
4007			.unwrap()
4008		};
4009
4010		// Re-open through the store: it must add the index columns, rebuild them from the bodies,
4011		// rebuild the evicted journal and admission sequence, and bump the database version.
4012		let store = open(&path);
4013
4014		// Bodies survived.
4015		assert_eq!(store.statements().unwrap().len(), 2);
4016		assert!(store.statement(&h_addressed).unwrap().is_some());
4017		assert!(store.statement(&h_broadcast).unwrap().is_some());
4018
4019		// The read index was rebuilt on disk.
4020		assert!(store.index_has_topic(&topic(1), &h_addressed));
4021		assert!(store.index_has_topic(&topic(1), &h_broadcast));
4022		assert!(store.index_has_topic(&topic(2), &h_addressed));
4023		assert!(store.index_has_dec_key(&Some(dec_key(9)), &h_addressed));
4024		assert!(store.index_has_dec_key(&None, &h_broadcast));
4025		let mut admitted = std::collections::HashSet::new();
4026		let mut iter = store.db.iter(col::ADMISSION_SEQ).unwrap();
4027		iter.seek_to_first().unwrap();
4028		while let Some((_key, value)) = iter.next().unwrap() {
4029			admitted.insert(value.as_slice().try_into().expect("admission hash is 32 bytes"));
4030		}
4031		assert_eq!(admitted, std::collections::HashSet::from([h_addressed, h_broadcast]));
4032		assert_eq!(store.submit_index.read().next_seq, 2);
4033
4034		// And it answers queries: only the broadcast matches topic 1 with no key, only the
4035		// addressed statement matches topic 1 for key 9.
4036		assert_eq!(store.broadcasts(&[topic(1)]).unwrap().len(), 1);
4037		assert_eq!(store.posted(&[topic(1)], dec_key(9)).unwrap().len(), 1);
4038
4039		// The evicted journal was rebuilt from the legacy EXPIRED column.
4040		assert!(store.is_evicted(&expired_hash));
4041		assert_eq!(store.evicted_count(), 1);
4042
4043		// The account and expiry indexes were rebuilt too, along with the counters row.
4044		assert_eq!(store.statement_count(), 2);
4045		assert_eq!(store.total_size(), 2);
4046		assert!(store.has_account(&addressed.account_id().unwrap()));
4047		assert_eq!(store.account_count(), 1);
4048
4049		// The database is now at the current version; re-opening does not migrate again.
4050		drop(store);
4051		let store = open(&path);
4052		assert_eq!(store.statements().unwrap().len(), 2);
4053		assert!(store.index_has_topic(&topic(1), &h_broadcast));
4054		assert!(store.is_evicted(&expired_hash));
4055	}
4056
4057	#[test]
4058	fn interrupted_migration_resumes_preserving_admission_entries() {
4059		// The migration commits in chunks and bumps the version only at the very end, so a crash
4060		// mid-way leaves a version-1 database with some derived rows already written. Re-running
4061		// the migration must preserve the admission entries it already assigned — replay cursors
4062		// depend on them — and rebuild everything else.
4063		sp_tracing::init_for_tests();
4064		let temp = tempfile::Builder::new().tempdir().expect("Error creating test dir");
4065		let mut path: std::path::PathBuf = temp.path().into();
4066		path.push("db");
4067		let mut db_path = path.clone();
4068		db_path.push("statements");
4069
4070		let s1 = signed_statement_with_topics(1, &[topic(1)], None);
4071		let s2 = statement(2, 5, Some(9), 200);
4072
4073		// A version-1 database whose columns were already extended and whose first migration
4074		// attempt committed s1's read-index and admission rows before crashing: the version was
4075		// never bumped.
4076		{
4077			let mut cfg = parity_db::Options::with_columns(&db_path, col::COUNT);
4078			let statement_col = &mut cfg.columns[col::STATEMENTS as usize];
4079			statement_col.ref_counted = false;
4080			statement_col.preimage = true;
4081			statement_col.uniform = true;
4082			for c in crate::BTREE_COLUMNS {
4083				cfg.columns[c as usize].btree_index = true;
4084			}
4085			let db = parity_db::Db::open_or_create(&cfg).unwrap();
4086			let mut commit: Vec<(u8, Vec<u8>, Option<Vec<u8>>)> = vec![
4087				(col::META, b"version".to_vec(), Some(1u32.to_le_bytes().to_vec())),
4088				(col::STATEMENTS, s1.hash().to_vec(), Some(s1.encode())),
4089				(col::STATEMENTS, s2.hash().to_vec(), Some(s2.encode())),
4090				(col::ADMISSION_SEQ, 7u64.to_be_bytes().to_vec(), Some(s1.hash().to_vec())),
4091			];
4092			commit.extend(crate::statement_index_ops(&s1.hash(), &s1, true));
4093			db.commit(commit).unwrap();
4094		}
4095
4096		let store = Store::new::<Block, TestClient, TestBackend>(
4097			&path,
4098			Default::default(),
4099			std::sync::Arc::new(TestClient),
4100			std::sync::Arc::new(sc_keystore::LocalKeystore::in_memory()),
4101			None,
4102			Box::new(sp_core::testing::TaskExecutor::new()),
4103		)
4104		.unwrap();
4105
4106		// Everything was rebuilt: bodies, read index, account and expiry indexes, counters.
4107		assert_eq!(store.statements().unwrap().len(), 2);
4108		assert_eq!(store.statement_count(), 2);
4109		assert_eq!(store.total_size(), 1 + 200);
4110		assert!(store.has_account(&s1.account_id().unwrap()));
4111		assert!(store.has_account(&account(2)));
4112		assert!(store
4113			.db
4114			.get(
4115				col::INDEX_BY_EXPIRY,
4116				&crate::expiry_index_key(crate::Expiry(s2.expiry()), &s2.hash())
4117			)
4118			.unwrap()
4119			.is_some());
4120
4121		// s1 kept the sequence number the first attempt assigned; s2 got a fresh one above it.
4122		assert_eq!(
4123			store.db.get(col::ADMISSION_SEQ, &7u64.to_be_bytes()).unwrap(),
4124			Some(s1.hash().to_vec())
4125		);
4126		assert_eq!(
4127			store.db.get(col::ADMISSION_SEQ, &8u64.to_be_bytes()).unwrap(),
4128			Some(s2.hash().to_vec())
4129		);
4130		assert_eq!(store.submit_index.read().next_seq, 9);
4131		let replay = store.replay_batch(&OptimizedTopicFilter::Any, 7, 9).unwrap();
4132		assert_eq!(replay.statements, vec![s1.encode(), s2.encode()]);
4133
4134		// Channel state was rebuilt from the bodies: a lower-priority replacement is rejected.
4135		assert!(matches!(
4136			store.submit(statement(2, 4, Some(9), 100), StatementSource::Network),
4137			SubmitResult::Rejected(RejectionReason::ChannelPriorityTooLow { .. })
4138		));
4139	}
4140
4141	#[test]
4142	fn counters_and_seq_survive_restart() {
4143		let (store, temp) = test_store();
4144		for i in 0..3u8 {
4145			assert_eq!(
4146				store.submit(signed_statement(i), StatementSource::Network),
4147				SubmitResult::New
4148			);
4149		}
4150		assert_eq!(
4151			store.submit(statement(1, 1, None, 100), StatementSource::Network),
4152			SubmitResult::New
4153		);
4154		assert_eq!(store.statement_count(), 4);
4155		assert_eq!(store.total_size(), 3 + 100);
4156		let keystore = store.keystore.clone();
4157		drop(store);
4158
4159		let mut path: std::path::PathBuf = temp.path().into();
4160		path.push("db");
4161		let store = Store::new::<Block, TestClient, TestBackend>(
4162			&path,
4163			Default::default(),
4164			std::sync::Arc::new(TestClient),
4165			keystore,
4166			None,
4167			Box::new(sp_core::testing::TaskExecutor::new()),
4168		)
4169		.unwrap();
4170		assert_eq!(store.statement_count(), 4);
4171		assert_eq!(store.total_size(), 3 + 100);
4172		assert_eq!(store.submit_index.read().next_seq, 4);
4173		// `signed_statement` signs as Alice, so two distinct accounts are stored.
4174		assert_eq!(store.account_count(), 2);
4175	}
4176
4177	#[test]
4178	fn resubmitting_live_statement_is_known_and_not_double_counted() {
4179		let (store, _temp) = test_store();
4180		let statement = statement(1, 1, None, 100);
4181		assert_eq!(store.submit(statement.clone(), StatementSource::Local), SubmitResult::New);
4182		assert_eq!(store.total_size(), 100);
4183		assert_eq!(store.submit(statement.clone(), StatementSource::Local), SubmitResult::Known);
4184		assert_eq!(store.submit(statement, StatementSource::Network), SubmitResult::Known);
4185		assert_eq!(store.total_size(), 100);
4186		assert_eq!(store.statement_count(), 1);
4187	}
4188
4189	#[test]
4190	fn eviction_clears_all_index_rows() {
4191		let (mut store, _temp) = test_store();
4192		store.set_time(0);
4193		let mut stmt = unsigned_statement(1, 1, Some(7), 100);
4194		stmt.set_topic(0, topic(3));
4195		stmt.set_expiry_from_parts(500, 1);
4196		sign_with(&mut stmt, 1);
4197		let hash = stmt.hash();
4198		let expiry = crate::Expiry(stmt.expiry());
4199		let who = account(1);
4200		assert_eq!(store.submit(stmt, StatementSource::Network), SubmitResult::New);
4201		let account_key = crate::account_index_key(&who, expiry, &hash);
4202		let expiry_key = crate::expiry_index_key(expiry, &hash);
4203		assert!(store.db.get(col::INDEX_BY_ACCOUNT, &account_key).unwrap().is_some());
4204		assert!(store.db.get(col::INDEX_BY_EXPIRY, &expiry_key).unwrap().is_some());
4205
4206		store.remove(&hash).unwrap();
4207		assert!(store.db.get(col::INDEX_BY_ACCOUNT, &account_key).unwrap().is_none());
4208		assert!(store.db.get(col::INDEX_BY_EXPIRY, &expiry_key).unwrap().is_none());
4209		assert!(!store.index_has_topic(&topic(3), &hash));
4210		assert_eq!(store.db.get(col::ADMISSION_SEQ, &0u64.to_be_bytes()).unwrap(), None);
4211		// Removed before its natural expiry: banned via the evicted journal.
4212		assert!(store.is_evicted(&hash));
4213		assert_eq!(store.statement_count(), 0);
4214		assert_eq!(store.total_size(), 0);
4215	}
4216
4217	#[test]
4218	fn index_key_layouts_roundtrip() {
4219		// The key builders and their parsers are the single place that knows the on-disk key
4220		// layouts; whoever changes one side must keep the other in sync, and this test is the
4221		// tripwire for that.
4222		use crate::{
4223			account_index_key, dec_key_index_key, dec_key_index_prefix, evicted_index_key,
4224			expiry_index_key, hash_from_index_key, parse_account_index_key,
4225			parse_dec_key_index_prefix, parse_time_index_key, prefix_from_index_key,
4226			topic_index_key, Expiry,
4227		};
4228
4229		let who = account(7);
4230		let hash: crate::Hash = account(8);
4231		let expiry = Expiry((123u64 << 32) | 45);
4232
4233		let key = account_index_key(&who, expiry, &hash);
4234		assert_eq!(parse_account_index_key(&key), Some((who, expiry, hash)));
4235		// Any other length is rejected rather than misparsed.
4236		assert_eq!(parse_account_index_key(&key[..key.len() - 1]), None);
4237		assert_eq!(parse_account_index_key(&[]), None);
4238
4239		let key = expiry_index_key(expiry, &hash);
4240		assert_eq!(parse_time_index_key(&key), Some((expiry.0, hash)));
4241		assert_eq!(parse_time_index_key(&key[1..]), None);
4242
4243		let key = evicted_index_key(789, &hash);
4244		assert_eq!(parse_time_index_key(&key), Some((789, hash)));
4245
4246		// Big-endian time keys keep numeric order; the expiry sweep and the evicted journal
4247		// drain rely on it to stop at the first entry that is not yet due.
4248		assert!(expiry_index_key(Expiry(256), &hash) > expiry_index_key(Expiry(1), &hash));
4249		assert!(evicted_index_key(256, &hash) > evicted_index_key(1, &hash));
4250
4251		let key = topic_index_key(&topic(3), &hash);
4252		assert_eq!(hash_from_index_key(&key), Some(hash));
4253		assert_eq!(prefix_from_index_key(&key), Some(&topic(3)[..]));
4254
4255		for dec_key in [None, Some(dec_key(5))] {
4256			assert_eq!(parse_dec_key_index_prefix(&dec_key_index_prefix(&dec_key)), Some(dec_key));
4257			let key = dec_key_index_key(&dec_key, &hash);
4258			assert_eq!(hash_from_index_key(&key), Some(hash));
4259			assert_eq!(
4260				prefix_from_index_key(&key).and_then(parse_dec_key_index_prefix),
4261				Some(dec_key)
4262			);
4263		}
4264	}
4265
4266	#[test]
4267	fn summary_fast_path_stays_coherent_with_disk() {
4268		use crate::{DETAILS_CACHE_BUDGET, SUMMARY_CACHE_ACCOUNTS};
4269
4270		let (store, _temp) = test_store();
4271		let source = StatementSource::Network;
4272
4273		// The test walks three accounts (5, 6 and 7); every summary must stay cached throughout.
4274		assert!(SUMMARY_CACHE_ACCOUNTS >= 3, "three account summaries must fit the cache");
4275		// Two filler accounts grow their records until the details cache overflows by at least
4276		// one statement, so that account 5's record (of one statement) is the one evicted.
4277		let per_filler_account = DETAILS_CACHE_BUDGET.div_ceil(2);
4278		assert!(
4279			per_filler_account <= 100,
4280			"the filler accounts' allowance (100 statements, 1000 bytes of data) caps the cache \
4281			 budget this test can exercise",
4282		);
4283
4284		// Account 5's record enters the details cache with its first statement.
4285		let s5_1 = statement(5, 1, None, 100);
4286		assert_eq!(store.submit(s5_1.clone(), source), SubmitResult::New);
4287		assert!(store.details_cached(&account(5)));
4288
4289		// Grow the filler accounts' records past the cache budget; account 5's record is the
4290		// least recently used one, so it is evicted, while its summary stays cached.
4291		for acc in [6u64, 7] {
4292			for c in 1..=per_filler_account as u64 {
4293				assert_eq!(store.submit(statement(acc, 1, Some(c), 10), source), SubmitResult::New);
4294			}
4295		}
4296		assert!(!store.details_cached(&account(5)));
4297
4298		// A channel-less statement within the account's allowance is admitted through the
4299		// summary alone: the record is not rematerialised.
4300		let s5_2 = statement(5, 2, None, 50);
4301		assert_eq!(store.submit(s5_2.clone(), source), SubmitResult::New);
4302		assert!(!store.details_cached(&account(5)));
4303
4304		// Pushing the account over its size allowance (1000 bytes) forces the record to be
4305		// loaded back from disk. It must contain both earlier statements — including the one
4306		// admitted through the fast path — and evict exactly the lowest-priority one.
4307		let s5_3 = statement(5, 3, None, 900);
4308		assert_eq!(store.submit(s5_3.clone(), source), SubmitResult::New);
4309		assert!(store.details_cached(&account(5)));
4310		assert!(!store.has_statement(&s5_1.hash()), "lowest priority must be evicted");
4311		assert!(store.has_statement(&s5_2.hash()));
4312		assert!(store.has_statement(&s5_3.hash()));
4313		let filler_statements = 2 * per_filler_account;
4314		assert_eq!(store.statement_count(), 2 + filler_statements);
4315		assert_eq!(store.total_size(), 50 + 900 + filler_statements * 10);
4316	}
4317
4318	#[test]
4319	fn rejected_submission_caches_the_loaded_record() {
4320		let (store, temp) = test_store();
4321		let source = StatementSource::Network;
4322
4323		// Account 5 owns one channel statement.
4324		assert_eq!(store.submit(statement(5, 2, Some(1), 100), source), SubmitResult::New);
4325
4326		// Reopen the store so that both caches start cold.
4327		let keystore = store.keystore.clone();
4328		drop(store);
4329		let mut path: std::path::PathBuf = temp.path().into();
4330		path.push("db");
4331		let store = Store::new::<Block, TestClient, TestBackend>(
4332			&path,
4333			Default::default(),
4334			std::sync::Arc::new(TestClient),
4335			keystore,
4336			None,
4337			Box::new(sp_core::testing::TaskExecutor::new()),
4338		)
4339		.unwrap();
4340
4341		// An oversize statement is rejected before the account record is even loaded, so the
4342		// caches stay cold.
4343		assert_eq!(
4344			store.submit(statement(5, 3, None, 1500), source),
4345			SubmitResult::Rejected(RejectionReason::DataTooLarge {
4346				submitted_size: 1500,
4347				available_size: 1000,
4348			})
4349		);
4350		assert!(!store.details_cached(&account(5)));
4351		assert!(!store.summary_cached(&account(5)));
4352
4353		// A channel statement of too low a priority is rejected only after planning against the
4354		// record loaded from disk. The record and its summary must stay cached: rejections cost
4355		// the sender nothing, so retries must not rescan the on-disk index every time.
4356		assert!(matches!(
4357			store.submit(statement(5, 1, Some(1), 100), source),
4358			SubmitResult::Rejected(RejectionReason::ChannelPriorityTooLow { .. })
4359		));
4360		assert!(store.details_cached(&account(5)));
4361		assert!(store.summary_cached(&account(5)));
4362	}
4363
4364	#[test]
4365	fn take_recent_statements_clears_index() {
4366		let (store, _temp) = test_store();
4367		let statement0 = signed_statement(0);
4368		let statement1 = signed_statement(1);
4369		let statement2 = signed_statement(2);
4370		let statement3 = signed_statement(3);
4371
4372		let _ = store.submit(statement0.clone(), StatementSource::Local);
4373		let _ = store.submit(statement1.clone(), StatementSource::Local);
4374		let _ = store.submit(statement2.clone(), StatementSource::Local);
4375
4376		let recent1 = store.take_recent_statements().unwrap();
4377		let (recent1_hashes, recent1_statements): (Vec<_>, Vec<_>) =
4378			recent1.into_iter().map(|(_seq, hash, statement)| (hash, statement)).unzip();
4379		let expected1 = vec![statement0, statement1, statement2];
4380		assert!(expected1.iter().all(|s| recent1_hashes.contains(&s.hash())));
4381		assert!(expected1.iter().all(|s| recent1_statements.contains(s)));
4382
4383		// Recent statements are cleared.
4384		let recent2 = store.take_recent_statements().unwrap();
4385		assert_eq!(recent2.len(), 0);
4386
4387		store.submit(statement3.clone(), StatementSource::Network);
4388
4389		let recent3 = store.take_recent_statements().unwrap();
4390		let (recent3_hashes, recent3_statements): (Vec<_>, Vec<_>) =
4391			recent3.into_iter().map(|(_seq, hash, statement)| (hash, statement)).unzip();
4392		let expected3 = vec![statement3];
4393		assert!(expected3.iter().all(|s| recent3_hashes.contains(&s.hash())));
4394		assert!(expected3.iter().all(|s| recent3_statements.contains(s)));
4395
4396		// Recent statements are cleared, but statements remain in the store.
4397		assert_eq!(store.statements().unwrap().len(), 4);
4398	}
4399
4400	#[test]
4401	fn search_by_topic_and_key() {
4402		let (store, _temp) = test_store();
4403		let statement0 = signed_statement(0);
4404		let statement1 = signed_statement_with_topics(1, &[topic(0)], None);
4405		let statement2 = signed_statement_with_topics(2, &[topic(0), topic(1)], Some(dec_key(2)));
4406		let statement3 = signed_statement_with_topics(3, &[topic(0), topic(1), topic(2)], None);
4407		let statement4 =
4408			signed_statement_with_topics(4, &[topic(0), topic(42), topic(2), topic(3)], None);
4409		let statements = vec![statement0, statement1, statement2, statement3, statement4];
4410		for s in &statements {
4411			store.submit(s.clone(), StatementSource::Network);
4412		}
4413
4414		let assert_topics = |topics: &[u64], key: Option<u64>, expected: &[u8]| {
4415			let key = key.map(dec_key);
4416			let topics: Vec<_> = topics.iter().map(|t| topic(*t)).collect();
4417			let mut got_vals: Vec<_> = if let Some(key) = key {
4418				store.posted(&topics, key).unwrap().into_iter().map(|d| d[0]).collect()
4419			} else {
4420				store.broadcasts(&topics).unwrap().into_iter().map(|d| d[0]).collect()
4421			};
4422			got_vals.sort();
4423			assert_eq!(expected.to_vec(), got_vals);
4424		};
4425
4426		assert_topics(&[], None, &[0, 1, 3, 4]);
4427		assert_topics(&[], Some(2), &[2]);
4428		assert_topics(&[0], None, &[1, 3, 4]);
4429		assert_topics(&[1], None, &[3]);
4430		assert_topics(&[2], None, &[3, 4]);
4431		assert_topics(&[3], None, &[4]);
4432		assert_topics(&[42], None, &[4]);
4433
4434		assert_topics(&[0, 1], None, &[3]);
4435		assert_topics(&[0, 1], Some(2), &[2]);
4436		assert_topics(&[0, 1, 99], Some(2), &[]);
4437		assert_topics(&[1, 2], None, &[3]);
4438		assert_topics(&[99], None, &[]);
4439		assert_topics(&[0, 99], None, &[]);
4440		assert_topics(&[0, 1, 2, 3, 42], None, &[]);
4441	}
4442
4443	#[test]
4444	fn constraints() {
4445		let (store, _temp) = test_store();
4446
4447		store.submit_index.write().config.max_total_size = 3000;
4448		let source = StatementSource::Network;
4449		let ok = SubmitResult::New;
4450
4451		// Account 1 (limit = 1 msg, 1000 bytes)
4452
4453		// Oversized statement is not allowed. Limit for account 1 is 1 msg, 1000 bytes
4454		assert!(matches!(
4455			store.submit(statement(1, 1, Some(1), 2000), source),
4456			SubmitResult::Rejected(_)
4457		));
4458		assert_eq!(store.submit(statement(1, 1, Some(1), 500), source), ok);
4459		// Would not replace channel message with same priority
4460		assert!(matches!(
4461			store.submit(statement(1, 1, Some(1), 200), source),
4462			SubmitResult::Rejected(_)
4463		));
4464		assert_eq!(store.submit(statement(1, 2, Some(1), 600), source), ok);
4465		// Submit another message to another channel with lower priority. Should not be allowed
4466		// because msg count limit is 1
4467		assert!(matches!(
4468			store.submit(statement(1, 1, Some(2), 100), source),
4469			SubmitResult::Rejected(_)
4470		));
4471		assert_eq!(store.evicted_count(), 1);
4472
4473		// Account 2 (limit = 2 msg, 1000 bytes)
4474
4475		let s2_prio1 = statement(2, 1, None, 500);
4476		let s2_prio2 = statement(2, 2, None, 100);
4477		assert_eq!(store.submit(s2_prio1.clone(), source), ok);
4478		assert_eq!(store.submit(s2_prio2.clone(), source), ok);
4479		// Equal priority to lowest should be rejected
4480		assert!(matches!(
4481			store.submit(statement(2, 1, None, 50), source),
4482			SubmitResult::Rejected(RejectionReason::AccountFull { .. })
4483		));
4484		// Should evict priority 1
4485		let s2_prio3 = statement(2, 3, None, 500);
4486		assert_eq!(store.submit(s2_prio3.clone(), source), ok);
4487		assert_eq!(store.evicted_count(), 2);
4488		assert!(store.is_evicted(&s2_prio1.hash()));
4489		assert!(store.statement(&s2_prio1.hash()).unwrap().is_none());
4490		// Should evict all
4491		assert_eq!(store.submit(statement(2, 4, None, 1000), source), ok);
4492		assert_eq!(store.evicted_count(), 4);
4493		assert!(store.is_evicted(&s2_prio2.hash()));
4494		assert!(store.is_evicted(&s2_prio3.hash()));
4495
4496		// Account 3 (limit = 3 msg, 1000 bytes)
4497
4498		let s3_prio2 = statement(3, 2, Some(1), 300);
4499		let s3_prio3 = statement(3, 3, Some(2), 300);
4500		assert_eq!(store.submit(s3_prio2.clone(), source), ok);
4501		assert_eq!(store.submit(s3_prio3.clone(), source), ok);
4502		assert_eq!(store.submit(statement(3, 4, Some(3), 300), source), ok);
4503		// Should evict 2 and 3
4504		assert_eq!(store.submit(statement(3, 5, None, 500), source), ok);
4505		assert_eq!(store.evicted_count(), 6);
4506		assert!(store.is_evicted(&s3_prio2.hash()));
4507		assert!(store.is_evicted(&s3_prio3.hash()));
4508
4509		assert_eq!(store.total_size(), 2400);
4510		assert_eq!(store.statement_count(), 4);
4511
4512		// Should be over the global size limit
4513		assert!(matches!(
4514			store.submit(statement(1, 1, None, 700), source),
4515			SubmitResult::Rejected(_)
4516		));
4517		// Should be over the global count limit
4518		store.submit_index.write().config.max_total_statements = 4;
4519		assert!(matches!(
4520			store.submit(statement(1, 1, None, 100), source),
4521			SubmitResult::Rejected(_)
4522		));
4523
4524		let mut expected_statements = vec![
4525			statement(1, 2, Some(1), 600).hash(),
4526			statement(2, 4, None, 1000).hash(),
4527			statement(3, 4, Some(3), 300).hash(),
4528			statement(3, 5, None, 500).hash(),
4529		];
4530		expected_statements.sort();
4531		let mut statements: Vec<_> =
4532			store.statements().unwrap().into_iter().map(|(hash, _)| hash).collect();
4533		statements.sort();
4534		assert_eq!(expected_statements, statements);
4535	}
4536
4537	#[test]
4538	fn insufficient_remaining_account_bytes_is_data_too_large() {
4539		let (store, _temp) = test_store();
4540		let source = StatementSource::Network;
4541
4542		// Account 4 allows four statements and 1000 data bytes. The count limit has room, but the
4543		// lower-priority submission cannot evict the existing statement to obtain enough bytes.
4544		assert_eq!(store.submit(statement(4, 2, None, 900), source), SubmitResult::New);
4545		assert_eq!(
4546			store.submit(statement(4, 1, None, 200), source),
4547			SubmitResult::Rejected(RejectionReason::DataTooLarge {
4548				submitted_size: 200,
4549				available_size: 100,
4550			})
4551		);
4552	}
4553
4554	#[test]
4555	fn max_statement_size_for_gossiping() {
4556		let (store, _temp) = test_store();
4557		store.submit_index.write().config.max_total_size = 42 * crate::MAX_STATEMENT_SIZE;
4558
4559		assert_eq!(
4560			store.submit(
4561				statement(42, 1, Some(1), crate::MAX_STATEMENT_SIZE - 500),
4562				StatementSource::Local
4563			),
4564			SubmitResult::New
4565		);
4566
4567		assert!(matches!(
4568			store.submit(
4569				statement(42, 2, Some(1), 2 * crate::MAX_STATEMENT_SIZE),
4570				StatementSource::Local
4571			),
4572			SubmitResult::Invalid(_)
4573		));
4574	}
4575
4576	#[test]
4577	fn expired_statements_are_purged() {
4578		use super::DEFAULT_PURGE_AFTER_SEC;
4579		let (mut store, temp) = test_store();
4580		let mut statement = unsigned_statement(1, 1, Some(3), 100);
4581		store.set_time(0);
4582		statement.set_topic(0, topic(4));
4583		sign_with(&mut statement, 1);
4584		store.submit(statement.clone(), StatementSource::Network);
4585		assert_eq!(store.statement_count(), 1);
4586		store.remove(&statement.hash()).unwrap();
4587		assert_eq!(store.statement_count(), 0);
4588		assert_eq!(store.account_count(), 0);
4589		store.set_time(DEFAULT_PURGE_AFTER_SEC + 1);
4590		store.maintain();
4591		assert_eq!(store.evicted_count(), 0);
4592		let keystore = store.keystore.clone();
4593		drop(store);
4594
4595		let client = std::sync::Arc::new(TestClient);
4596		let mut path: std::path::PathBuf = temp.path().into();
4597		path.push("db");
4598		let store = Store::new::<Block, TestClient, TestBackend>(
4599			&path,
4600			Default::default(),
4601			client,
4602			keystore,
4603			None,
4604			Box::new(sp_core::testing::TaskExecutor::new()),
4605		)
4606		.unwrap();
4607		assert_eq!(store.statements().unwrap().len(), 0);
4608		assert_eq!(store.evicted_count(), 0);
4609	}
4610
4611	#[test]
4612	fn posted_clear_decrypts() {
4613		let (store, _temp) = test_store();
4614		let public = store
4615			.keystore
4616			.ed25519_generate_new(sp_core::crypto::key_types::STATEMENT, None)
4617			.unwrap();
4618		let statement1 = statement(1, 1, None, 100);
4619		let mut statement2 = unsigned_statement(1, 2, None, 0);
4620		let plain = b"The most valuable secret".to_vec();
4621		statement2.encrypt(&plain, &public).unwrap();
4622		sign_with(&mut statement2, 1);
4623		store.submit(statement1, StatementSource::Network);
4624		store.submit(statement2, StatementSource::Network);
4625		let posted_clear = store.posted_clear(&[], public.into()).unwrap();
4626		assert_eq!(posted_clear, vec![plain]);
4627	}
4628
4629	#[test]
4630	fn broadcasts_stmt_returns_encoded_statements() {
4631		let (store, _tmp) = test_store();
4632
4633		// no key, no topic
4634		let s0 = signed_statement_with_topics(0, &[], None);
4635		// same, but with a topic = 42
4636		let s1 = signed_statement_with_topics(1, &[topic(42)], None);
4637		// has a decryption key -> must NOT be returned by broadcasts_stmt
4638		let s2 = signed_statement_with_topics(2, &[topic(42)], Some(dec_key(99)));
4639
4640		for s in [&s0, &s1, &s2] {
4641			store.submit(s.clone(), StatementSource::Network);
4642		}
4643
4644		// no topic filter
4645		let mut hashes: Vec<_> = store
4646			.broadcasts_stmt(&[])
4647			.unwrap()
4648			.into_iter()
4649			.map(|bytes| Statement::decode(&mut &bytes[..]).unwrap().hash())
4650			.collect();
4651		hashes.sort();
4652		let expected_hashes = {
4653			let mut e = vec![s0.hash(), s1.hash()];
4654			e.sort();
4655			e
4656		};
4657		assert_eq!(hashes, expected_hashes);
4658
4659		// filter on topic 42
4660		let got = store.broadcasts_stmt(&[topic(42)]).unwrap();
4661		assert_eq!(got.len(), 1);
4662		let st = Statement::decode(&mut &got[0][..]).unwrap();
4663		assert_eq!(st.hash(), s1.hash());
4664	}
4665
4666	#[test]
4667	fn posted_stmt_returns_encoded_statements_for_dest() {
4668		let (store, _tmp) = test_store();
4669
4670		let public1 = store
4671			.keystore
4672			.ed25519_generate_new(sp_core::crypto::key_types::STATEMENT, None)
4673			.unwrap();
4674		let dest: [u8; 32] = public1.into();
4675
4676		let public2 = store
4677			.keystore
4678			.ed25519_generate_new(sp_core::crypto::key_types::STATEMENT, None)
4679			.unwrap();
4680
4681		// A statement that does have dec_key = dest
4682		let mut s_with_key = unsigned_statement(1, 1, None, 0);
4683		let plain1 = b"The most valuable secret".to_vec();
4684		s_with_key.encrypt(&plain1, &public1).unwrap();
4685		sign_with(&mut s_with_key, 1);
4686
4687		// A statement with a different dec_key
4688		let mut s_other_key = unsigned_statement(2, 2, None, 0);
4689		let plain2 = b"The second most valuable secret".to_vec();
4690		s_other_key.encrypt(&plain2, &public2).unwrap();
4691		sign_with(&mut s_other_key, 2);
4692
4693		// Submit them all
4694		for s in [&s_with_key, &s_other_key] {
4695			store.submit(s.clone(), StatementSource::Network);
4696		}
4697
4698		// posted_stmt should only return the one with dec_key = dest
4699		let retrieved = store.posted_stmt(&[], dest).unwrap();
4700		assert_eq!(retrieved.len(), 1, "Only one statement has dec_key=dest");
4701
4702		// Re-decode that returned statement to confirm it is correct
4703		let returned_stmt = Statement::decode(&mut &retrieved[0][..]).unwrap();
4704		assert_eq!(
4705			returned_stmt.hash(),
4706			s_with_key.hash(),
4707			"Returned statement must match s_with_key"
4708		);
4709	}
4710
4711	#[test]
4712	fn posted_clear_stmt_returns_statement_followed_by_plain_data() {
4713		let (store, _tmp) = test_store();
4714
4715		let public1 = store
4716			.keystore
4717			.ed25519_generate_new(sp_core::crypto::key_types::STATEMENT, None)
4718			.unwrap();
4719		let dest: [u8; 32] = public1.into();
4720
4721		let public2 = store
4722			.keystore
4723			.ed25519_generate_new(sp_core::crypto::key_types::STATEMENT, None)
4724			.unwrap();
4725
4726		// A statement that does have dec_key = dest
4727		let mut s_with_key = unsigned_statement(1, 1, None, 0);
4728		let plain1 = b"The most valuable secret".to_vec();
4729		s_with_key.encrypt(&plain1, &public1).unwrap();
4730		sign_with(&mut s_with_key, 1);
4731
4732		// A statement with a different dec_key
4733		let mut s_other_key = unsigned_statement(2, 2, None, 0);
4734		let plain2 = b"The second most valuable secret".to_vec();
4735		s_other_key.encrypt(&plain2, &public2).unwrap();
4736		sign_with(&mut s_other_key, 2);
4737
4738		// Submit them all
4739		for s in [&s_with_key, &s_other_key] {
4740			store.submit(s.clone(), StatementSource::Network);
4741		}
4742
4743		// posted_stmt should only return the one with dec_key = dest
4744		let retrieved = store.posted_clear_stmt(&[], dest).unwrap();
4745		assert_eq!(retrieved.len(), 1, "Only one statement has dec_key=dest");
4746
4747		// We expect: [ encoded Statement ] + [ the decrypted bytes ]
4748		let encoded_stmt = s_with_key.encode();
4749		let stmt_len = encoded_stmt.len();
4750
4751		// 1) statement is first
4752		assert_eq!(&retrieved[0][..stmt_len], &encoded_stmt[..]);
4753
4754		// 2) followed by the decrypted payload
4755		let trailing = &retrieved[0][stmt_len..];
4756		assert_eq!(trailing, &plain1[..]);
4757	}
4758
4759	#[test]
4760	fn posted_clear_returns_plain_data_for_dest_and_topics() {
4761		let (store, _tmp) = test_store();
4762
4763		// prepare two key-pairs
4764		let public_dest = store
4765			.keystore
4766			.ed25519_generate_new(sp_core::crypto::key_types::STATEMENT, None)
4767			.unwrap();
4768		let dest: [u8; 32] = public_dest.into();
4769
4770		let public_other = store
4771			.keystore
4772			.ed25519_generate_new(sp_core::crypto::key_types::STATEMENT, None)
4773			.unwrap();
4774
4775		// statement that SHOULD be returned (matches dest & topic 42)
4776		let mut s_good = unsigned_statement(1, 1, None, 0);
4777		let plaintext_good = b"The most valuable secret".to_vec();
4778		s_good.encrypt(&plaintext_good, &public_dest).unwrap();
4779		s_good.set_topic(0, topic(42));
4780		sign_with(&mut s_good, 1);
4781
4782		// statement that should NOT be returned (same dest but different topic)
4783		let mut s_wrong_topic = unsigned_statement(2, 2, None, 0);
4784		s_wrong_topic.encrypt(b"Wrong topic", &public_dest).unwrap();
4785		s_wrong_topic.set_topic(0, topic(99));
4786		sign_with(&mut s_wrong_topic, 2);
4787
4788		// statement that should NOT be returned (different dest)
4789		let mut s_other_dest = unsigned_statement(3, 3, None, 0);
4790		s_other_dest.encrypt(b"Other dest", &public_other).unwrap();
4791		s_other_dest.set_topic(0, topic(42));
4792		sign_with(&mut s_other_dest, 3);
4793
4794		// submit all
4795		for s in [&s_good, &s_wrong_topic, &s_other_dest] {
4796			store.submit(s.clone(), StatementSource::Network);
4797		}
4798
4799		// call posted_clear with the topic filter and dest
4800		let retrieved = store.posted_clear(&[topic(42)], dest).unwrap();
4801
4802		// exactly one element, equal to the expected plaintext
4803		assert_eq!(retrieved, vec![plaintext_good]);
4804	}
4805
4806	#[test]
4807	fn already_expired_statement_is_rejected() {
4808		let (mut store, _temp) = test_store();
4809
4810		// Set current time to 1000 seconds
4811		store.set_time(1000);
4812
4813		// Create a statement that has already expired (expiration at 500 seconds, before current
4814		// time)
4815		let mut expired_statement = unsigned_statement(1, 1, None, 100);
4816		// set_expiry_from_parts: first arg is expiration timestamp in seconds, second is priority
4817		expired_statement.set_expiry_from_parts(500, 1);
4818		sign_with(&mut expired_statement, 1);
4819
4820		// Submit should fail with AlreadyExpired
4821		assert_eq!(
4822			store.submit(expired_statement, StatementSource::Network),
4823			SubmitResult::Invalid(InvalidReason::AlreadyExpired)
4824		);
4825
4826		// Verify the statement was not added
4827		assert_eq!(store.statements().unwrap().len(), 0);
4828
4829		// Now create a statement that is not expired (expiration at 2000 seconds, after current
4830		// time)
4831		let mut valid_statement = unsigned_statement(1, 1, None, 100);
4832		valid_statement.set_expiry_from_parts(2000, 1);
4833		sign_with(&mut valid_statement, 1);
4834
4835		// Submit should succeed
4836		assert_eq!(store.submit(valid_statement, StatementSource::Network), SubmitResult::New);
4837		assert_eq!(store.statements().unwrap().len(), 1);
4838	}
4839
4840	#[test]
4841	fn remove_by_covers_various_situations() {
4842		use sp_statement_store::{StatementSource, StatementStore, SubmitResult};
4843
4844		// Use a fresh store and fixed time so we can control purging.
4845		let (mut store, _temp) = test_store();
4846		store.set_time(0);
4847
4848		// Reuse helpers from this module.
4849		let t42 = topic(42);
4850		let k7 = dec_key(7);
4851
4852		// Account A = 4 (has per-account limits (4, 1000) in the mock runtime)
4853		// - Mix of topic, decryption-key and channel to exercise every index.
4854		let mut s_a1 = unsigned_statement(4, 10, Some(100), 100);
4855		s_a1.set_topic(0, t42);
4856		sign_with(&mut s_a1, 4);
4857		let h_a1 = s_a1.hash();
4858
4859		let mut s_a2 = unsigned_statement(4, 20, Some(200), 150);
4860		s_a2.set_decryption_key(k7);
4861		sign_with(&mut s_a2, 4);
4862		let h_a2 = s_a2.hash();
4863
4864		let s_a3 = statement(4, 30, None, 50);
4865		let h_a3 = s_a3.hash();
4866
4867		// Account B = 3 (control group that must remain untouched).
4868		let s_b1 = statement(3, 10, None, 100);
4869		let h_b1 = s_b1.hash();
4870
4871		let mut s_b2 = unsigned_statement(3, 15, Some(300), 100);
4872		s_b2.set_topic(0, t42);
4873		s_b2.set_decryption_key(k7);
4874		sign_with(&mut s_b2, 3);
4875		let h_b2 = s_b2.hash();
4876
4877		// Submit all statements.
4878		for s in [&s_a1, &s_a2, &s_a3, &s_b1, &s_b2] {
4879			assert_eq!(store.submit(s.clone(), StatementSource::Network), SubmitResult::New);
4880		}
4881
4882		// --- Pre-conditions: everything is indexed as expected.
4883		{
4884			assert_eq!(store.statement_count(), 5, "all 5 should be present");
4885			assert!(store.has_account(&account(4)));
4886			assert!(store.has_account(&account(3)));
4887			assert_eq!(store.total_size(), 100 + 150 + 50 + 100 + 100);
4888
4889			// Topic and key sets contain both A & B entries.
4890			assert!(store.index_has_topic(&t42, &h_a1) && store.index_has_topic(&t42, &h_b2));
4891			assert!(
4892				store.index_has_dec_key(&Some(k7), &h_a2) &&
4893					store.index_has_dec_key(&Some(k7), &h_b2)
4894			);
4895		}
4896
4897		// --- Action: remove all statements by Account A.
4898		store.remove_by(account(4)).expect("remove_by should succeed");
4899
4900		// --- Post-conditions: A's statements are gone and marked expired; B's remain.
4901		{
4902			// A's statements removed from DB view.
4903			for h in [h_a1, h_a2, h_a3] {
4904				assert!(store.statement(&h).unwrap().is_none(), "A's statement should be removed");
4905			}
4906
4907			// B's statements still present.
4908			for h in [h_b1, h_b2] {
4909				assert!(store.statement(&h).unwrap().is_some(), "B's statement should remain");
4910			}
4911
4912			// Account index updated.
4913			assert!(!store.has_account(&account(4)), "Account A must be gone");
4914			assert!(store.has_account(&account(3)), "Account B must remain");
4915			// Entry count & total_size reflect only B's data.
4916			assert_eq!(store.statement_count(), 2);
4917			assert_eq!(store.total_size(), 100 + 100);
4918
4919			// Removed statements are banned in the on-disk evicted journal.
4920			assert!(store.is_evicted(&h_a1));
4921			assert!(store.is_evicted(&h_a2));
4922			assert!(store.is_evicted(&h_a3));
4923			assert_eq!(store.evicted_count(), 3);
4924
4925			// Topic index: only B2 remains for topic 42.
4926			assert!(store.index_has_topic(&t42, &h_b2));
4927			assert!(!store.index_has_topic(&t42, &h_a1));
4928
4929			// Decryption-key index: only B2 remains for key 7.
4930			assert!(store.index_has_dec_key(&Some(k7), &h_b2));
4931			assert!(!store.index_has_dec_key(&Some(k7), &h_a2));
4932		}
4933
4934		// --- Idempotency: removing again is a no-op and should not error.
4935		store.remove_by(account(4)).expect("second remove_by should be a no-op");
4936
4937		// --- Purge: advance time beyond TTL and run maintenance; expired entries disappear.
4938		let purge_after = store.submit_index.read().config.purge_after_sec;
4939		store.set_time(purge_after + 1);
4940		store.maintain();
4941		assert_eq!(store.evicted_count(), 0, "expired entries should be purged");
4942
4943		// --- Reuse: Account A can submit again after purge.
4944		let s_new = statement(4, 40, None, 10);
4945		assert_eq!(store.submit(s_new, StatementSource::Network), SubmitResult::New);
4946	}
4947
4948	#[test]
4949	fn check_expiration_expires_statements_past_current_time() {
4950		let (mut store, _temp) = test_store();
4951
4952		// The check_expiration function compares Expiry(current_time << 32) against
4953		// Expiry(expiry) where expiry is the full 64-bit value with timestamp in high 32 bits.
4954		// Statements with expiration timestamp < current_time will be expired.
4955
4956		store.set_time(100);
4957
4958		// Create a statement that will expire at timestamp 500
4959		let mut expired_stmt = unsigned_statement(1, 1, None, 100);
4960		expired_stmt.set_expiry_from_parts(500, 1);
4961		sign_with(&mut expired_stmt, 1);
4962		let expired_hash = expired_stmt.hash();
4963		store.submit(expired_stmt, StatementSource::Network);
4964
4965		// Create a statement that won't expire (far future expiry)
4966		let valid_stmt = statement(2, 1, None, 100); // Uses u32::MAX as timestamp
4967		let valid_hash = valid_stmt.hash();
4968		store.submit(valid_stmt, StatementSource::Network);
4969
4970		// Verify both statements are in the store
4971		assert_eq!(store.statement_count(), 2);
4972
4973		// Advance time past the expiry of the first statement
4974		store.set_time(1000);
4975
4976		// The sweep should find and expire the statement
4977		store.enforce_limits();
4978
4979		// Naturally-expired statements are not added to the expired map (AlreadyExpired check
4980		// in submit rejects them without consulting the map)
4981		assert!(
4982			!store.is_evicted(&expired_hash),
4983			"Naturally expired statement must not be added to the expired map"
4984		);
4985		assert!(
4986			!store.has_statement(&expired_hash),
4987			"Expired statement should be removed from the store"
4988		);
4989
4990		// The valid statement should still be stored
4991		assert!(store.has_statement(&valid_hash), "Valid statement should still be stored");
4992		assert!(!store.is_evicted(&valid_hash), "Valid statement should not be expired");
4993	}
4994
4995	#[test]
4996	fn allowance_sweep_resumes_from_cursor_across_calls() {
4997		use std::time::Duration;
4998
4999		let (mut store, _temp) = test_store();
5000		store.set_time(0);
5001
5002		// Five accounts, each 200 data bytes over its 1000-byte allowance, so enforcement
5003		// evicts exactly one statement per account processed.
5004		for seed in 10u64..15 {
5005			store.force_insert(&statement(seed, 1, None, 600));
5006			store.force_insert(&statement(seed, 2, None, 600));
5007		}
5008		assert_eq!(store.statement_count(), 10);
5009		assert!(store.submit_index.read().allowance_cursor.is_none());
5010
5011		// With a budget of two accounts per call, the sweep needs three calls to cover all
5012		// five, resuming from the stored cursor each time.
5013		store.enforce_limits_bounded(usize::MAX, 2, Duration::from_secs(3600));
5014		assert_eq!(store.statement_count(), 8);
5015		assert!(store.submit_index.read().allowance_cursor.is_some());
5016
5017		store.enforce_limits_bounded(usize::MAX, 2, Duration::from_secs(3600));
5018		assert_eq!(store.statement_count(), 6);
5019		assert!(store.submit_index.read().allowance_cursor.is_some());
5020
5021		// The last call processes the fifth account, wraps, resets the cursor and refreshes
5022		// the account gauge with the number of accounts the pass has seen.
5023		store.enforce_limits_bounded(usize::MAX, 2, Duration::from_secs(3600));
5024		assert_eq!(store.statement_count(), 5);
5025		assert!(store.submit_index.read().allowance_cursor.is_none());
5026		assert_eq!(store.known_accounts_count.load(std::sync::atomic::Ordering::Relaxed), 5);
5027
5028		// A further unrestricted pass finds every account within its allowance.
5029		store.enforce_limits();
5030		assert_eq!(store.statement_count(), 5);
5031	}
5032
5033	#[test]
5034	fn allowance_eviction_budget_defers_the_residue_to_the_next_pass() {
5035		use std::time::Duration;
5036
5037		let (mut store, _temp) = test_store();
5038		store.set_time(0);
5039
5040		// Account 1's allowance is a single statement: three of the four must be evicted.
5041		for priority in 1..=4 {
5042			store.force_insert(&statement(1, priority, None, 100));
5043		}
5044		assert_eq!(store.statement_count(), 4);
5045
5046		// The per-call eviction budget stops enforcement mid-account. The cursor has already
5047		// moved past the account, so it is not revisited within the same pass...
5048		store.enforce_limits_bounded(2, usize::MAX, Duration::from_secs(3600));
5049		assert_eq!(store.statement_count(), 2);
5050
5051		// ...the pass first has to wrap, finding no further accounts...
5052		store.enforce_limits_bounded(2, usize::MAX, Duration::from_secs(3600));
5053		assert_eq!(store.statement_count(), 2);
5054		assert!(store.submit_index.read().allowance_cursor.is_none());
5055
5056		// ...and the next pass evicts the remaining excess statement.
5057		store.enforce_limits_bounded(2, usize::MAX, Duration::from_secs(3600));
5058		assert_eq!(store.statement_count(), 1);
5059	}
5060
5061	#[test]
5062	fn expiry_sweep_is_bounded_by_the_statement_budget() {
5063		use std::time::Duration;
5064
5065		let (mut store, _temp) = test_store();
5066		store.set_time(100);
5067
5068		// Five statements, all past their expiry once the clock advances.
5069		for seed in 1u64..=5 {
5070			let mut stmt = unsigned_statement(seed, 1, None, 100);
5071			stmt.set_expiry_from_parts(200, 1);
5072			sign_with(&mut stmt, seed);
5073			assert_eq!(store.submit(stmt, StatementSource::Network), SubmitResult::New);
5074		}
5075		store.set_time(300);
5076
5077		// Each call reaps at most the statement budget off the expiry index; the index itself
5078		// is the resume point, so the backlog drains across calls with no cursor involved.
5079		store.enforce_limits_bounded(2, usize::MAX, Duration::from_secs(3600));
5080		assert_eq!(store.statement_count(), 3);
5081		store.enforce_limits_bounded(2, usize::MAX, Duration::from_secs(3600));
5082		assert_eq!(store.statement_count(), 1);
5083		store.enforce_limits_bounded(2, usize::MAX, Duration::from_secs(3600));
5084		assert_eq!(store.statement_count(), 0);
5085		// Naturally expired statements are not banned via the evicted journal.
5086		assert_eq!(store.evicted_count(), 0);
5087	}
5088
5089	#[test]
5090	fn expiry_sweep_leaves_inconsistent_data_in_place() {
5091		let (mut store, _temp) = test_store();
5092		store.set_time(100);
5093
5094		let expiry = crate::Expiry(200u64 << 32 | 1);
5095		let undecodable_hash = [0xAB_u8; 32];
5096		let undecodable_key = crate::expiry_index_key(expiry, &undecodable_hash);
5097		store
5098			.db
5099			.commit([
5100				(col::STATEMENTS, undecodable_hash.to_vec(), Some(vec![0xFF])),
5101				(col::INDEX_BY_EXPIRY, undecodable_key.clone(), Some(Vec::new())),
5102			])
5103			.unwrap();
5104
5105		// The sweep cannot remove the statement; the corruption is logged as an error and
5106		// everything is left exactly as it is, on every pass.
5107		store.set_time(300);
5108		store.enforce_limits();
5109		store.enforce_limits();
5110		assert!(store.has_statement(&undecodable_hash));
5111		assert!(store.db.get(col::INDEX_BY_EXPIRY, &undecodable_key).unwrap().is_some());
5112
5113		// Same for an expiry row with no body behind it at all: the sweep reports it and leaves
5114		// it in place.
5115		let orphan_hash = [0xCD_u8; 32];
5116		let orphan_key = crate::expiry_index_key(expiry, &orphan_hash);
5117		store
5118			.db
5119			.commit([(col::INDEX_BY_EXPIRY, orphan_key.clone(), Some(Vec::new()))])
5120			.unwrap();
5121		store.enforce_limits();
5122		assert!(!store.has_statement(&orphan_hash));
5123		assert!(store.db.get(col::INDEX_BY_EXPIRY, &orphan_key).unwrap().is_some());
5124	}
5125
5126	#[test]
5127	fn corrupt_account_index_key_does_not_end_account_enumeration() {
5128		let (store, _temp) = test_store();
5129		assert_eq!(
5130			store.submit(statement(1, 1, None, 1), StatementSource::Network),
5131			SubmitResult::New
5132		);
5133		assert_eq!(
5134			store.submit(statement(2, 1, None, 1), StatementSource::Network),
5135			SubmitResult::New
5136		);
5137		assert_eq!(store.account_count(), 2);
5138
5139		// Two unparseable keys: one sorting before every valid key (but after the scan's
5140		// starting cursor) and one sorting between the two accounts' rows. Both must be skipped
5141		// by account enumeration, not taken for the end of the index.
5142		let (first, second) = {
5143			let (a, b) = (account(1), account(2));
5144			if a < b {
5145				(a, b)
5146			} else {
5147				(b, a)
5148			}
5149		};
5150		let mut between = first.to_vec();
5151		between.extend_from_slice(&[0xFF; 41]);
5152		store
5153			.db
5154			.commit([
5155				(col::INDEX_BY_ACCOUNT, vec![0u8; 33], Some(Vec::new())),
5156				(col::INDEX_BY_ACCOUNT, between, Some(Vec::new())),
5157			])
5158			.unwrap();
5159
5160		assert_eq!(store.account_count(), 2);
5161		assert!(store.has_account(&first));
5162		assert!(store.has_account(&second));
5163
5164		// A full allowance pass still sees both accounts and reports the true account count.
5165		store.enforce_limits();
5166		assert_eq!(store.known_accounts_count.load(std::sync::atomic::Ordering::Relaxed), 2);
5167	}
5168
5169	#[test]
5170	fn concurrent_remove_and_resubmit_keep_query_bookkeeping_consistent() {
5171		// Exercises the #12624 interleaving: a `remove` racing a resubmission of the same
5172		// statement must never apply its query-index bookkeeping on top of the newer submit's.
5173		// The invariant below holds for any timing, so the test is deterministic even though
5174		// the race window itself is only hit probabilistically.
5175		use std::sync::Arc;
5176
5177		let (store, _temp) = test_store();
5178		let store = Arc::new(store);
5179		let mut stmt = unsigned_statement(1, 1, None, 100);
5180		stmt.set_topic(0, topic(7));
5181		sign_with(&mut stmt, 1);
5182		let hash = stmt.hash();
5183
5184		for _ in 0..200 {
5185			// The statement is live at the start of every round.
5186			if !store.has_statement(&hash) {
5187				assert_eq!(store.submit(stmt.clone(), StatementSource::Local), SubmitResult::New);
5188			}
5189			let remover = {
5190				let store = store.clone();
5191				std::thread::spawn(move || store.remove(&hash).expect("remove succeeds"))
5192			};
5193			let resubmitter = {
5194				let store = store.clone();
5195				let stmt = stmt.clone();
5196				std::thread::spawn(move || store.submit(stmt, StatementSource::Local))
5197			};
5198			remover.join().expect("remover joins");
5199			let _ = resubmitter.join().expect("resubmitter joins");
5200
5201			// Whatever the interleaving, the query-index bookkeeping must agree with the store.
5202			let query_index = store.query_index.read();
5203			let present = store.has_statement(&hash);
5204			assert_eq!(query_index.recent.contains_key(&hash), present);
5205			assert_eq!(
5206				query_index.topic_counts.get(&topic(7)).copied().unwrap_or(0),
5207				present as usize
5208			);
5209		}
5210	}
5211
5212	#[test]
5213	fn check_expiration_handles_multiple_statements_per_account() {
5214		let (mut store, _temp) = test_store();
5215		store.set_time(100);
5216
5217		// Create multiple statements for the same account with different expiry timestamps
5218		// Account 42 has limit of 42 statements
5219		let mut stmt1 = unsigned_statement(42, 1, Some(1), 100);
5220		stmt1.set_expiry_from_parts(200, 1); // Expires at timestamp 200
5221		sign_with(&mut stmt1, 42);
5222		let hash1 = stmt1.hash();
5223		store.submit(stmt1, StatementSource::Network);
5224
5225		let mut stmt2 = unsigned_statement(42, 2, Some(2), 100);
5226		stmt2.set_expiry_from_parts(300, 2); // Expires at timestamp 300
5227		sign_with(&mut stmt2, 42);
5228		let hash2 = stmt2.hash();
5229		store.submit(stmt2, StatementSource::Network);
5230
5231		let mut stmt3 = unsigned_statement(42, 3, Some(3), 100);
5232		stmt3.set_expiry_from_parts(500, 3); // Expires at timestamp 500
5233		sign_with(&mut stmt3, 42);
5234		let hash3 = stmt3.hash();
5235		store.submit(stmt3, StatementSource::Network);
5236
5237		// Verify all statements are in the store
5238		assert_eq!(store.statement_count(), 3);
5239
5240		// Advance time to 250 (stmt1 should expire since 250 > 200)
5241		store.set_time(250);
5242		store.enforce_limits();
5243
5244		{
5245			// Naturally expired statements are not added to the expired map.
5246			assert!(!store.is_evicted(&hash1), "stmt1 naturally expired, not in map");
5247			assert!(!store.is_evicted(&hash2), "stmt2 should not be expired yet");
5248			assert!(!store.is_evicted(&hash3), "stmt3 should not be expired yet");
5249			assert_eq!(store.statement_count(), 2);
5250		}
5251
5252		// Advance time to 400 (stmt2 should also expire since 400 > 300)
5253		store.set_time(400);
5254		store.enforce_limits();
5255
5256		{
5257			assert!(!store.is_evicted(&hash1));
5258			assert!(!store.is_evicted(&hash2), "stmt2 naturally expired, not in map");
5259			assert!(!store.is_evicted(&hash3), "stmt3 should not be expired yet");
5260			assert_eq!(store.statement_count(), 1);
5261		}
5262
5263		// Check again at time 600 (stmt3 should expire since 600 > 500)
5264		store.set_time(600);
5265		store.enforce_limits();
5266
5267		{
5268			assert!(!store.is_evicted(&hash1));
5269			assert!(!store.is_evicted(&hash2));
5270			assert!(!store.is_evicted(&hash3), "stmt3 naturally expired, not in map");
5271			assert_eq!(store.statement_count(), 0);
5272		}
5273	}
5274
5275	#[test]
5276	fn check_expiration_does_nothing_when_no_expired_statements() {
5277		let (mut store, _temp) = test_store();
5278		store.set_time(1000);
5279
5280		// Create statement with expiry far in the future
5281		// The statement() helper uses set_expiry_from_parts(u32::MAX, priority)
5282		let stmt = statement(1, 1, None, 100);
5283		let hash = stmt.hash();
5284		store.submit(stmt, StatementSource::Network);
5285
5286		// Check expiration - nothing should happen
5287		store.enforce_limits();
5288
5289		// Statement should still be there
5290		assert!(store.has_statement(&hash));
5291		assert!(!store.is_evicted(&hash));
5292		assert_eq!(store.statement_count(), 1);
5293		assert_eq!(store.evicted_count(), 0);
5294	}
5295
5296	#[test]
5297	fn check_expiration_correctly_updates_account_data() {
5298		let (mut store, _temp) = test_store();
5299		store.set_time(100);
5300
5301		// Create a statement with expiry at timestamp 200
5302		let mut stmt = unsigned_statement(1, 1, Some(1), 100);
5303		stmt.set_expiry_from_parts(200, 1);
5304		sign_with(&mut stmt, 1);
5305		let hash = stmt.hash();
5306		store.submit(stmt, StatementSource::Network);
5307
5308		// Verify account exists before expiration
5309		{
5310			assert!(store.has_account(&account(1)));
5311			assert_eq!(store.total_size(), 100);
5312		}
5313
5314		// Expire
5315		store.set_time(300);
5316		store.enforce_limits();
5317
5318		// Verify account is removed after its only statement expires
5319		{
5320			assert!(
5321				!store.has_account(&account(1)),
5322				"Account should be removed when all its statements expire"
5323			);
5324			assert_eq!(store.total_size(), 0, "Total size should be zero");
5325			assert!(!store.is_evicted(&hash), "Naturally expired, not in map");
5326		}
5327	}
5328
5329	#[test]
5330	fn check_expiration_clears_topic_and_key_indexes() {
5331		let (mut store, _temp) = test_store();
5332		store.set_time(100);
5333
5334		// Create a statement with topic and decryption key
5335		let mut stmt = unsigned_statement(1, 1, Some(1), 100);
5336		stmt.set_expiry_from_parts(200, 1);
5337		stmt.set_topic(0, topic(42));
5338		stmt.set_decryption_key(dec_key(7));
5339		sign_with(&mut stmt, 1);
5340		let hash = stmt.hash();
5341		store.submit(stmt, StatementSource::Network);
5342
5343		// Verify indexes are populated
5344		{
5345			assert!(store.index_has_topic(&topic(42), &hash));
5346			assert!(store.index_has_dec_key(&Some(dec_key(7)), &hash));
5347		}
5348
5349		// Expire
5350		store.set_time(300);
5351		store.enforce_limits();
5352
5353		// Verify indexes are cleared
5354		{
5355			assert!(!store.index_has_topic(&topic(42), &hash), "Topic index should be cleared");
5356			assert!(
5357				!store.index_has_dec_key(&Some(dec_key(7)), &hash),
5358				"Decryption key index should be cleared"
5359			);
5360			assert!(!store.is_evicted(&hash), "Naturally expired, not in map");
5361		}
5362	}
5363
5364	#[test]
5365	fn check_expiration_handles_empty_store() {
5366		let (mut store, _temp) = test_store();
5367		store.set_time(1000);
5368
5369		// With no statements, check_expiration should not panic
5370		store.enforce_limits();
5371
5372		// Second call should also work
5373		store.enforce_limits();
5374
5375		assert!(store.submit_index.read().allowance_cursor.is_none());
5376		assert_eq!(store.statement_count(), 0);
5377		assert_eq!(store.evicted_count(), 0);
5378	}
5379
5380	#[test]
5381	fn check_expiration_expires_properly_formatted_statements() {
5382		// With the fix (Expiry(current_time << 32)), check_expiration properly
5383		// compares timestamps and can expire statements submitted through normal flow.
5384
5385		let (mut store, _temp) = test_store();
5386		store.set_time(1000);
5387
5388		// Create a statement with expiration timestamp just 1 second in the future
5389		let mut stmt = unsigned_statement(1, 1, None, 100);
5390		stmt.set_expiry_from_parts(1001, 1); // Expires at timestamp 1001
5391		sign_with(&mut stmt, 1);
5392		let hash = stmt.hash();
5393		store.submit(stmt, StatementSource::Network);
5394
5395		assert_eq!(store.statement_count(), 1);
5396
5397		// Advance time past the expiration timestamp
5398		store.set_time(2000);
5399		store.enforce_limits();
5400
5401		// Statement SHOULD be expired because check_expiration now compares
5402		// Expiry(2000 << 32) against Expiry(1001 << 32 | 1), and
5403		// (2000 << 32) > (1001 << 32 | 1)
5404		assert!(
5405			!store.has_statement(&hash),
5406			"Statement should be removed from the store after expiration"
5407		);
5408		// Naturally expired: timestamp 1001 < current_time 2000, not added to expired map.
5409		assert!(!store.is_evicted(&hash), "Naturally expired, not in map");
5410	}
5411
5412	#[test]
5413	fn check_expiration_updates_database_columns() {
5414		// This test verifies that check_expiration properly updates the database.
5415		let (mut store, _temp) = test_store();
5416		store.set_time(100);
5417
5418		// Create a statement with expiry at timestamp 200
5419		let mut stmt = unsigned_statement(1, 1, None, 100);
5420		stmt.set_expiry_from_parts(200, 1);
5421		sign_with(&mut stmt, 1);
5422		let hash = stmt.hash();
5423		store.submit(stmt.clone(), StatementSource::Network);
5424
5425		// Verify statement is in the database
5426		let db_entry = store.db.get(col::STATEMENTS, &hash).unwrap();
5427		assert!(db_entry.is_some(), "Statement should be in col::STATEMENTS after submit");
5428
5429		// Advance time past expiry and run check_expiration
5430		store.set_time(300);
5431		store.enforce_limits();
5432
5433		// Verify the store state is updated correctly
5434		{
5435			assert_eq!(store.statement_count(), 0, "Statement should be removed from the store");
5436			// Naturally expired: not added to expired map, no need for suppression.
5437			assert!(
5438				!store.is_evicted(&hash),
5439				"Naturally expired statement must not be in the expired map"
5440			);
5441		}
5442
5443		let db_entry = store.db.get(col::STATEMENTS, &hash).unwrap();
5444		assert!(
5445			db_entry.is_none(),
5446			"Statement should be removed from col::STATEMENTS after expiration"
5447		);
5448
5449		// Naturally expired statements are not written to col::EXPIRED either, so that
5450		// the optimization survives node restarts.
5451		let expired_entry = store.db.get(col::EXPIRED, &hash).unwrap();
5452		assert!(expired_entry.is_none(), "Naturally expired: not written to col::EXPIRED");
5453	}
5454
5455	#[test]
5456	fn enforce_allowances_evicts_excess_statements() {
5457		// This test verifies that check_expiration correctly evicts statements
5458		// when statements exceed the current allowance. We directly insert into
5459		// the index (bypassing submit's validation) to simulate statements that
5460		// existed before allowances were reduced.
5461		let (mut store, _temp) = test_store();
5462		store.set_time(0);
5463
5464		// Account 4 has allowance (4 statements, 1000 bytes) from TestClient
5465		let s1 = statement(4, 10, None, 100); // lowest priority - will be evicted
5466		let s2 = statement(4, 20, None, 100);
5467		let s3 = statement(4, 30, None, 100);
5468		let s4 = statement(4, 40, None, 100);
5469		let s5 = statement(4, 50, None, 100); // highest priority
5470
5471		let h1 = s1.hash();
5472		let h5 = s5.hash();
5473
5474		// Directly insert into the store, bypassing `submit`'s allowance check
5475		for statement in [&s1, &s2, &s3, &s4, &s5] {
5476			store.force_insert(statement);
5477		}
5478
5479		// Verify initial state - all 5 should be present
5480		assert_eq!(store.statement_count(), 5);
5481		assert_eq!(store.total_size(), 500);
5482
5483		// Run check_expiration which handles both expiration and allowance enforcement
5484		// Since account 4 has max_count=4, one statement should be evicted
5485		store.enforce_limits();
5486
5487		// Should evict the lowest priority statement (s1)
5488		assert_eq!(store.statement_count(), 4, "Should have 4 statements after eviction");
5489		assert!(!store.has_statement(&h1), "Lowest priority should be evicted");
5490		assert!(store.has_statement(&h5), "Highest priority should remain");
5491		assert_eq!(store.total_size(), 400);
5492
5493		// Evicted statement should be marked as expired
5494		assert!(store.is_evicted(&h1));
5495	}
5496
5497	#[test]
5498	fn enforce_allowances_evicts_all_when_no_allowance_found() {
5499		let (mut store, _temp) = test_store();
5500		store.set_time(0);
5501
5502		// Account 0 has NO allowance in TestClient
5503		let s1 = statement(0, 10, None, 100);
5504		let s2 = statement(0, 20, None, 150);
5505
5506		let h1 = s1.hash();
5507		let h2 = s2.hash();
5508
5509		// Directly insert statements for account with no allowance
5510		store.force_insert(&s1);
5511		store.force_insert(&s2);
5512
5513		assert_eq!(store.statement_count(), 2);
5514
5515		// Run check_expiration - should evict ALL statements since no allowance exists
5516		store.enforce_limits();
5517
5518		assert_eq!(store.statement_count(), 0, "All statements should be evicted");
5519		assert!(!store.has_account(&account(0)), "Account should be removed");
5520		assert!(store.is_evicted(&h1));
5521		assert!(store.is_evicted(&h2));
5522	}
5523
5524	#[test]
5525	fn enforce_allowances_based_on_size() {
5526		// This test verifies that check_expiration evicts based on size limits.
5527		let (mut store, _temp) = test_store();
5528		store.set_time(0);
5529
5530		// Account 2 has allowance (2, 1000) from TestClient
5531		// Insert 2 statements that together exceed 1000 bytes
5532		let s1 = statement(2, 10, None, 600); // lowest priority
5533		let s2 = statement(2, 20, None, 600); // higher priority
5534
5535		let h1 = s1.hash();
5536		let h2 = s2.hash();
5537
5538		// Directly insert both statements (total 1200 bytes > 1000 limit)
5539		store.force_insert(&s1);
5540		store.force_insert(&s2);
5541
5542		assert_eq!(store.total_size(), 1200);
5543
5544		// Run check_expiration - should evict s1 to get under 1000 bytes
5545		store.enforce_limits();
5546
5547		assert_eq!(store.statement_count(), 1);
5548		assert!(store.has_statement(&h2), "Higher priority should remain");
5549		assert!(!store.has_statement(&h1), "Lower priority should be evicted");
5550		assert_eq!(store.total_size(), 600);
5551	}
5552
5553	#[test]
5554	fn channel_replacement_only_higher_priority_succeeds() {
5555		let (store, _temp) = test_store();
5556		let source = StatementSource::Network;
5557
5558		// Account 1: max_count=1, max_size=1000
5559		// Submit channel 1 with priority 5
5560		let s1 = statement(1, 5, Some(1), 100);
5561		let h1 = s1.hash();
5562		assert_eq!(store.submit(s1, source), SubmitResult::New);
5563
5564		// Lower priority on same channel → ChannelPriorityTooLow
5565		let result = store.submit(statement(1, 3, Some(1), 100), source);
5566		assert!(
5567			matches!(result, SubmitResult::Rejected(RejectionReason::ChannelPriorityTooLow { .. })),
5568			"Lower priority should be rejected with ChannelPriorityTooLow, got: {result:?}"
5569		);
5570
5571		// Equal priority on same channel → ChannelPriorityTooLow (check is <=)
5572		// Use different data_len to get a distinct hash with same priority
5573		let result = store.submit(statement(1, 5, Some(1), 101), source);
5574		assert!(
5575			matches!(result, SubmitResult::Rejected(RejectionReason::ChannelPriorityTooLow { .. })),
5576			"Equal priority should be rejected with ChannelPriorityTooLow, got: {result:?}"
5577		);
5578
5579		// Higher priority on same channel → replaces
5580		let s2 = statement(1, 10, Some(1), 200);
5581		let h2 = s2.hash();
5582		assert_eq!(store.submit(s2, source), SubmitResult::New);
5583
5584		{
5585			assert_eq!(store.statement_count(), 1);
5586			assert!(!store.has_statement(&h1), "Old channel message should be gone");
5587			assert!(store.has_statement(&h2), "New channel message should exist");
5588			assert!(store.is_evicted(&h1), "Old should be in expired");
5589			assert_eq!(store.total_size(), 200);
5590		}
5591	}
5592
5593	#[test]
5594	fn submit_rejects_malformed_statements() {
5595		let (store, _temp) = test_store();
5596
5597		let mut base = Statement::new();
5598		base.set_expiry(u64::MAX);
5599		base.set_plain_data(vec![1]);
5600
5601		let ed_kp = sp_core::ed25519::Pair::from_string("//Alice", None).unwrap();
5602		let sr_kp = sp_core::sr25519::Pair::from_string("//Alice", None).unwrap();
5603		let ecdsa_kp = sp_core::ecdsa::Pair::from_string("//Alice", None).unwrap();
5604
5605		assert_eq!(
5606			store.submit(base.clone(), StatementSource::Network),
5607			SubmitResult::Invalid(InvalidReason::NoProof)
5608		);
5609
5610		let bad_proofs = [
5611			Proof::Ed25519 { signature: [0xAB; 64], signer: ed_kp.public().0 },
5612			Proof::Sr25519 { signature: [0xCD; 64], signer: sr_kp.public().0 },
5613			Proof::Secp256k1Ecdsa { signature: [0xEF; 65], signer: ecdsa_kp.public().0 },
5614		];
5615		for proof in bad_proofs {
5616			let mut s = base.clone();
5617			s.set_proof(proof);
5618			assert_eq!(
5619				store.submit(s, StatementSource::Network),
5620				SubmitResult::Invalid(InvalidReason::BadProof)
5621			);
5622		}
5623
5624		let mut wrong_signer = base.clone();
5625		wrong_signer.sign_ed25519_private(&ed_kp);
5626		let alice_sig = match wrong_signer.proof().unwrap() {
5627			Proof::Ed25519 { signature, .. } => *signature,
5628			_ => panic!("expected Ed25519 proof after sign_ed25519_private"),
5629		};
5630		let bob_kp = sp_core::ed25519::Pair::from_string("//Bob", None).unwrap();
5631		wrong_signer.set_proof(Proof::Ed25519 { signature: alice_sig, signer: bob_kp.public().0 });
5632		assert_eq!(
5633			store.submit(wrong_signer, StatementSource::Network),
5634			SubmitResult::Invalid(InvalidReason::BadProof)
5635		);
5636	}
5637
5638	#[test]
5639	fn channel_replacement_with_size_increase_evicts_others() {
5640		let (store, _temp) = test_store();
5641		let source = StatementSource::Network;
5642
5643		// Account 3: max_count=3, max_size=1000
5644		// channel msg (200b) + two non-channel msgs (300b each) = 800b
5645		let s_ch = statement(3, 5, Some(1), 200);
5646		let s_low = statement(3, 2, None, 300);
5647		let s_mid = statement(3, 3, None, 300);
5648		let h_ch = s_ch.hash();
5649		let h_low = s_low.hash();
5650		let h_mid = s_mid.hash();
5651
5652		assert_eq!(store.submit(s_ch, source), SubmitResult::New);
5653		assert_eq!(store.submit(s_low, source), SubmitResult::New);
5654		assert_eq!(store.submit(s_mid, source), SubmitResult::New);
5655		assert_eq!(store.total_size(), 800);
5656
5657		// Replace channel with 600b message (priority 10 > 5)
5658		// Must evict lowest priority non-channel statement (priority 2) to fit
5659		let s_ch_big = statement(3, 10, Some(1), 600);
5660		let h_ch_big = s_ch_big.hash();
5661		assert_eq!(store.submit(s_ch_big, source), SubmitResult::New);
5662
5663		{
5664			assert_eq!(store.statement_count(), 2);
5665			assert!(!store.has_statement(&h_ch), "Old channel message replaced");
5666			assert!(!store.has_statement(&h_low), "Priority 2 evicted to fit size");
5667			assert!(store.has_statement(&h_mid), "Priority 3 should remain");
5668			assert!(store.has_statement(&h_ch_big), "New channel message added");
5669			assert_eq!(store.total_size(), 900); // 300 (mid) + 600 (new channel)
5670		}
5671	}
5672
5673	#[test]
5674	fn subscription_reconnect_receives_current_state() {
5675		use crate::StatementStoreSubscriptionApi;
5676		use sp_statement_store::OptimizedTopicFilter;
5677
5678		let (store, _temp) = test_store();
5679		let source = StatementSource::Local;
5680
5681		// Submit 3 statements
5682		for i in 0..3u8 {
5683			let res = store.submit(signed_statement(i), source);
5684			assert_eq!(res, SubmitResult::New);
5685		}
5686
5687		// First subscribe → should get 3 existing statements
5688		let (existing, sender, stream) =
5689			store.subscribe_statement(OptimizedTopicFilter::Any).unwrap();
5690		assert_eq!(existing.len(), 3, "First subscribe should return 3 existing statements");
5691
5692		// Drop stream
5693		drop(stream);
5694		drop(sender);
5695
5696		// Submit 2 more while disconnected
5697		for i in 3..5u8 {
5698			assert_eq!(store.submit(signed_statement(i), source), SubmitResult::New);
5699		}
5700		let (existing, sender, stream) =
5701			store.subscribe_statement(OptimizedTopicFilter::Any).unwrap();
5702		assert_eq!(existing.len(), 5, "Re-subscribe should return all 5 current statements");
5703
5704		// Drop and remove one statement
5705		drop(stream);
5706		drop(sender);
5707		let hash_to_remove = signed_statement(0).hash();
5708		store.remove(&hash_to_remove).unwrap();
5709
5710		// Re-subscribe → should get 4
5711		let (existing, _sender, _stream) =
5712			store.subscribe_statement(OptimizedTopicFilter::Any).unwrap();
5713		assert_eq!(existing.len(), 4, "Re-subscribe after removal should return 4 statements");
5714	}
5715
5716	#[test]
5717	fn subscription_reconnect_with_topic_filter() {
5718		use crate::StatementStoreSubscriptionApi;
5719		use sp_statement_store::OptimizedTopicFilter;
5720
5721		let (store, _temp) = test_store();
5722		let source = StatementSource::Local;
5723		let topic_a = topic(1);
5724		let topic_b = topic(2);
5725
5726		// s1: topic A only
5727		let s1 = signed_statement_with_topics(1, &[topic_a], None);
5728		// s2: topic B only
5729		let s2 = signed_statement_with_topics(2, &[topic_b], None);
5730		// s3: topics A + B
5731		let s3 = signed_statement_with_topics(3, &[topic_a, topic_b], None);
5732
5733		assert_eq!(store.submit(s1, source), SubmitResult::New);
5734		assert_eq!(store.submit(s2, source), SubmitResult::New);
5735		assert_eq!(store.submit(s3, source), SubmitResult::New);
5736
5737		// Subscribe with MatchAll([A]) → s1, s3
5738		let filter_a = OptimizedTopicFilter::MatchAll(std::collections::HashSet::from([topic_a]));
5739		let (existing, sender, stream) = store.subscribe_statement(filter_a.clone()).unwrap();
5740		assert_eq!(existing.len(), 2, "MatchAll([A]) should match s1 and s3");
5741
5742		// Drop and add s4 with topic A
5743		drop(sender);
5744		drop(stream);
5745		let s4 = signed_statement_with_topics(4, &[topic_a], None);
5746		assert_eq!(store.submit(s4, source), SubmitResult::New);
5747		// Re-subscribe with same filter → s1, s3, s4
5748		let (existing, sender, stream) = store.subscribe_statement(filter_a).unwrap();
5749		assert_eq!(existing.len(), 3, "Re-subscribe MatchAll([A]) should return s1, s3, s4");
5750
5751		// Drop and re-subscribe with different filter MatchAll([B]) → s2, s3
5752		drop(sender);
5753		drop(stream);
5754		let filter_b = OptimizedTopicFilter::MatchAll(std::collections::HashSet::from([topic_b]));
5755		let (existing, _sender, _stream) = store.subscribe_statement(filter_b).unwrap();
5756		assert_eq!(existing.len(), 2, "Re-subscribe MatchAll([B]) should return s2 and s3");
5757	}
5758
5759	#[tokio::test]
5760	async fn subscription_delivers_each_statement_exactly_once_across_boundary() {
5761		// Exactly-once: a statement existing before the subscription is delivered only through the
5762		// initial snapshot, and a statement submitted afterwards only through the live stream —
5763		// never both (which was the at-least-once regression) and never neither.
5764		use crate::StatementStoreSubscriptionApi;
5765		use futures::StreamExt;
5766		use sp_statement_store::{OptimizedTopicFilter, StatementEvent};
5767
5768		let (store, _temp) = test_store();
5769		let source = StatementSource::Local;
5770
5771		// Two statements exist before the subscription is created.
5772		let a = signed_statement(0);
5773		let b = signed_statement(1);
5774		assert_eq!(store.submit(a.clone(), source), SubmitResult::New);
5775		assert_eq!(store.submit(b.clone(), source), SubmitResult::New);
5776
5777		// The snapshot must contain exactly the pre-existing statements.
5778		let (existing, _sender, mut stream) =
5779			store.subscribe_statement(OptimizedTopicFilter::Any).unwrap();
5780		let mut snapshot: Vec<Statement> = existing
5781			.iter()
5782			.map(|bytes| Statement::decode(&mut &bytes[..]).unwrap())
5783			.collect();
5784		snapshot.sort_by_key(|s| s.hash());
5785		let mut expected_snapshot = vec![a.clone(), b.clone()];
5786		expected_snapshot.sort_by_key(|s| s.hash());
5787		assert_eq!(
5788			snapshot, expected_snapshot,
5789			"snapshot must contain exactly the pre-existing statements"
5790		);
5791
5792		// A statement submitted after the subscription must arrive on the live stream, exactly
5793		// once.
5794		let c = signed_statement(2);
5795		assert_eq!(store.submit(c.clone(), source), SubmitResult::New);
5796
5797		let event = tokio::time::timeout(std::time::Duration::from_secs(5), stream.next())
5798			.await
5799			.expect("live statement should arrive within the timeout")
5800			.expect("stream should yield an event");
5801		let StatementEvent::NewStatements { statements, .. } = event;
5802		let live: Vec<Statement> = statements
5803			.iter()
5804			.map(|bytes| Statement::decode(&mut &bytes.0[..]).unwrap())
5805			.collect();
5806		assert_eq!(
5807			live,
5808			vec![c.clone()],
5809			"live stream must deliver exactly the post-subscribe statement"
5810		);
5811
5812		// No duplicate: neither the new statement nor the snapshot statements are delivered again.
5813		assert!(
5814			tokio::time::timeout(std::time::Duration::from_millis(500), stream.next())
5815				.await
5816				.is_err(),
5817			"no further (duplicate) statements must be delivered"
5818		);
5819	}
5820
5821	#[test]
5822	fn subscription_snapshot_deduplicates_multi_topic_match_any() {
5823		// A `MatchAny` snapshot must contain a statement matching several of the filter's topics
5824		// only once, not once per matching topic.
5825		use crate::StatementStoreSubscriptionApi;
5826		use sp_statement_store::OptimizedTopicFilter;
5827
5828		let (store, _temp) = test_store();
5829		let topic_a = topic(1);
5830		let topic_b = topic(2);
5831
5832		// A statement carrying BOTH topics.
5833		let s = signed_statement_with_topics(1, &[topic_a, topic_b], None);
5834		assert_eq!(store.submit(s, StatementSource::Local), SubmitResult::New);
5835
5836		let filter =
5837			OptimizedTopicFilter::MatchAny(std::collections::HashSet::from([topic_a, topic_b]));
5838		let (existing, _sender, _stream) = store.subscribe_statement(filter).unwrap();
5839		assert_eq!(
5840			existing.len(),
5841			1,
5842			"MatchAny snapshot must not duplicate a multi-topic statement"
5843		);
5844	}
5845
5846	#[tokio::test]
5847	async fn subscription_match_all_delivers_exactly_once_across_boundary() {
5848		// The `MatchAll` snapshot is enumerated authoritatively from disk, so a matching statement
5849		// present before the subscription is delivered via the snapshot (never lost to the
5850		// in-memory counters/cache lagging a commit), while one submitted afterwards is delivered
5851		// live exactly once.
5852		use crate::StatementStoreSubscriptionApi;
5853		use futures::StreamExt;
5854		use sp_statement_store::{OptimizedTopicFilter, StatementEvent};
5855
5856		let (store, _temp) = test_store();
5857		let source = StatementSource::Local;
5858		let t = topic(7);
5859
5860		// Two matching statements exist before the subscription.
5861		let a = signed_statement_with_topics(0, &[t], None);
5862		let b = signed_statement_with_topics(1, &[t], None);
5863		assert_eq!(store.submit(a, source), SubmitResult::New);
5864		assert_eq!(store.submit(b, source), SubmitResult::New);
5865
5866		let filter = OptimizedTopicFilter::MatchAll(std::collections::HashSet::from([t]));
5867		let (existing, _sender, mut stream) = store.subscribe_statement(filter).unwrap();
5868		assert_eq!(
5869			existing.len(),
5870			2,
5871			"MatchAll snapshot must contain both pre-existing statements"
5872		);
5873
5874		// A matching statement submitted afterwards must arrive live, exactly once.
5875		let c = signed_statement_with_topics(2, &[t], None);
5876		assert_eq!(store.submit(c.clone(), source), SubmitResult::New);
5877
5878		let event = tokio::time::timeout(std::time::Duration::from_secs(5), stream.next())
5879			.await
5880			.expect("live statement should arrive within the timeout")
5881			.expect("stream should yield an event");
5882		let StatementEvent::NewStatements { statements, .. } = event;
5883		let live: Vec<Statement> = statements
5884			.iter()
5885			.map(|bytes| Statement::decode(&mut &bytes.0[..]).unwrap())
5886			.collect();
5887		assert_eq!(
5888			live,
5889			vec![c],
5890			"MatchAll live stream must deliver exactly the post-subscribe statement"
5891		);
5892
5893		assert!(
5894			tokio::time::timeout(std::time::Duration::from_millis(500), stream.next())
5895				.await
5896				.is_err(),
5897			"no further (duplicate) statements must be delivered"
5898		);
5899	}
5900
5901	#[tokio::test]
5902	async fn subscription_no_loss_or_duplicate_under_concurrent_submits() {
5903		// Race a stream of submissions against the subscription registration: whatever the
5904		// interleaving, every matching statement must be delivered exactly once across the snapshot
5905		// and the live stream — no loss (the bug the on-disk `MatchAll` snapshot could cause) and
5906		// no duplicate (the original at-least-once regression). This assertion holds for any
5907		// timing, so the test is deterministic even though the race window is hit
5908		// non-deterministically.
5909		use crate::StatementStoreSubscriptionApi;
5910		use futures::StreamExt;
5911		use sp_statement_store::{OptimizedTopicFilter, StatementEvent};
5912		use std::collections::HashSet;
5913
5914		let (store, _temp) = test_store();
5915		let store = std::sync::Arc::new(store);
5916		let t = topic(9);
5917		// Keep well under SUBSCRIPTION_BUFFER_SIZE (128) so the live channel never overflows (which
5918		// would auto-unsubscribe and legitimately drop statements).
5919		const N: u8 = 60;
5920
5921		let all: Vec<Statement> =
5922			(0..N).map(|i| signed_statement_with_topics(i, &[t], None)).collect();
5923		let all_hashes: HashSet<_> = all.iter().map(|s| s.hash()).collect();
5924
5925		// A handful exist before subscribing; the rest are submitted concurrently with the
5926		// subscribe.
5927		let split = 10usize;
5928		for s in &all[..split] {
5929			assert_eq!(store.submit(s.clone(), StatementSource::Local), SubmitResult::New);
5930		}
5931		let store2 = store.clone();
5932		let rest: Vec<Statement> = all[split..].to_vec();
5933		let submitter = std::thread::spawn(move || {
5934			for s in rest {
5935				let _ = store2.submit(s, StatementSource::Local);
5936			}
5937		});
5938
5939		let filter = OptimizedTopicFilter::MatchAll(HashSet::from([t]));
5940		let (existing, _sender, mut stream) = store.subscribe_statement(filter).unwrap();
5941
5942		submitter.join().unwrap();
5943
5944		// Everything delivered so far, snapshot first.
5945		let mut seen = existing
5946			.iter()
5947			.map(|b| Statement::decode(&mut &b[..]).unwrap().hash())
5948			.collect::<Vec<_>>();
5949		// Drain the live stream until every statement is accounted for (or a timeout on loss).
5950		while seen.len() < N as usize {
5951			match tokio::time::timeout(std::time::Duration::from_secs(5), stream.next()).await {
5952				Ok(Some(StatementEvent::NewStatements { statements, .. })) => {
5953					for b in statements {
5954						seen.push(Statement::decode(&mut &b.0[..]).unwrap().hash());
5955					}
5956				},
5957				_ => break,
5958			}
5959		}
5960
5961		let seen_set: HashSet<_> = seen.iter().copied().collect();
5962		assert_eq!(
5963			seen.len(),
5964			N as usize,
5965			"each statement must be delivered exactly once (got {} deliveries for {} statements — loss or duplicate)",
5966			seen.len(),
5967			N
5968		);
5969		assert_eq!(seen_set, all_hashes, "delivered set must equal submitted set");
5970	}
5971
5972	// Tests for the multi-filter subscription API (`MultiFilterSubscriptionApi` /
5973	// `create_subscription`), as opposed to the single-filter `subscribe_statement` tests above.
5974	mod multi_filter {
5975		use super::*;
5976		use crate::{
5977			MultiFilterEventStream, MultiFilterSubscriptionApi, MultiFilterSubscriptionEvent,
5978		};
5979		use futures::StreamExt;
5980		use sp_statement_store::OptimizedTopicFilter;
5981		use std::{collections::HashSet, sync::Arc, time::Duration};
5982
5983		fn arc_test_store() -> (Arc<Store>, tempfile::TempDir) {
5984			let (store, dir) = test_store();
5985			(Arc::new(store), dir)
5986		}
5987
5988		async fn drain_all(
5989			stream: &mut MultiFilterEventStream,
5990			idle: Duration,
5991		) -> Vec<MultiFilterSubscriptionEvent> {
5992			let mut events = Vec::new();
5993			while let Ok(Some(event)) = tokio::time::timeout(idle, stream.next()).await {
5994				events.push(event);
5995			}
5996			events
5997		}
5998
5999		#[test]
6000		fn replay_watermark_excludes_later_admissions() {
6001			let (store, _dir) = test_store();
6002			let before = signed_statement(1);
6003			let after = signed_statement(2);
6004			assert_eq!(store.submit(before.clone(), StatementSource::Local), SubmitResult::New);
6005
6006			let mut watermark = None;
6007			assert!(store
6008				.register_replay(&mut |captured| {
6009					watermark = Some(captured);
6010					true
6011				})
6012				.unwrap()
6013				.is_some());
6014			assert_eq!(store.submit(after.clone(), StatementSource::Local), SubmitResult::New);
6015
6016			let replay =
6017				store.replay_batch(&OptimizedTopicFilter::Any, 0, watermark.unwrap()).unwrap();
6018			let hashes: HashSet<_> = replay
6019				.statements
6020				.iter()
6021				.map(|encoded| Statement::decode(&mut encoded.as_slice()).unwrap().hash())
6022				.collect();
6023			assert!(replay.done);
6024			assert!(hashes.contains(&before.hash()));
6025			assert!(!hashes.contains(&after.hash()));
6026		}
6027
6028		#[tokio::test]
6029		async fn add_filter_replays_snapshot_and_delivers_later_submissions_live() {
6030			let (store, _dir) = arc_test_store();
6031			let (handle, mut stream) = store.create_subscription();
6032
6033			const NUM_STATEMENTS: u8 = 50;
6034			const NUM_PRE_FILTER: u8 = 20;
6035
6036			// Statements stored before the filter is attached; the replay must cover exactly these.
6037			for i in 0..NUM_PRE_FILTER {
6038				let stmt = signed_statement(i);
6039				assert_eq!(store.submit(stmt, StatementSource::Local), SubmitResult::New);
6040			}
6041
6042			let filter_id = handle.add_filter(OptimizedTopicFilter::Any).unwrap();
6043
6044			// The watermark is captured atomically with the filter registration, so statements
6045			// submitted after `add_filter` returns sit above it and arrive as live events only.
6046			for i in NUM_PRE_FILTER..NUM_STATEMENTS {
6047				let stmt = signed_statement(i);
6048				assert_eq!(store.submit(stmt, StatementSource::Local), SubmitResult::New);
6049			}
6050
6051			let mut snapshot_hashes: HashSet<[u8; 32]> = HashSet::new();
6052			let mut live_with_filter: HashSet<[u8; 32]> = HashSet::new();
6053			for event in drain_all(&mut stream, Duration::from_millis(300)).await {
6054				match event {
6055					MultiFilterSubscriptionEvent::ReplayStatements { statements, .. } => {
6056						snapshot_hashes.extend(
6057							statements
6058								.iter()
6059								.map(|bytes| Statement::decode(&mut &bytes[..]).unwrap().hash()),
6060						);
6061					},
6062					MultiFilterSubscriptionEvent::NewStatement(event)
6063						if event.matched_filter_ids.contains(&filter_id) =>
6064					{
6065						live_with_filter.insert(event.hash);
6066					},
6067					_ => {},
6068				}
6069			}
6070
6071			let expected_replayed: HashSet<[u8; 32]> =
6072				(0..NUM_PRE_FILTER).map(|i| signed_statement(i).hash()).collect();
6073			let expected_live: HashSet<[u8; 32]> =
6074				(NUM_PRE_FILTER..NUM_STATEMENTS).map(|i| signed_statement(i).hash()).collect();
6075
6076			assert_eq!(snapshot_hashes, expected_replayed);
6077			assert_eq!(live_with_filter, expected_live);
6078		}
6079	}
6080}