#![doc = include_str!("../docs/overview.md")]
#![doc = include_str!("../docs/usage.md")]
#![warn(missing_docs)]
#![warn(unused_extern_crates)]
mod metrics;
mod subscription;
#[cfg(feature = "test-helpers")]
pub mod subxt_client;
#[cfg(feature = "test-helpers")]
pub mod test_utils;
use crate::subscription::{SubscriptionStatementsStream, SubscriptionsHandle};
use futures::FutureExt;
use metrics::MetricsLink as PrometheusMetrics;
use parking_lot::RwLock;
use prometheus_endpoint::Registry as PrometheusRegistry;
use sc_client_api::{backend::StorageProvider, Backend, StorageKey};
use sc_keystore::LocalKeystore;
use schnellru::{ByLength, LruMap};
use sp_blockchain::HeaderBackend;
use sp_core::{crypto::UncheckedFrom, hexdisplay::HexDisplay, traits::SpawnNamed, Decode, Encode};
use sp_runtime::traits::Block as BlockT;
use sp_statement_store::{
runtime_api::{StatementSource, StatementStoreExt},
AccountId, AdmittedBatch, BlockHash, Channel, DecryptionKey, FilterDecision, Hash,
InvalidReason, OptimizedTopicFilter, RejectionReason, Result, SignatureVerificationResult,
Statement, StatementAllowance, StatementEvent, SubmitResult, Topic,
};
pub use sp_statement_store::{Error, StatementStore, MAX_TOPICS};
use std::{
collections::{BTreeMap, HashMap, HashSet},
sync::{
atomic::{AtomicUsize, Ordering as AtomicOrdering},
Arc, Weak,
},
time::{Duration, Instant},
};
pub use subscription::{
AddFilterError, MultiFilterEventStream, MultiFilterSubscriptionApi,
MultiFilterSubscriptionEvent, StatementStoreSubscriptionApi, SubscriptionHandle,
MAX_FILTERS_PER_SUBSCRIPTION,
};
use subscription::{ReplayBatch, ReplaySnapshotProvider, REPLAY_CHUNK_RAW_BYTES};
const KEY_VERSION: &[u8] = b"version".as_slice();
const CURRENT_VERSION: u32 = 2;
const KEY_COUNTERS: &[u8] = b"counters".as_slice();
const MIGRATION_COMMIT_CHUNK: usize = 100_000;
struct DbOperation {
column: u8,
key: Vec<u8>,
value: Option<Vec<u8>>,
}
impl From<(u8, Vec<u8>, Option<Vec<u8>>)> for DbOperation {
fn from((column, key, value): (u8, Vec<u8>, Option<Vec<u8>>)) -> Self {
Self { column, key, value }
}
}
struct MigrationBatch<'a> {
db: &'a parity_db::Db,
operations: Vec<DbOperation>,
committed: usize,
}
impl<'a> MigrationBatch<'a> {
fn new(db: &'a parity_db::Db) -> Self {
Self { db, operations: Vec::new(), committed: 0 }
}
fn push(&mut self, operation: DbOperation) -> Result<()> {
self.operations.push(operation);
self.flush_if_full()
}
fn extend(&mut self, operations: impl IntoIterator<Item = DbOperation>) -> Result<()> {
self.operations.extend(operations);
self.flush_if_full()
}
fn flush_if_full(&mut self) -> Result<()> {
if self.operations.len() >= MIGRATION_COMMIT_CHUNK {
self.flush()?;
}
Ok(())
}
fn flush(&mut self) -> Result<()> {
if self.operations.is_empty() {
return Ok(());
}
let operations = std::mem::take(&mut self.operations);
let count = operations.len();
self.db
.commit(
operations
.into_iter()
.map(|operation| (operation.column, operation.key, operation.value)),
)
.map_err(|error| Error::Db(error.to_string()))?;
self.committed += count;
Ok(())
}
fn finish(mut self) -> Result<usize> {
self.flush()?;
Ok(self.committed)
}
}
const LOG_TARGET: &str = "statement-store";
pub const DEFAULT_PURGE_AFTER_SEC: u64 = 2 * 24 * 60 * 60; pub const DEFAULT_MAX_TOTAL_STATEMENTS: usize = 4 * 1024 * 1024; pub const DEFAULT_MAX_TOTAL_SIZE: usize = 2 * 1024 * 1024 * 1024; pub const MAX_STATEMENT_SIZE: usize =
sc_network_statement::config::MAX_STATEMENT_NOTIFICATION_SIZE as usize - 1;
const MAX_EXPIRY_STATEMENTS_PER_ITERATION: usize = 10_000;
const MAX_EXPIRY_ACCOUNTS_PER_ITERATION: usize = 10_000;
const MAX_EXPIRY_TIME_PER_ITERATION: Duration = Duration::from_millis(100);
const NUM_FILTER_WORKERS: usize = 1;
const MAINTENANCE_PERIOD: std::time::Duration = std::time::Duration::from_secs(29);
enum AllowanceBlock {
Best,
Finalized,
}
const ENFORCE_LIMITS_PERIOD: std::time::Duration = std::time::Duration::from_secs(31);
mod col {
pub const META: u8 = 0;
pub const STATEMENTS: u8 = 1;
pub const EXPIRED: u8 = 2;
pub const INDEX_BY_TOPIC: u8 = 3;
pub const INDEX_BY_DEC_KEY: u8 = 4;
pub const INDEX_EVICTED: u8 = 5;
pub const ADMISSION_SEQ: u8 = 6;
pub const INDEX_BY_ACCOUNT: u8 = 7;
pub const INDEX_BY_EXPIRY: u8 = 8;
pub const COUNT: u8 = 9;
}
const BTREE_COLUMNS: [u8; 6] = [
col::INDEX_BY_TOPIC,
col::INDEX_BY_DEC_KEY,
col::INDEX_EVICTED,
col::ADMISSION_SEQ,
col::INDEX_BY_ACCOUNT,
col::INDEX_BY_EXPIRY,
];
#[cfg(not(test))]
const DETAILS_CACHE_BUDGET: usize = 65_536;
#[cfg(test)]
const DETAILS_CACHE_BUDGET: usize = 8;
#[cfg(not(test))]
const SUMMARY_CACHE_ACCOUNTS: u32 = 131_072;
#[cfg(test)]
const SUMMARY_CACHE_ACCOUNTS: u32 = 4;
const INDEX_EMPTY_VALUE: &[u8] = &[];
const DEC_KEY_TAG_NONE: u8 = 0;
const DEC_KEY_TAG_SOME: u8 = 1;
fn topic_index_key(topic: &Topic, hash: &Hash) -> Vec<u8> {
let mut key = Vec::with_capacity(topic.len() + hash.len());
key.extend_from_slice(&topic[..]);
key.extend_from_slice(&hash[..]);
key
}
fn dec_key_index_prefix(dec_key: &Option<DecryptionKey>) -> Vec<u8> {
match dec_key {
None => vec![DEC_KEY_TAG_NONE],
Some(dec_key) => {
let mut prefix = Vec::with_capacity(1 + dec_key.len());
prefix.push(DEC_KEY_TAG_SOME);
prefix.extend_from_slice(&dec_key[..]);
prefix
},
}
}
fn dec_key_index_key(dec_key: &Option<DecryptionKey>, hash: &Hash) -> Vec<u8> {
let mut key = dec_key_index_prefix(dec_key);
key.extend_from_slice(&hash[..]);
key
}
fn parse_dec_key_index_prefix(prefix: &[u8]) -> Option<Option<DecryptionKey>> {
match prefix.split_first() {
Some((&DEC_KEY_TAG_NONE, [])) => Some(None),
Some((&DEC_KEY_TAG_SOME, dec_key)) if dec_key.len() == size_of::<DecryptionKey>() => {
dec_key.try_into().ok().map(Some)
},
_ => {
log::error!(
target: LOG_TARGET,
"Corrupt decryption-key index prefix: {:?}",
HexDisplay::from(&prefix)
);
None
},
}
}
fn evicted_index_key(purge_at: u64, hash: &Hash) -> Vec<u8> {
let mut key = Vec::with_capacity(8 + hash.len());
key.extend_from_slice(&purge_at.to_be_bytes());
key.extend_from_slice(&hash[..]);
key
}
fn account_index_key(account: &AccountId, expiry: Expiry, hash: &Hash) -> Vec<u8> {
let mut key = Vec::with_capacity(account.len() + size_of::<u64>() + hash.len());
key.extend_from_slice(&account[..]);
key.extend_from_slice(&expiry.0.to_be_bytes());
key.extend_from_slice(&hash[..]);
key
}
fn parse_account_index_key(key: &[u8]) -> Option<(AccountId, Expiry, Hash)> {
if key.len() != size_of::<AccountId>() + size_of::<u64>() + size_of::<Hash>() {
log::error!(
target: LOG_TARGET,
"Corrupt account index key: {:?}",
HexDisplay::from(&key)
);
return None;
}
let (account, rest) = key.split_at(size_of::<AccountId>());
let (expiry, hash) = rest.split_at(size_of::<u64>());
Some((
account.try_into().ok()?,
Expiry(u64::from_be_bytes(expiry.try_into().ok()?)),
hash.try_into().ok()?,
))
}
fn expiry_index_key(expiry: Expiry, hash: &Hash) -> Vec<u8> {
let mut key = Vec::with_capacity(size_of::<u64>() + hash.len());
key.extend_from_slice(&expiry.0.to_be_bytes());
key.extend_from_slice(&hash[..]);
key
}
fn parse_time_index_key(key: &[u8]) -> Option<(u64, Hash)> {
if key.len() != size_of::<u64>() + size_of::<Hash>() {
log::error!(target: LOG_TARGET, "Corrupt time index key: {:?}", HexDisplay::from(&key));
return None;
}
let (time, hash) = key.split_at(size_of::<u64>());
Some((u64::from_be_bytes(time.try_into().ok()?), hash.try_into().ok()?))
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
struct EntryDetails {
channel: Option<Channel>,
data_len: usize,
admission_seq: u64,
}
impl Encode for EntryDetails {
fn size_hint(&self) -> usize {
self.channel.size_hint() +
(self.data_len as u32).size_hint() +
self.admission_seq.size_hint()
}
fn encode_to<T: codec::Output + ?Sized>(&self, dest: &mut T) {
self.channel.encode_to(dest);
(self.data_len as u32).encode_to(dest);
self.admission_seq.encode_to(dest);
}
}
impl Decode for EntryDetails {
fn decode<I: codec::Input>(input: &mut I) -> std::result::Result<Self, codec::Error> {
let (channel, data_len, admission_seq) = <(Option<Channel>, u32, u64)>::decode(input)?;
Ok(EntryDetails { channel, data_len: data_len as usize, admission_seq })
}
}
fn account_index_ops(
account: &AccountId,
expiry: Expiry,
hash: &Hash,
details: Option<&EntryDetails>,
) -> [(u8, Vec<u8>, Option<Vec<u8>>); 2] {
[
(
col::INDEX_BY_ACCOUNT,
account_index_key(account, expiry, hash),
details.map(EntryDetails::encode),
),
(
col::INDEX_BY_EXPIRY,
expiry_index_key(expiry, hash),
details.map(|_| INDEX_EMPTY_VALUE.to_vec()),
),
]
}
fn next_account_id(mut account: AccountId) -> Option<AccountId> {
for byte in account.iter_mut().rev() {
match byte.checked_add(1) {
Some(incremented) => {
*byte = incremented;
return Some(account);
},
None => *byte = 0,
}
}
None
}
fn hash_from_index_key(key: &[u8]) -> Option<Hash> {
let Some(prefix_len) = key.len().checked_sub(size_of::<Hash>()) else {
log::error!(target: LOG_TARGET, "Corrupt index key: {:?}", HexDisplay::from(&key));
return None;
};
key[prefix_len..].try_into().ok()
}
fn prefix_from_index_key(key: &[u8]) -> Option<&[u8]> {
let Some(prefix_len) = key.len().checked_sub(size_of::<Hash>()) else {
log::error!(target: LOG_TARGET, "Corrupt index key: {:?}", HexDisplay::from(&key));
return None;
};
Some(&key[..prefix_len])
}
fn statement_index_ops(
hash: &Hash,
statement: &Statement,
insert: bool,
) -> Vec<(u8, Vec<u8>, Option<Vec<u8>>)> {
let value = insert.then(|| INDEX_EMPTY_VALUE.to_vec());
let mut ops = Vec::new();
let mut nt = 0;
while let Some(topic) = statement.topic(nt) {
ops.push((col::INDEX_BY_TOPIC, topic_index_key(&topic, hash), value.clone()));
nt += 1;
}
let dec_key = statement.decryption_key();
ops.push((col::INDEX_BY_DEC_KEY, dec_key_index_key(&dec_key, hash), value));
ops
}
#[derive(Eq, PartialEq, Debug, Ord, PartialOrd, Clone, Copy)]
struct Expiry(u64);
impl Expiry {
fn get_expiration_timestamp_secs(self) -> u64 {
self.0 >> 32
}
}
#[derive(PartialEq, Eq, Clone, Copy)]
struct PriorityKey {
hash: Hash,
expiry: Expiry,
}
impl PartialOrd for PriorityKey {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PriorityKey {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.expiry.cmp(&other.expiry).then_with(|| self.hash.cmp(&other.hash))
}
}
#[derive(PartialEq, Eq)]
struct ChannelEntry {
hash: Hash,
expiry: Expiry,
}
#[derive(Default)]
struct StatementsForAccount {
by_priority: BTreeMap<PriorityKey, EntryDetails>,
channels: HashMap<Channel, ChannelEntry>,
data_size: usize,
}
impl StatementsForAccount {
fn insert_entry(&mut self, hash: Hash, expiry: Expiry, details: EntryDetails) {
self.data_size += details.data_len;
if let Some(channel) = details.channel {
self.channels.insert(channel, ChannelEntry { hash, expiry });
}
self.by_priority.insert(PriorityKey { hash, expiry }, details);
}
fn remove_entry(&mut self, key: &PriorityKey) -> Option<EntryDetails> {
let details = self.by_priority.remove(key)?;
self.data_size -= details.data_len;
if let Some(channel) = details.channel {
self.channels.remove(&channel);
}
Some(details)
}
}
pub const DEFAULT_NETWORK_WORKERS: usize = 1;
pub use sc_network_statement::config::DEFAULT_STATEMENTS_PER_SECOND as DEFAULT_RATE_LIMIT;
#[derive(Debug, Clone, Copy)]
pub struct Config {
pub max_total_statements: usize,
pub max_total_size: usize,
pub purge_after_sec: u64,
pub network_workers: usize,
pub rate_limit: u32,
}
impl Config {
pub fn validate(&self) -> Result<()> {
if self.max_total_statements == 0 {
return Err(Error::InvalidConfig(
"max_total_statements must be greater than zero".into(),
));
}
if self.max_total_size == 0 {
return Err(Error::InvalidConfig("max_total_size must be greater than zero".into()));
}
if self.network_workers == 0 {
return Err(Error::InvalidConfig("network_workers must be greater than zero".into()));
}
Ok(())
}
}
impl Default for Config {
fn default() -> Self {
Config {
max_total_statements: DEFAULT_MAX_TOTAL_STATEMENTS,
max_total_size: DEFAULT_MAX_TOTAL_SIZE,
purge_after_sec: DEFAULT_PURGE_AFTER_SEC,
network_workers: DEFAULT_NETWORK_WORKERS,
rate_limit: DEFAULT_RATE_LIMIT,
}
}
}
struct QueryIndex {
topic_counts: HashMap<Topic, usize>,
dec_key_counts: HashMap<Option<DecryptionKey>, usize>,
recent: HashMap<Hash, u64>,
}
impl QueryIndex {
fn new() -> Self {
QueryIndex {
topic_counts: HashMap::new(),
dec_key_counts: HashMap::new(),
recent: HashMap::new(),
}
}
fn note_insert(&mut self, hash: Hash, statement: &Statement, seq: u64) {
let mut nt = 0;
while let Some(topic) = statement.topic(nt) {
*self.topic_counts.entry(topic).or_insert(0) += 1;
nt += 1;
}
let dec_key = statement.decryption_key();
*self.dec_key_counts.entry(dec_key).or_insert(0) += 1;
self.recent.insert(hash, seq);
}
fn note_remove(&mut self, hash: &Hash, statement: &Statement) {
let mut nt = 0;
while let Some(topic) = statement.topic(nt) {
if let Some(count) = self.topic_counts.get_mut(&topic) {
*count = count.saturating_sub(1);
if *count == 0 {
self.topic_counts.remove(&topic);
}
}
nt += 1;
}
let dec_key = statement.decryption_key();
if let Some(count) = self.dec_key_counts.get_mut(&dec_key) {
*count = count.saturating_sub(1);
if *count == 0 {
self.dec_key_counts.remove(&dec_key);
}
}
self.recent.remove(hash);
}
fn take_recent(&mut self) -> HashMap<Hash, u64> {
std::mem::take(&mut self.recent)
}
}
#[derive(Clone, Copy)]
struct AccountSummary {
count: usize,
data_size: usize,
}
struct SubmitIndex {
account_statements: LruMap<AccountId, StatementsForAccount>,
cached_statement_count: usize,
summaries: LruMap<AccountId, AccountSummary>,
allowance_cursor: Option<AccountId>,
allowance_cycle_seen: usize,
config: Config,
statement_count: usize,
total_size: usize,
evicted_count: usize,
next_seq: u64,
recent_seqs: HashMap<Hash, u64>,
active_scan_floors: BTreeMap<u64, usize>,
}
struct ClientWrapper<Block, Client, BE> {
client: Arc<Client>,
_block: std::marker::PhantomData<Block>,
_backend: std::marker::PhantomData<BE>,
}
impl<Block, Client, BE> ClientWrapper<Block, Client, BE>
where
Block: BlockT,
Block::Hash: From<BlockHash>,
BE: Backend<Block> + 'static,
Client: HeaderBackend<Block> + StorageProvider<Block, BE> + Send + Sync + 'static,
{
fn read_allowance(
&self,
account_id: &AccountId,
allowance_block: AllowanceBlock,
) -> Result<Option<StatementAllowance>> {
use sp_statement_store::{statement_allowance_key, StatementAllowance};
let block_hash = match allowance_block {
AllowanceBlock::Best => self.client.info().best_hash,
AllowanceBlock::Finalized => self.client.info().finalized_hash,
};
let key = statement_allowance_key(account_id);
let storage_key = StorageKey(key);
self.client
.storage(block_hash, &storage_key)
.map_err(|e| Error::Storage(format!("Failed to read allowance: {:?}", e)))?
.map(|value| {
StatementAllowance::decode(&mut &value.0[..])
.map_err(|e| Error::Decode(format!("Failed to decode allowance: {:?}", e)))
})
.transpose()
}
}
pub struct Store {
db: parity_db::Db,
submit_index: RwLock<SubmitIndex>,
query_index: RwLock<QueryIndex>,
read_allowance_fn:
Box<dyn Fn(&AccountId, AllowanceBlock) -> Result<Option<StatementAllowance>> + Send + Sync>,
subscription_manager: SubscriptionsHandle,
keystore: Arc<LocalKeystore>,
known_accounts_count: AtomicUsize,
time_override: Option<u64>,
metrics: PrometheusMetrics,
}
impl ReplaySnapshotProvider for Weak<Store> {
fn register_replay(&self, enqueue: &mut dyn FnMut(u64) -> bool) -> Result<Option<u64>> {
let Some(store) = self.upgrade() else {
return Err(Error::InvalidConfig("statement store is closed".into()));
};
store.register_replay(enqueue)
}
fn replay_batch(
&self,
filter: &OptimizedTopicFilter,
cursor: u64,
watermark: u64,
) -> Result<ReplayBatch> {
let Some(store) = self.upgrade() else {
return Err(Error::InvalidConfig("statement store is closed".into()));
};
store.replay_batch(filter, cursor, watermark)
}
}
struct InsertPlan {
seq: u64,
evicted: Vec<(PriorityKey, EntryDetails)>,
banned: Vec<(Hash, u64)>,
}
#[derive(Clone)]
enum IndexSet {
Topic(Topic),
DecKey(Option<DecryptionKey>),
}
impl IndexSet {
fn column(&self) -> u8 {
match self {
IndexSet::Topic(_) => col::INDEX_BY_TOPIC,
IndexSet::DecKey(_) => col::INDEX_BY_DEC_KEY,
}
}
fn prefix(&self) -> Vec<u8> {
match self {
IndexSet::Topic(t) => t[..].to_vec(),
IndexSet::DecKey(k) => dec_key_index_prefix(k),
}
}
fn member_key(&self, hash: &Hash) -> Vec<u8> {
match self {
IndexSet::Topic(t) => topic_index_key(t, hash),
IndexSet::DecKey(k) => dec_key_index_key(k, hash),
}
}
fn len(&self, read_index: &QueryIndex) -> usize {
match self {
IndexSet::Topic(t) => read_index.topic_counts.get(t).copied().unwrap_or(0),
IndexSet::DecKey(k) => read_index.dec_key_counts.get(k).copied().unwrap_or(0),
}
}
}
impl SubmitIndex {
fn new(config: Config) -> SubmitIndex {
SubmitIndex {
account_statements: LruMap::new(ByLength::new(u32::MAX)),
summaries: LruMap::new(ByLength::new(SUMMARY_CACHE_ACCOUNTS)),
config,
statement_count: 0,
total_size: 0,
next_seq: 0,
cached_statement_count: 0,
allowance_cursor: None,
allowance_cycle_seen: 0,
evicted_count: 0,
recent_seqs: HashMap::new(),
active_scan_floors: BTreeMap::new(),
}
}
fn counters_op(
statement_count: usize,
total_size: usize,
next_seq: u64,
) -> (u8, Vec<u8>, Option<Vec<u8>>) {
(
col::META,
KEY_COUNTERS.to_vec(),
Some((statement_count as u64, total_size as u64, next_seq).encode()),
)
}
fn uncache_record(&mut self, account: &AccountId) -> Option<StatementsForAccount> {
let record = self.account_statements.remove(account)?;
self.cached_statement_count -= record.by_priority.len();
Some(record)
}
fn cache_record(&mut self, account: AccountId, record: StatementsForAccount) {
if record.by_priority.is_empty() {
return;
}
self.cached_statement_count += record.by_priority.len();
self.account_statements.insert(account, record);
while self.cached_statement_count > DETAILS_CACHE_BUDGET &&
self.account_statements.len() > 1
{
match self.account_statements.pop_oldest() {
Some((_, evicted)) => self.cached_statement_count -= evicted.by_priority.len(),
None => break,
}
}
}
fn cache_record_with_summary(&mut self, account: AccountId, record: StatementsForAccount) {
if !record.by_priority.is_empty() {
self.summaries.insert(
account,
AccountSummary { count: record.by_priority.len(), data_size: record.data_size },
);
}
self.cache_record(account, record);
}
fn note_seq(&mut self, hash: Hash, seq: u64) {
if !self.active_scan_floors.is_empty() {
self.recent_seqs.insert(hash, seq);
}
}
fn begin_scan(&mut self) -> u64 {
let watermark = self.next_seq;
*self.active_scan_floors.entry(watermark).or_insert(0) += 1;
watermark
}
fn end_scan(&mut self, watermark: u64) {
if let Some(count) = self.active_scan_floors.get_mut(&watermark) {
*count -= 1;
if *count == 0 {
self.active_scan_floors.remove(&watermark);
}
}
match self.active_scan_floors.keys().next() {
Some(&floor) => self.recent_seqs.retain(|_, seq| *seq >= floor),
None => self.recent_seqs.clear(),
}
}
fn seq_covered_by_snapshot(&self, hash: &Hash, watermark: u64) -> bool {
match self.recent_seqs.get(hash) {
Some(&seq) => seq < watermark,
None => true,
}
}
fn plan_insert(
&self,
record: &StatementsForAccount,
hash: Hash,
statement: &Statement,
account: &AccountId,
validation: &StatementAllowance,
current_time: u64,
) -> std::result::Result<InsertPlan, RejectionReason> {
let statement_len = statement.data_len();
if statement_len > validation.max_size as usize {
log::debug!(
target: LOG_TARGET,
"Ignored oversize message from account {}: {:?} ({} bytes)",
HexDisplay::from(account),
HexDisplay::from(&hash),
statement_len,
);
return Err(RejectionReason::DataTooLarge {
submitted_size: statement_len,
available_size: validation.max_size as usize,
});
}
let mut evicted: Vec<(PriorityKey, EntryDetails)> = Vec::new();
let mut evicted_hashes = HashSet::new();
let mut would_free_size = 0;
let expiry = Expiry(statement.expiry());
let (max_size, max_count) = (validation.max_size as usize, validation.max_count as usize);
if let Some(channel) = statement.channel() {
if let Some(channel_record) = record.channels.get(&channel) {
if expiry <= channel_record.expiry {
log::debug!(
target: LOG_TARGET,
"Ignored lower priority channel message from account {}: {:?} {:?} <= {:?}",
HexDisplay::from(account),
HexDisplay::from(&hash),
expiry,
channel_record.expiry,
);
return Err(RejectionReason::ChannelPriorityTooLow {
submitted_expiry: expiry.0,
min_expiry: channel_record.expiry.0,
});
} else {
log::debug!(
target: LOG_TARGET,
"Replacing higher priority channel message from account {}: {:?} ({:?}) > {:?} ({:?})",
HexDisplay::from(account),
HexDisplay::from(&hash),
expiry,
HexDisplay::from(&channel_record.hash),
channel_record.expiry,
);
let key =
PriorityKey { hash: channel_record.hash, expiry: channel_record.expiry };
if let Some(details) = record.by_priority.get(&key) {
would_free_size += details.data_len;
evicted_hashes.insert(key.hash);
evicted.push((key, *details));
}
}
}
}
for (entry, details) in record.by_priority.iter() {
if (record.data_size - would_free_size + statement_len <= max_size) &&
record.by_priority.len() + 1 - evicted_hashes.len() <= max_count
{
break;
}
if evicted_hashes.contains(&entry.hash) {
continue;
}
if entry.expiry >= expiry {
log::debug!(
target: LOG_TARGET,
"Ignored message from account {} due to constraints {:?} {:?} < {:?}",
HexDisplay::from(account),
HexDisplay::from(&hash),
expiry,
entry.expiry,
);
let retained_size = record.data_size - would_free_size;
if retained_size + statement_len > max_size {
return Err(RejectionReason::DataTooLarge {
submitted_size: statement_len,
available_size: max_size.saturating_sub(retained_size),
});
}
return Err(RejectionReason::AccountFull {
submitted_expiry: expiry.0,
min_expiry: entry.expiry.0,
});
}
evicted_hashes.insert(entry.hash);
would_free_size += details.data_len;
evicted.push((*entry, *details));
}
if !((self.total_size - would_free_size + statement_len <= self.config.max_total_size) &&
self.statement_count + 1 - evicted.len() <= self.config.max_total_statements)
{
log::debug!(
target: LOG_TARGET,
"Ignored statement {} from account {} because the store is full (size={}, count={})",
HexDisplay::from(&hash),
HexDisplay::from(account),
self.total_size,
self.statement_count,
);
return Err(RejectionReason::StoreFull);
}
let banned = evicted
.iter()
.filter_map(|(key, _)| {
let expiry_ts = key.expiry.get_expiration_timestamp_secs();
(current_time < expiry_ts).then(|| {
(
key.hash,
expiry_ts.min(current_time.saturating_add(self.config.purge_after_sec)),
)
})
})
.collect();
Ok(InsertPlan { seq: self.next_seq, evicted, banned })
}
fn apply_insert(
&mut self,
account: &AccountId,
loaded_record: Option<StatementsForAccount>,
hash: Hash,
statement: &Statement,
plan: &InsertPlan,
) {
let statement_len = statement.data_len();
let freed: usize = plan.evicted.iter().map(|(_, details)| details.data_len).sum();
self.statement_count = self.statement_count + 1 - plan.evicted.len();
self.total_size = self.total_size + statement_len - freed;
self.evicted_count += plan.banned.len();
self.next_seq = plan.seq.saturating_add(1);
self.note_seq(hash, plan.seq);
for (key, _) in &plan.evicted {
log::trace!(target: LOG_TARGET, "Expired statement {:?}", HexDisplay::from(&key.hash));
}
let details = EntryDetails {
channel: statement.channel(),
data_len: statement_len,
admission_seq: plan.seq,
};
match loaded_record.or_else(|| self.uncache_record(account)) {
Some(mut record) => {
for (key, _) in &plan.evicted {
record.remove_entry(key);
}
record.insert_entry(hash, Expiry(statement.expiry()), details);
self.cache_record_with_summary(*account, record);
},
None => {
assert!(plan.evicted.is_empty());
if let Some(summary) = self.summaries.get(account) {
summary.count += 1;
summary.data_size += statement_len;
}
},
}
}
fn apply_removal(
&mut self,
account: &AccountId,
key: &PriorityKey,
data_len: usize,
banned: bool,
) {
self.statement_count = self.statement_count.saturating_sub(1);
self.total_size = self.total_size.saturating_sub(data_len);
if banned {
self.evicted_count += 1;
}
if let Some(record) = self.account_statements.peek_mut(account) {
if record.remove_entry(key).is_some() {
self.cached_statement_count -= 1;
}
if record.by_priority.is_empty() {
self.account_statements.remove(account);
}
}
if let Some(summary) = self.summaries.peek_mut(account) {
summary.count = summary.count.saturating_sub(1);
summary.data_size = summary.data_size.saturating_sub(data_len);
if summary.count == 0 {
self.summaries.remove(account);
}
}
}
fn apply_account_removal(
&mut self,
account: &AccountId,
removed_count: usize,
freed_size: usize,
banned_count: usize,
) {
self.statement_count = self.statement_count.saturating_sub(removed_count);
self.total_size = self.total_size.saturating_sub(freed_size);
self.evicted_count += banned_count;
if let Some(record) = self.account_statements.remove(account) {
self.cached_statement_count -= record.by_priority.len();
}
self.summaries.remove(account);
}
}
impl Store {
pub fn new_shared<Block, Client, BE>(
path: &std::path::Path,
config: Config,
client: Arc<Client>,
keystore: Arc<LocalKeystore>,
prometheus: Option<&PrometheusRegistry>,
task_spawner: Box<dyn SpawnNamed>,
) -> Result<Arc<Store>>
where
Block: BlockT,
Block::Hash: From<BlockHash>,
BE: Backend<Block> + 'static,
Client: HeaderBackend<Block> + StorageProvider<Block, BE> + Send + Sync + 'static,
{
let store =
Arc::new(Self::new(path, config, client, keystore, prometheus, task_spawner.clone())?);
let worker_store = store.clone();
task_spawner.spawn(
"statement-store-maintenance",
Some("statement-store"),
Box::pin(async move {
let mut maintenance_interval = tokio::time::interval(MAINTENANCE_PERIOD);
let mut enforce_limits_interval = tokio::time::interval(ENFORCE_LIMITS_PERIOD);
loop {
futures::select! {
_ = maintenance_interval.tick().fuse() => {worker_store.maintain();}
_ = enforce_limits_interval.tick().fuse() => {worker_store.enforce_limits();}
}
}
}),
);
Ok(store)
}
#[doc(hidden)]
pub fn new<Block, Client, BE>(
path: &std::path::Path,
config: Config,
client: Arc<Client>,
keystore: Arc<LocalKeystore>,
prometheus: Option<&PrometheusRegistry>,
task_spawner: Box<dyn SpawnNamed>,
) -> Result<Store>
where
Block: BlockT,
Block::Hash: From<BlockHash>,
BE: Backend<Block> + 'static,
Client: HeaderBackend<Block> + StorageProvider<Block, BE> + Send + Sync + 'static,
{
config.validate()?;
let mut path: std::path::PathBuf = path.into();
path.push("statements");
Self::migrate_columns(&path)?;
let db = Self::open_db(&path)?;
let needs_index_migration = Self::check_db_version(&db)?;
let storage_reader =
ClientWrapper { client, _block: Default::default(), _backend: Default::default() };
let read_allowance_fn =
Box::new(move |account_id: &AccountId, allowance_block: AllowanceBlock| {
storage_reader.read_allowance(account_id, allowance_block)
});
let store = Store {
db,
submit_index: RwLock::new(SubmitIndex::new(config)),
query_index: RwLock::new(QueryIndex::new()),
read_allowance_fn,
keystore,
known_accounts_count: AtomicUsize::new(0),
time_override: None,
metrics: PrometheusMetrics::new(prometheus),
subscription_manager: SubscriptionsHandle::new(
task_spawner.clone(),
NUM_FILTER_WORKERS,
),
};
store.populate(needs_index_migration)?;
Ok(store)
}
fn migrate_columns(path: &std::path::Path) -> Result<()> {
let Some(metadata) =
parity_db::Options::load_metadata(path).map_err(|e| Error::Db(e.to_string()))?
else {
return Ok(());
};
if metadata.columns.len() >= col::COUNT as usize {
return Ok(());
}
let mut migrate_config = parity_db::Options::with_columns(path, 0);
migrate_config.columns = metadata.columns;
while migrate_config.columns.len() < col::COUNT as usize {
let mut new_column_options = parity_db::ColumnOptions::default();
let column = migrate_config.columns.len() as u8;
new_column_options.btree_index = BTREE_COLUMNS.contains(&column);
parity_db::Db::add_column(&mut migrate_config, new_column_options)
.map_err(|e| Error::Db(e.to_string()))?;
}
Ok(())
}
fn open_db(path: &std::path::Path) -> Result<parity_db::Db> {
let mut db_config = parity_db::Options::with_columns(path, col::COUNT);
let statement_col = &mut db_config.columns[col::STATEMENTS as usize];
statement_col.ref_counted = false;
statement_col.preimage = true;
statement_col.uniform = true;
for c in BTREE_COLUMNS {
db_config.columns[c as usize].btree_index = true;
}
parity_db::Db::open_or_create(&db_config).map_err(|e| Error::Db(e.to_string()))
}
fn check_db_version(db: &parity_db::Db) -> Result<bool> {
match db.get(col::META, &KEY_VERSION).map_err(|e| Error::Db(e.to_string()))? {
Some(version) => {
let version = u32::from_le_bytes(
version
.try_into()
.map_err(|_| Error::Db("Error reading database version".into()))?,
);
if version > CURRENT_VERSION {
return Err(Error::Db(format!("Unsupported database version: {version}")));
}
Ok(version < CURRENT_VERSION)
},
None => {
db.commit([(
col::META,
KEY_VERSION.to_vec(),
Some(CURRENT_VERSION.to_le_bytes().to_vec()),
)])
.map_err(|e| Error::Db(e.to_string()))?;
Ok(false)
},
}
}
fn populate(&self, migrate_index: bool) -> Result<()> {
if migrate_index {
self.migrate_database()?;
}
{
let mut submit_index = self.submit_index.write();
let mut query_index = self.query_index.write();
if let Some(counters) =
self.db.get(col::META, KEY_COUNTERS).map_err(|e| Error::Db(e.to_string()))?
{
let (statement_count, total_size, next_seq) =
<(u64, u64, u64)>::decode(&mut counters.as_slice())
.map_err(|_| Error::Db("Error reading the store counters".into()))?;
submit_index.statement_count = statement_count as usize;
submit_index.total_size = total_size as usize;
submit_index.next_seq = next_seq;
}
let mut iter =
self.db.iter(col::ADMISSION_SEQ).map_err(|e| Error::Db(e.to_string()))?;
iter.seek_to_last().map_err(|e| Error::Db(e.to_string()))?;
if let Some((key, _)) = iter.prev().map_err(|e| Error::Db(e.to_string()))? {
let seq = u64::from_be_bytes(
key.try_into()
.map_err(|_| Error::Db("Invalid admission sequence key".into()))?,
);
if submit_index.next_seq <= seq {
log::error!(
target: LOG_TARGET,
"The store counters lag the admission journal (next_seq {} <= last \
admission {}); recovering. This must be a bug, please report.",
submit_index.next_seq,
seq,
);
submit_index.next_seq = seq.saturating_add(1);
}
}
let mut evicted_count = 0usize;
self.db
.iter_column_while(col::EXPIRED, |_| {
evicted_count += 1;
true
})
.map_err(|e| Error::Db(e.to_string()))?;
submit_index.evicted_count = evicted_count;
for (prefix, count) in self.count_index_prefixes(col::INDEX_BY_TOPIC)? {
let Ok(topic) = <[u8; 32]>::try_from(prefix.as_slice()).map(Topic::from) else {
log::error!(
target: LOG_TARGET,
"Corrupt topic index prefix: {:?}",
HexDisplay::from(&prefix)
);
continue;
};
query_index.topic_counts.insert(topic, count);
}
for (prefix, count) in self.count_index_prefixes(col::INDEX_BY_DEC_KEY)? {
let Some(dec_key) = parse_dec_key_index_prefix(&prefix) else { continue };
query_index.dec_key_counts.insert(dec_key, count);
}
}
self.known_accounts_count.store(self.count_accounts()?, AtomicOrdering::Relaxed);
self.maintain();
Ok(())
}
fn migrate_database(&self) -> Result<()> {
let purge_after_sec = self.submit_index.read().config.purge_after_sec;
let mut admission_seqs = HashMap::new();
let mut next_seq = 0u64;
{
let mut iter =
self.db.iter(col::ADMISSION_SEQ).map_err(|e| Error::Db(e.to_string()))?;
iter.seek_to_first().map_err(|e| Error::Db(e.to_string()))?;
while let Some((key, value)) = iter.next().map_err(|e| Error::Db(e.to_string()))? {
let seq = u64::from_be_bytes(
key.try_into()
.map_err(|_| Error::Db("Invalid admission sequence key".into()))?,
);
let hash: Hash = value
.as_slice()
.try_into()
.map_err(|_| Error::Db("Invalid admission sequence hash".into()))?;
admission_seqs.insert(hash, seq);
next_seq = next_seq.max(seq.saturating_add(1));
}
}
let mut migration = MigrationBatch::new(&self.db);
let mut migration_error = None;
let mut statement_count = 0usize;
let mut total_size = 0usize;
self.db
.iter_column_while(col::STATEMENTS, |item| {
let Ok(statement) = Statement::decode(&mut item.value.as_slice()) else {
log::error!(
target: LOG_TARGET,
"Corrupt statement {:?}",
HexDisplay::from(&sp_statement_store::hash_encoded(&item.value))
);
return true;
};
let hash = statement.hash();
let Some(account) = statement.account_id() else {
log::error!(
target: LOG_TARGET,
"Statement without an account id loaded from the DB: {:?}",
HexDisplay::from(&hash)
);
return true;
};
log::trace!(target: LOG_TARGET, "Statement loaded {:?}", HexDisplay::from(&hash));
let persisted_seq = admission_seqs.remove(&hash);
let seq = persisted_seq.unwrap_or_else(|| {
let seq = next_seq;
next_seq = seq.saturating_add(1);
seq
});
statement_count += 1;
total_size += statement.data_len();
let details = EntryDetails {
channel: statement.channel(),
data_len: statement.data_len(),
admission_seq: seq,
};
let admission_op = persisted_seq.is_none().then(|| DbOperation {
column: col::ADMISSION_SEQ,
key: seq.to_be_bytes().to_vec(),
value: Some(hash.to_vec()),
});
let operations = statement_index_ops(&hash, &statement, true)
.into_iter()
.chain(account_index_ops(
&account,
Expiry(statement.expiry()),
&hash,
Some(&details),
))
.map(DbOperation::from)
.chain(admission_op);
if let Err(error) = migration.extend(operations) {
migration_error = Some(error);
return false;
}
true
})
.map_err(|e| Error::Db(e.to_string()))?;
if let Some(error) = migration_error.take() {
return Err(error);
}
self.db
.iter_column_while(col::EXPIRED, |item| {
if let Ok((hash, timestamp)) = <(Hash, u64)>::decode(&mut item.value.as_slice()) {
log::trace!(
target: LOG_TARGET,
"Statement loaded (expired): {:?}",
HexDisplay::from(&hash)
);
let purge_at = timestamp.saturating_add(purge_after_sec);
let operation = DbOperation {
column: col::INDEX_EVICTED,
key: evicted_index_key(purge_at, &hash),
value: Some(INDEX_EMPTY_VALUE.to_vec()),
};
if let Err(error) = migration.push(operation) {
migration_error = Some(error);
return false;
}
}
true
})
.map_err(|e| Error::Db(e.to_string()))?;
if let Some(error) = migration_error {
return Err(error);
}
migration.push(SubmitIndex::counters_op(statement_count, total_size, next_seq).into())?;
let migrated_entries = migration.finish()?;
self.db
.commit([(
col::META,
KEY_VERSION.to_vec(),
Some(CURRENT_VERSION.to_le_bytes().to_vec()),
)])
.map_err(|e| Error::Db(e.to_string()))?;
log::info!(
target: LOG_TARGET,
"Migrated the statement store index to the on-disk format ({} rows)",
migrated_entries
);
Ok(())
}
fn count_index_prefixes(&self, column: u8) -> Result<Vec<(Vec<u8>, usize)>> {
let mut counts: Vec<(Vec<u8>, usize)> = Vec::new();
let mut iter = self.db.iter(column).map_err(|e| Error::Db(e.to_string()))?;
iter.seek_to_first().map_err(|e| Error::Db(e.to_string()))?;
while let Some((key, _)) = iter.next().map_err(|e| Error::Db(e.to_string()))? {
let Some(prefix) = prefix_from_index_key(&key) else { continue };
match counts.last_mut() {
Some((last, count)) if last.as_slice() == prefix => *count += 1,
_ => counts.push((prefix.to_vec(), 1)),
}
}
Ok(counts)
}
fn next_account_from(&self, from: &AccountId) -> Result<Option<AccountId>> {
let mut iter = self.db.iter(col::INDEX_BY_ACCOUNT).map_err(|e| Error::Db(e.to_string()))?;
iter.seek(&from[..]).map_err(|e| Error::Db(e.to_string()))?;
while let Some((key, _)) = iter.next().map_err(|e| Error::Db(e.to_string()))? {
if let Some((account, _, _)) = parse_account_index_key(&key) {
return Ok(Some(account));
}
}
Ok(None)
}
fn count_accounts(&self) -> Result<usize> {
let mut count = 0usize;
let mut cursor = [0u8; 32];
while let Some(account) = self.next_account_from(&cursor)? {
count += 1;
match next_account_id(account) {
Some(next) => cursor = next,
None => break,
}
}
Ok(count)
}
fn load_account_entries(
&self,
account: &AccountId,
) -> Result<Vec<(PriorityKey, EntryDetails)>> {
let mut entries = Vec::new();
let mut iter = self.db.iter(col::INDEX_BY_ACCOUNT).map_err(|e| Error::Db(e.to_string()))?;
iter.seek(&account[..]).map_err(|e| Error::Db(e.to_string()))?;
while let Some((key, value)) = iter.next().map_err(|e| Error::Db(e.to_string()))? {
if !key.starts_with(&account[..]) {
break;
}
let Some((_, expiry, hash)) = parse_account_index_key(&key) else { continue };
let Ok(details) = EntryDetails::decode(&mut value.as_slice()) else {
log::error!(
target: LOG_TARGET,
"Corrupt account index row for statement {:?}",
HexDisplay::from(&hash)
);
continue;
};
entries.push((PriorityKey { hash, expiry }, details));
}
Ok(entries)
}
fn load_account_record(&self, account: &AccountId) -> Result<StatementsForAccount> {
let mut record = StatementsForAccount::default();
for (key, details) in self.load_account_entries(account)? {
record.insert_entry(key.hash, key.expiry, details);
}
Ok(record)
}
fn scan_index_prefix(&self, column: u8, prefix: &[u8]) -> Result<HashSet<Hash>> {
let mut set = HashSet::new();
let mut iter = self.db.iter(column).map_err(|e| Error::Db(e.to_string()))?;
iter.seek(prefix).map_err(|e| Error::Db(e.to_string()))?;
while let Some((key, _)) = iter.next().map_err(|e| Error::Db(e.to_string()))? {
if !key.starts_with(prefix) {
break;
}
if let Some(hash) = hash_from_index_key(&key) {
set.insert(hash);
}
}
Ok(set)
}
fn enumerate_hashes(&self) -> Result<Vec<Hash>> {
let mut hashes = Vec::new();
let mut iter = self.db.iter(col::INDEX_BY_DEC_KEY).map_err(|e| Error::Db(e.to_string()))?;
iter.seek_to_first().map_err(|e| Error::Db(e.to_string()))?;
while let Some((key, _)) = iter.next().map_err(|e| Error::Db(e.to_string()))? {
if let Some(hash) = hash_from_index_key(&key) {
hashes.push(hash);
}
}
Ok(hashes)
}
fn index_set_contains(&self, set: &IndexSet, hash: &Hash) -> Result<bool> {
Ok(self
.db
.get_size(set.column(), &set.member_key(hash))
.map_err(|e| Error::Db(e.to_string()))?
.is_some())
}
fn hash_in_all_sets(&self, hash: &Hash, sets: &[IndexSet]) -> Result<bool> {
sets.iter().try_fold(true, |acc, set| {
Ok::<bool, Error>(acc && self.index_set_contains(set, hash)?)
})
}
fn iterate_with(
&self,
key: Option<DecryptionKey>,
topic_filter: &OptimizedTopicFilter,
f: impl FnMut(&Hash) -> Result<()>,
) -> Result<()> {
match topic_filter {
OptimizedTopicFilter::Any => self.iterate_with_any(key, f),
OptimizedTopicFilter::MatchAll(topics) => self.iterate_with_match_all(key, topics, f),
OptimizedTopicFilter::MatchAny(topics) => self.iterate_with_match_any(key, topics, f),
}
}
fn iterate_with_any(
&self,
key: Option<DecryptionKey>,
mut f: impl FnMut(&Hash) -> Result<()>,
) -> Result<()> {
let prefix = IndexSet::DecKey(key).prefix();
let mut iter = self.db.iter(col::INDEX_BY_DEC_KEY).map_err(|e| Error::Db(e.to_string()))?;
iter.seek(&prefix).map_err(|e| Error::Db(e.to_string()))?;
while let Some((k, _)) = iter.next().map_err(|e| Error::Db(e.to_string()))? {
if !k.starts_with(&prefix) {
break;
}
if let Some(hash) = hash_from_index_key(&k) {
f(&hash)?;
}
}
Ok(())
}
fn iterate_with_match_any(
&self,
key: Option<DecryptionKey>,
topics: &HashSet<Topic>,
mut f: impl FnMut(&Hash) -> Result<()>,
) -> Result<()> {
let key_set = IndexSet::DecKey(key);
for topic in topics {
let prefix = topic[..].to_vec();
let mut iter =
self.db.iter(col::INDEX_BY_TOPIC).map_err(|e| Error::Db(e.to_string()))?;
iter.seek(&prefix).map_err(|e| Error::Db(e.to_string()))?;
while let Some((k, _)) = iter.next().map_err(|e| Error::Db(e.to_string()))? {
if !k.starts_with(&prefix) {
break;
}
if let Some(hash) = hash_from_index_key(&k) {
if self.index_set_contains(&key_set, &hash)? {
f(&hash)?;
}
}
}
}
Ok(())
}
fn iterate_with_match_all(
&self,
key: Option<DecryptionKey>,
topics: &HashSet<Topic>,
mut f: impl FnMut(&Hash) -> Result<()>,
) -> Result<()> {
if topics.len() > MAX_TOPICS {
return Ok(());
}
let mut sets = Vec::with_capacity(topics.len() + 1);
sets.push(IndexSet::DecKey(key));
for topic in topics {
sets.push(IndexSet::Topic(*topic));
}
{
let query_index = self.query_index.read();
sets.sort_by_key(|s| s.len(&query_index));
}
let smallest = self.scan_index_prefix(sets[0].column(), &sets[0].prefix())?;
let others = &sets[1..];
for hash in &smallest {
if self.hash_in_all_sets(hash, others)? {
log::trace!(
target: LOG_TARGET,
"Iterating by topic/key: statement {:?}",
HexDisplay::from(hash)
);
f(hash)?;
}
}
Ok(())
}
fn read_statement_encoded(&self, hash: &Hash) -> Result<Option<Vec<u8>>> {
match self.db.get(col::STATEMENTS, hash).map_err(|e| Error::Db(e.to_string()))? {
Some(entry) => Ok(Some(entry)),
None => {
log::debug!(target: LOG_TARGET, "Missing statement {:?}", HexDisplay::from(hash));
Ok(None)
},
}
}
fn read_statement(&self, hash: &Hash) -> Result<Option<Statement>> {
let Some(entry) = self.read_statement_encoded(hash)? else { return Ok(None) };
match Statement::decode(&mut entry.as_slice()) {
Ok(statement) => Ok(Some(statement)),
Err(_) => {
log::error!(target: LOG_TARGET, "Corrupt statement {:?}", HexDisplay::from(hash));
Ok(None)
},
}
}
fn collect_statements<R>(
&self,
key: Option<DecryptionKey>,
topic_filter: &OptimizedTopicFilter,
mut f: impl FnMut(Statement) -> Option<R>,
) -> Result<Vec<R>> {
let mut result = Vec::new();
self.iterate_with(key, topic_filter, |hash| {
if let Some(statement) = self.read_statement(hash)? {
if let Some(data) = f(statement) {
result.push(data);
}
}
Ok(())
})?;
Ok(result)
}
fn enforce_account_allowance(
&self,
account: &AccountId,
current_time: u64,
budget: &mut usize,
) {
let allowance = match (self.read_allowance_fn)(account, AllowanceBlock::Finalized) {
Ok(Some(allowance)) => allowance,
Ok(None) => {
log::debug!(
target: LOG_TARGET,
"No allowance found for account {:?}, treating as zero allowance",
HexDisplay::from(account)
);
StatementAllowance { max_count: 0, max_size: 0 }
},
Err(e) => {
log::error!(target: LOG_TARGET, "Error reading allowance: {:?}", e);
return;
},
};
let (max_count, max_size) = (allowance.max_count as usize, allowance.max_size as usize);
if let Some(summary) = self.submit_index.read().summaries.peek(account) {
if summary.count <= max_count && summary.data_size <= max_size {
return;
}
}
let entries = match self.load_account_entries(account) {
Ok(entries) => entries,
Err(e) => {
log::warn!(target: LOG_TARGET, "Error reading the account index: {:?}", e);
return;
},
};
let expiry_bound = Expiry(current_time << 32);
let mut remaining_count = 0usize;
let mut remaining_size = 0usize;
for (key, details) in &entries {
if key.expiry >= expiry_bound {
remaining_count += 1;
remaining_size += details.data_len;
}
}
if remaining_count <= max_count && remaining_size <= max_size {
return;
}
log::debug!(
target: LOG_TARGET,
"Account {:?} exceeds allowance: count={}/{}, size={}/{}",
HexDisplay::from(account),
remaining_count,
allowance.max_count,
remaining_size,
allowance.max_size
);
for (key, details) in &entries {
if (remaining_count <= max_count && remaining_size <= max_size) || *budget == 0 {
break;
}
if key.expiry < expiry_bound {
continue;
}
log::debug!(
target: LOG_TARGET,
"Evicting statement {:?} due to allowance enforcement",
HexDisplay::from(&key.hash)
);
if let Err(e) = self.remove(&key.hash) {
log::debug!(
target: LOG_TARGET,
"Error evicting statement {:?}: {:?}",
HexDisplay::from(&key.hash),
e
);
} else {
*budget -= 1;
remaining_count -= 1;
remaining_size -= details.data_len;
}
}
}
#[doc(hidden)]
pub fn enforce_limits(&self) {
self.enforce_limits_bounded(
MAX_EXPIRY_STATEMENTS_PER_ITERATION,
MAX_EXPIRY_ACCOUNTS_PER_ITERATION,
MAX_EXPIRY_TIME_PER_ITERATION,
)
}
fn enforce_limits_bounded(
&self,
statement_budget: usize,
account_budget: usize,
time_budget: Duration,
) {
let _start_check_expiration_timer = self.metrics.start_check_expiration_timer();
let current_time = self.timestamp();
let start = Instant::now();
let mut expired = 0u64;
let mut due = Vec::new();
let scan = (|| -> Result<()> {
let mut iter =
self.db.iter(col::INDEX_BY_EXPIRY).map_err(|e| Error::Db(e.to_string()))?;
iter.seek_to_first().map_err(|e| Error::Db(e.to_string()))?;
while let Some((key, _)) = iter.next().map_err(|e| Error::Db(e.to_string()))? {
let Some((expiry, hash)) = parse_time_index_key(&key) else { continue };
if expiry >= (current_time << 32) {
break;
}
due.push((expiry, hash));
if due.len() >= statement_budget || start.elapsed() >= time_budget {
break;
}
}
Ok(())
})();
if let Err(e) = scan {
log::warn!(target: LOG_TARGET, "Error scanning the expiry index: {:?}", e);
}
for (expiry, hash) in due {
match self.remove_statement(&hash) {
Ok(true) => {
expired += 1;
log::trace!(
target: LOG_TARGET,
"Marked statement {:?} as expired",
HexDisplay::from(&hash)
);
},
Ok(false) => self.report_orphan_expiry_row(expiry, &hash),
Err(e) => {
log::debug!(
target: LOG_TARGET,
"Error marking statement {:?} as expired: {:?}",
HexDisplay::from(&hash),
e
);
},
}
}
let allowance_start = Instant::now();
let mut allowance_budget = statement_budget;
let mut cursor = self.submit_index.read().allowance_cursor;
let mut checked = 0usize;
let mut wrapped = false;
while checked < account_budget &&
allowance_budget > 0 &&
allowance_start.elapsed() < time_budget
{
let from = cursor.unwrap_or([0u8; 32]);
let account = match self.next_account_from(&from) {
Ok(Some(account)) => account,
Ok(None) => {
wrapped = true;
break;
},
Err(e) => {
log::warn!(target: LOG_TARGET, "Error reading the account index: {:?}", e);
break;
},
};
checked += 1;
let budget_before = allowance_budget;
self.enforce_account_allowance(&account, current_time, &mut allowance_budget);
expired += (budget_before - allowance_budget) as u64;
cursor = match next_account_id(account) {
next @ Some(_) => next,
None => {
wrapped = true;
break;
},
};
}
{
let mut submit_index = self.submit_index.write();
submit_index.allowance_cycle_seen += checked;
if wrapped {
self.known_accounts_count
.store(submit_index.allowance_cycle_seen, AtomicOrdering::Relaxed);
submit_index.allowance_cycle_seen = 0;
submit_index.allowance_cursor = None;
} else {
submit_index.allowance_cursor = cursor;
}
}
drop(_start_check_expiration_timer);
self.metrics.report(|metrics| {
metrics.statements_expired_total.inc_by(expired);
});
}
fn drain_due_evicted(&self, current_time: u64) -> Result<usize> {
let mut commit: Vec<(u8, Vec<u8>, Option<Vec<u8>>)> = Vec::new();
let mut drained = 0usize;
{
let mut iter =
self.db.iter(col::INDEX_EVICTED).map_err(|e| Error::Db(e.to_string()))?;
iter.seek_to_first().map_err(|e| Error::Db(e.to_string()))?;
loop {
let Some((key, _)) = iter.next().map_err(|e| Error::Db(e.to_string()))? else {
break;
};
let Some((purge_at, hash)) = parse_time_index_key(&key) else { continue };
if purge_at > current_time {
break;
}
commit.push((col::EXPIRED, hash.to_vec(), None));
commit.push((col::INDEX_EVICTED, key, None));
drained += 1;
}
}
if !commit.is_empty() {
self.db.commit(commit).map_err(|e| Error::Db(e.to_string()))?;
}
Ok(drained)
}
pub fn maintain(&self) {
log::trace!(target: LOG_TARGET, "Started store maintenance");
let current_time = self.timestamp();
let deleted_count = match self.drain_due_evicted(current_time) {
Ok(count) => count as u64,
Err(e) => {
log::warn!(target: LOG_TARGET, "Error writing to the statement database: {:?}", e);
0
},
};
let (active_count, expired_count, total_size, capacity_statements, capacity_bytes) = {
let mut submit_index = self.submit_index.write();
submit_index.evicted_count =
submit_index.evicted_count.saturating_sub(deleted_count as usize);
(
submit_index.statement_count,
submit_index.evicted_count,
submit_index.total_size,
submit_index.config.max_total_statements,
submit_index.config.max_total_size,
)
};
let accounts_count = self.known_accounts_count.load(AtomicOrdering::Relaxed);
if deleted_count > 0 {
self.metrics.report(|metrics| metrics.statements_pruned.inc_by(deleted_count));
}
self.metrics.report(|metrics| {
metrics.statements_total.set(active_count as u64);
metrics.bytes_total.set(total_size as u64);
metrics.accounts_total.set(accounts_count as u64);
metrics.expired_total.set(expired_count as u64);
metrics.capacity_statements.set(capacity_statements as u64);
metrics.capacity_bytes.set(capacity_bytes as u64);
});
log::trace!(
target: LOG_TARGET,
"Completed store maintenance. Purged: {}, Active: {}, Expired: {}",
deleted_count,
active_count,
expired_count
);
}
fn timestamp(&self) -> u64 {
self.time_override.unwrap_or_else(|| {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
})
}
#[cfg(test)]
fn set_time(&mut self, time: u64) {
self.time_override = Some(time);
}
pub fn as_statement_store_ext(self: Arc<Self>) -> StatementStoreExt {
StatementStoreExt::new(self)
}
fn posted_clear_inner<R>(
&self,
match_all_topics: &[Topic],
dest: [u8; 32],
mut map_f: impl FnMut(Statement, Vec<u8>) -> R,
) -> Result<Vec<R>> {
self.collect_statements(
Some(dest),
&OptimizedTopicFilter::MatchAll(match_all_topics.iter().cloned().collect()),
|statement| {
if let (Some(key), Some(_)) = (statement.decryption_key(), statement.data()) {
let public: sp_core::ed25519::Public = UncheckedFrom::unchecked_from(key);
let public: sp_statement_store::ed25519::Public = public.into();
match self.keystore.key_pair::<sp_statement_store::ed25519::Pair>(&public) {
Err(e) => {
log::debug!(
target: LOG_TARGET,
"Keystore error: {:?}, for statement {:?}",
e,
HexDisplay::from(&statement.hash())
);
None
},
Ok(None) => {
log::debug!(
target: LOG_TARGET,
"Keystore is missing key for statement {:?}",
HexDisplay::from(&statement.hash())
);
None
},
Ok(Some(pair)) => match statement.decrypt_private(&pair.into_inner()) {
Ok(r) => r.map(|data| map_f(statement, data)),
Err(e) => {
log::debug!(
target: LOG_TARGET,
"Decryption error: {:?}, for statement {:?}",
e,
HexDisplay::from(&statement.hash())
);
None
},
},
}
} else {
None
}
},
)
}
}
impl StatementStore for Store {
fn statements(&self) -> Result<Vec<(Hash, Statement)>> {
let hashes = self.enumerate_hashes()?;
let mut result = Vec::with_capacity(hashes.len());
for hash in hashes {
let Some(encoded) =
self.db.get(col::STATEMENTS, &hash).map_err(|e| Error::Db(e.to_string()))?
else {
continue;
};
match Statement::decode(&mut encoded.as_slice()) {
Ok(statement) => result.push((hash, statement)),
Err(_) => log::error!(
target: LOG_TARGET,
"Corrupt statement {:?}",
HexDisplay::from(&hash)
),
}
}
Ok(result)
}
fn take_recent_statements(&self) -> Result<Vec<(u64, Hash, Statement)>> {
let recent = self.query_index.write().take_recent();
let mut result = Vec::with_capacity(recent.len());
for (hash, seq) in recent {
let Some(encoded) =
self.db.get(col::STATEMENTS, &hash).map_err(|e| Error::Db(e.to_string()))?
else {
continue;
};
match Statement::decode(&mut encoded.as_slice()) {
Ok(statement) => result.push((seq, hash, statement)),
Err(_) => log::error!(
target: LOG_TARGET,
"Corrupt statement {:?}",
HexDisplay::from(&hash)
),
}
}
result.sort_unstable_by_key(|(seq, ..)| *seq);
Ok(result)
}
fn admission_watermark(&self) -> Result<u64> {
Ok(self.submit_index.read().next_seq)
}
fn admitted_statements(
&self,
mut cursor: u64,
watermark: u64,
scan_limit: usize,
filter: &mut dyn FnMut(&Hash, &[u8], &Statement) -> FilterDecision,
) -> Result<AdmittedBatch> {
debug_assert!(
cursor <= watermark,
"admission cursor {cursor} must not exceed the watermark {watermark}"
);
debug_assert!(scan_limit > 0, "a zero scan limit cannot advance the cursor");
let mut statements = Vec::new();
let mut aborted = false;
let mut scanned = 0usize;
let mut iter = self.db.iter(col::ADMISSION_SEQ).map_err(|e| Error::Db(e.to_string()))?;
iter.seek(&cursor.to_be_bytes()).map_err(|e| Error::Db(e.to_string()))?;
while let Some((key, value)) = iter.next().map_err(|e| Error::Db(e.to_string()))? {
let seq = u64::from_be_bytes(
key.try_into().map_err(|_| Error::Db("Invalid admission sequence key".into()))?,
);
if seq >= watermark {
cursor = watermark;
break;
}
if scanned == scan_limit {
aborted = true;
break;
}
scanned += 1;
let hash: Hash = value
.as_slice()
.try_into()
.map_err(|_| Error::Db("Invalid admission sequence hash".into()))?;
let next_cursor = seq.saturating_add(1);
let Some(encoded) =
self.db.get(col::STATEMENTS, &hash).map_err(|e| Error::Db(e.to_string()))?
else {
cursor = next_cursor;
continue;
};
let is_current = self
.db
.get(col::ADMISSION_SEQ, &seq.to_be_bytes())
.map_err(|e| Error::Db(e.to_string()))?
.is_some_and(|current| current.as_slice() == hash.as_slice());
if !is_current {
cursor = next_cursor;
continue;
}
let statement = match Statement::decode(&mut encoded.as_slice()) {
Ok(statement) => statement,
Err(e) => {
log::error!(
target: LOG_TARGET,
"Could not decode statement {:?} while walking the admission journal: {:?}",
HexDisplay::from(&hash),
e
);
cursor = next_cursor;
continue;
},
};
match filter(&hash, &encoded, &statement) {
FilterDecision::Skip => cursor = next_cursor,
FilterDecision::Take => {
statements.push((hash, statement));
cursor = next_cursor;
},
FilterDecision::Abort => {
aborted = true;
break;
},
}
}
if !aborted && cursor < watermark {
cursor = watermark;
}
Ok(AdmittedBatch { statements, cursor, done: cursor >= watermark })
}
fn statement(&self, hash: &Hash) -> Result<Option<Statement>> {
Ok(
match self
.db
.get(col::STATEMENTS, hash.as_slice())
.map_err(|e| Error::Db(e.to_string()))?
{
Some(entry) => {
log::trace!(
target: LOG_TARGET,
"Queried statement {:?}",
HexDisplay::from(hash)
);
Some(Statement::decode(&mut entry.as_slice()).map_err(|e| {
log::error!(
target: LOG_TARGET,
"Corrupt statement {:?}",
HexDisplay::from(hash)
);
Error::Decode(e.to_string())
})?)
},
None => {
log::trace!(
target: LOG_TARGET,
"Queried missing statement {:?}",
HexDisplay::from(hash)
);
None
},
},
)
}
fn has_statement(&self, hash: &Hash) -> bool {
match self.db.get_size(col::STATEMENTS, hash.as_slice()) {
Ok(size) => size.is_some(),
Err(e) => {
log::debug!(
target: LOG_TARGET,
"Error checking statement presence {:?}: {:?}",
HexDisplay::from(hash),
e
);
false
},
}
}
fn statements_by_hashes(
&self,
hashes: &[Hash],
filter: &mut dyn FnMut(&Hash, &[u8], &Statement) -> FilterDecision,
) -> Result<(Vec<(Hash, Statement)>, usize)> {
let mut result = Vec::new();
let mut processed = 0;
for hash in hashes {
processed += 1;
let Some(encoded) =
self.db.get(col::STATEMENTS, hash).map_err(|e| Error::Db(e.to_string()))?
else {
continue;
};
let Ok(statement) = Statement::decode(&mut encoded.as_slice()) else {
log::error!(target: LOG_TARGET, "Corrupt statement {:?}", HexDisplay::from(hash));
continue;
};
match filter(hash, &encoded, &statement) {
FilterDecision::Skip => {},
FilterDecision::Take => {
result.push((*hash, statement));
},
FilterDecision::Abort => {
processed -= 1;
break;
},
}
}
Ok((result, processed))
}
fn broadcasts(&self, match_all_topics: &[Topic]) -> Result<Vec<Vec<u8>>> {
self.collect_statements(
None,
&OptimizedTopicFilter::MatchAll(match_all_topics.iter().cloned().collect()),
|statement| statement.into_data(),
)
}
fn posted(&self, match_all_topics: &[Topic], dest: [u8; 32]) -> Result<Vec<Vec<u8>>> {
self.collect_statements(
Some(dest),
&OptimizedTopicFilter::MatchAll(match_all_topics.iter().cloned().collect()),
|statement| statement.into_data(),
)
}
fn posted_clear(&self, match_all_topics: &[Topic], dest: [u8; 32]) -> Result<Vec<Vec<u8>>> {
self.posted_clear_inner(match_all_topics, dest, |_statement, data| data)
}
fn broadcasts_stmt(&self, match_all_topics: &[Topic]) -> Result<Vec<Vec<u8>>> {
self.collect_statements(
None,
&OptimizedTopicFilter::MatchAll(match_all_topics.iter().cloned().collect()),
|statement| Some(statement.encode()),
)
}
fn posted_stmt(&self, match_all_topics: &[Topic], dest: [u8; 32]) -> Result<Vec<Vec<u8>>> {
self.collect_statements(
Some(dest),
&OptimizedTopicFilter::MatchAll(match_all_topics.iter().cloned().collect()),
|statement| Some(statement.encode()),
)
}
fn posted_clear_stmt(
&self,
match_all_topics: &[Topic],
dest: [u8; 32],
) -> Result<Vec<Vec<u8>>> {
self.posted_clear_inner(match_all_topics, dest, |statement, data| {
let mut res = Vec::with_capacity(statement.size_hint() + data.len());
statement.encode_to(&mut res);
res.extend_from_slice(&data);
res
})
}
fn submit(&self, statement: Statement, source: StatementSource) -> SubmitResult {
let _histogram_submit_start_timer = self.metrics.start_submit_timer();
let hash = statement.hash();
if statement.is_expired(self.timestamp()) {
log::debug!(
target: LOG_TARGET,
"Statement is already expired: {:?}",
HexDisplay::from(&hash),
);
let reason = InvalidReason::AlreadyExpired;
self.metrics.report(|metrics| {
metrics.validations_invalid.with_label_values(&[reason.label()]).inc();
});
return SubmitResult::Invalid(reason);
}
let encoded_size = statement.encoded_size();
if encoded_size > MAX_STATEMENT_SIZE {
log::debug!(
target: LOG_TARGET,
"Statement is too big for propogation: {:?} ({}/{} bytes)",
HexDisplay::from(&hash),
statement.encoded_size(),
MAX_STATEMENT_SIZE
);
let reason = InvalidReason::EncodingTooLarge {
submitted_size: encoded_size,
max_size: MAX_STATEMENT_SIZE,
};
self.metrics.report(|metrics| {
metrics.validations_invalid.with_label_values(&[reason.label()]).inc();
});
return SubmitResult::Invalid(reason);
}
if self.db.get_size(col::STATEMENTS, hash.as_slice()).ok().flatten().is_some() {
if !source.can_be_resubmitted() {
self.metrics.report(|metrics| {
metrics.known_statements.with_label_values(&["known"]).inc();
});
return SubmitResult::Known;
}
} else if self.db.get_size(col::EXPIRED, hash.as_slice()).ok().flatten().is_some() {
if !source.can_be_resubmitted() {
self.metrics.report(|metrics| {
metrics.known_statements.with_label_values(&["known_expired"]).inc();
});
return SubmitResult::KnownExpired;
}
}
let Some(account_id) = statement.account_id() else {
log::debug!(
target: LOG_TARGET,
"Statement validation failed: Missing proof ({:?})",
HexDisplay::from(&hash),
);
let reason = InvalidReason::NoProof;
self.metrics.report(|metrics| {
metrics.validations_invalid.with_label_values(&[reason.label()]).inc();
});
return SubmitResult::Invalid(reason);
};
match statement.verify_signature() {
SignatureVerificationResult::Valid(_) => {},
SignatureVerificationResult::Invalid => {
log::debug!(
target: LOG_TARGET,
"Statement validation failed: BadProof, {:?}",
HexDisplay::from(&hash),
);
let reason = InvalidReason::BadProof;
self.metrics.report(|metrics| {
metrics.validations_invalid.with_label_values(&[reason.label()]).inc();
});
return SubmitResult::Invalid(reason);
},
SignatureVerificationResult::NoSignature => {
log::debug!(
target: LOG_TARGET,
"Statement validation failed: NoProof, {:?}",
HexDisplay::from(&hash),
);
let reason = InvalidReason::NoProof;
self.metrics.report(|metrics| {
metrics.validations_invalid.with_label_values(&[reason.label()]).inc();
});
return SubmitResult::Invalid(reason);
},
};
let validation = match (self.read_allowance_fn)(&account_id, AllowanceBlock::Best) {
Ok(Some(allowance)) if !allowance.is_depleted() => allowance,
Ok(Some(_)) | Ok(None) => {
log::debug!(
target: LOG_TARGET,
"Account {} has no usable statement allowance",
HexDisplay::from(&account_id),
);
let reason = RejectionReason::NoAllowance;
self.metrics.report(|metrics| {
metrics.rejections.with_label_values(&[reason.label()]).inc();
});
return SubmitResult::Rejected(reason);
},
Err(e) => {
log::debug!(
target: LOG_TARGET,
"Reading statement allowance for account {} failed",
HexDisplay::from(&account_id),
);
self.metrics.report(|metrics| {
metrics.internal_errors.with_label_values(&["read_allowance"]).inc();
});
return SubmitResult::InternalError(e);
},
};
let current_time = self.timestamp();
let seq = {
let mut submit_index = self.submit_index.write();
match self.db.get_size(col::STATEMENTS, hash.as_slice()) {
Ok(Some(_)) => {
self.metrics.report(|metrics| {
metrics.known_statements.with_label_values(&["known"]).inc();
});
return SubmitResult::Known;
},
Ok(None) => {},
Err(e) => {
self.metrics.report(|metrics| {
metrics.internal_errors.with_label_values(&["db_read"]).inc();
});
return SubmitResult::InternalError(Error::Db(e.to_string()));
},
}
let statement_len = statement.data_len();
let oversize = statement_len > validation.max_size as usize;
let cached = submit_index.account_statements.peek(&account_id).is_some();
let summary_admits = !cached &&
statement.channel().is_none() &&
submit_index.summaries.peek(&account_id).is_some_and(|summary| {
summary.count < validation.max_count as usize &&
summary.data_size + statement_len <= validation.max_size as usize
}) && submit_index.statement_count <
submit_index.config.max_total_statements &&
submit_index.total_size + statement_len <= submit_index.config.max_total_size;
let loaded_record = if cached || summary_admits || oversize {
None
} else {
match self.load_account_record(&account_id) {
Ok(record) => Some(record),
Err(e) => {
self.metrics.report(|metrics| {
metrics.internal_errors.with_label_values(&["db_read"]).inc();
});
return SubmitResult::InternalError(e);
},
}
};
let empty_record = StatementsForAccount::default();
let record = loaded_record
.as_ref()
.or_else(|| submit_index.account_statements.peek(&account_id))
.unwrap_or(&empty_record);
let plan = match submit_index.plan_insert(
record,
hash,
&statement,
&account_id,
&validation,
current_time,
) {
Ok(plan) => plan,
Err(reason) => {
self.metrics.report(|metrics| {
metrics.rejections.with_label_values(&[reason.label()]).inc();
});
if let Some(record) = loaded_record {
submit_index.cache_record_with_summary(account_id, record);
}
return SubmitResult::Rejected(reason);
},
};
let mut commit = Vec::new();
commit.push((col::STATEMENTS, hash.to_vec(), Some(statement.encode())));
commit.push((col::ADMISSION_SEQ, plan.seq.to_be_bytes().to_vec(), Some(hash.to_vec())));
commit.extend(statement_index_ops(&hash, &statement, true));
let details = EntryDetails {
channel: statement.channel(),
data_len: statement_len,
admission_seq: plan.seq,
};
commit.extend(account_index_ops(
&account_id,
Expiry(statement.expiry()),
&hash,
Some(&details),
));
let mut evicted_statements = Vec::new();
for (key, evicted_details) in &plan.evicted {
commit.push((col::STATEMENTS, key.hash.to_vec(), None));
commit.push((
col::ADMISSION_SEQ,
evicted_details.admission_seq.to_be_bytes().to_vec(),
None,
));
commit.extend(account_index_ops(&account_id, key.expiry, &key.hash, None));
match self.db.get(col::STATEMENTS, &key.hash) {
Ok(Some(encoded)) => match Statement::decode(&mut encoded.as_slice()) {
Ok(evicted_statement) => {
commit.extend(statement_index_ops(
&key.hash,
&evicted_statement,
false,
));
evicted_statements.push(evicted_statement);
},
Err(_) => log::error!(
target: LOG_TARGET,
"Corrupt statement {:?}",
HexDisplay::from(&key.hash)
),
},
Ok(None) => log::error!(
target: LOG_TARGET,
"Missing body of the indexed statement {:?}",
HexDisplay::from(&key.hash)
),
Err(e) => log::warn!(
target: LOG_TARGET,
"Could not read evicted statement {:?} to clear its index: {:?}",
HexDisplay::from(&key.hash),
e
),
}
}
for (h, purge_at) in &plan.banned {
commit.push((col::EXPIRED, h.to_vec(), Some((h, current_time).encode())));
commit.push((
col::INDEX_EVICTED,
evicted_index_key(*purge_at, h),
Some(INDEX_EMPTY_VALUE.to_vec()),
));
}
let freed: usize = plan.evicted.iter().map(|(_, details)| details.data_len).sum();
commit.push(SubmitIndex::counters_op(
submit_index.statement_count + 1 - plan.evicted.len(),
submit_index.total_size + statement_len - freed,
plan.seq.saturating_add(1),
));
if let Err(e) = self.db.commit(commit) {
log::debug!(
target: LOG_TARGET,
"Statement validation failed: database error {}, {:?}",
e,
statement
);
self.metrics.report(|metrics| {
metrics.internal_errors.with_label_values(&["db_commit"]).inc();
});
return SubmitResult::InternalError(Error::Db(e.to_string()));
}
let seq = plan.seq;
submit_index.apply_insert(&account_id, loaded_record, hash, &statement, &plan);
{
let mut query_index = self.query_index.write();
for evicted_statement in &evicted_statements {
query_index.note_remove(&evicted_statement.hash(), evicted_statement);
}
query_index.note_insert(hash, &statement, plan.seq);
}
seq
}; self.subscription_manager.notify(seq, statement);
self.metrics.report(|metrics| metrics.submitted_statements.inc());
log::trace!(target: LOG_TARGET, "Statement submitted: {:?}", HexDisplay::from(&hash));
SubmitResult::New
}
fn remove(&self, hash: &Hash) -> Result<()> {
self.remove_statement(hash).map(|_| ())
}
fn remove_by(&self, who: [u8; 32]) -> Result<()> {
let current_time = self.timestamp();
{
let mut submit_index = self.submit_index.write();
let entries: Vec<(PriorityKey, EntryDetails)> =
match submit_index.account_statements.peek(&who) {
Some(record) => {
record.by_priority.iter().map(|(key, details)| (*key, *details)).collect()
},
None => self.load_account_entries(&who)?,
};
if entries.is_empty() {
return Ok(());
}
let mut commit = Vec::new();
let mut removed_statements = Vec::new();
let mut banned_count = 0usize;
let mut freed_size = 0usize;
for (key, details) in &entries {
commit.push((col::STATEMENTS, key.hash.to_vec(), None));
commit.push((
col::ADMISSION_SEQ,
details.admission_seq.to_be_bytes().to_vec(),
None,
));
commit.extend(account_index_ops(&who, key.expiry, &key.hash, None));
if current_time < key.expiry.get_expiration_timestamp_secs() {
let purge_at = key
.expiry
.get_expiration_timestamp_secs()
.min(current_time.saturating_add(submit_index.config.purge_after_sec));
commit.push((
col::EXPIRED,
key.hash.to_vec(),
Some((key.hash, current_time).encode()),
));
commit.push((
col::INDEX_EVICTED,
evicted_index_key(purge_at, &key.hash),
Some(INDEX_EMPTY_VALUE.to_vec()),
));
banned_count += 1;
}
freed_size += details.data_len;
match self.db.get(col::STATEMENTS, &key.hash) {
Ok(Some(encoded)) => match Statement::decode(&mut encoded.as_slice()) {
Ok(statement) => {
commit.extend(statement_index_ops(&key.hash, &statement, false));
removed_statements.push((key.hash, statement));
},
Err(_) => log::error!(
target: LOG_TARGET,
"Corrupt statement {:?}",
HexDisplay::from(&key.hash)
),
},
Ok(None) => log::error!(
target: LOG_TARGET,
"Missing body of the indexed statement {:?}",
HexDisplay::from(&key.hash)
),
Err(e) => {
log::warn!(
target: LOG_TARGET,
"Could not read statement {:?} to clear its index: {:?}",
HexDisplay::from(&key.hash),
e
);
},
}
}
commit.push(SubmitIndex::counters_op(
submit_index.statement_count.saturating_sub(entries.len()),
submit_index.total_size.saturating_sub(freed_size),
submit_index.next_seq,
));
self.db.commit(commit).map_err(|e| {
log::debug!(
target: LOG_TARGET,
"Error removing statement: database error {}, remove by {:?}",
e,
HexDisplay::from(&who),
);
Error::Db(e.to_string())
})?;
submit_index.apply_account_removal(&who, entries.len(), freed_size, banned_count);
let mut query_index = self.query_index.write();
for (hash, statement) in &removed_statements {
query_index.note_remove(hash, statement);
}
}
Ok(())
}
}
struct ScanGuard<'a> {
store: &'a Store,
watermark: u64,
}
impl Drop for ScanGuard<'_> {
fn drop(&mut self) {
self.store.submit_index.write().end_scan(self.watermark);
}
}
impl StatementStoreSubscriptionApi for Store {
fn subscribe_statement(
&self,
topic_filter: OptimizedTopicFilter,
) -> Result<(Vec<Vec<u8>>, async_channel::Sender<StatementEvent>, SubscriptionStatementsStream)>
{
let (subscription_sender, subscription_stream, watermark) = {
let mut submit_index = self.submit_index.write();
let watermark = submit_index.begin_scan();
let (sender, stream) =
self.subscription_manager.subscribe(topic_filter.clone(), watermark);
(sender, stream, watermark)
};
let _scan_guard = ScanGuard { store: self, watermark };
let mut hashes = HashSet::new();
self.iterate_with(None, &topic_filter, |hash| {
hashes.insert(*hash);
Ok(())
})?;
let hashes: Vec<Hash> = {
let submit_index = self.submit_index.read();
hashes
.into_iter()
.filter(|hash| submit_index.seq_covered_by_snapshot(hash, watermark))
.collect()
};
let mut existing_statements = Vec::with_capacity(hashes.len());
for hash in hashes {
if let Some(entry) = self.read_statement_encoded(&hash)? {
existing_statements.push(entry);
}
}
if existing_statements.is_empty() {
subscription_sender
.send_blocking(StatementEvent::NewStatements {
statements: vec![],
remaining: Some(0),
})
.ok();
}
Ok((existing_statements, subscription_sender, subscription_stream))
}
}
impl Store {
fn remove_statement(&self, hash: &Hash) -> Result<bool> {
let current_time = self.timestamp();
{
let mut submit_index = self.submit_index.write();
let Some(encoded) =
self.db.get(col::STATEMENTS, hash).map_err(|e| Error::Db(e.to_string()))?
else {
return Ok(false);
};
let Some((statement, account)) = Statement::decode(&mut encoded.as_slice())
.ok()
.and_then(|statement| statement.account_id().map(|account| (statement, account)))
else {
log::error!(target: LOG_TARGET, "Corrupt statement {:?}", HexDisplay::from(hash));
return Ok(false);
};
let expiry = Expiry(statement.expiry());
let account_key = account_index_key(&account, expiry, hash);
let details = self
.db
.get(col::INDEX_BY_ACCOUNT, &account_key)
.map_err(|e| Error::Db(e.to_string()))?
.as_deref()
.and_then(|mut value| EntryDetails::decode(&mut value).ok());
let mut commit = vec![(col::STATEMENTS, hash.to_vec(), None)];
commit.extend(statement_index_ops(hash, &statement, false));
commit.extend(account_index_ops(&account, expiry, hash, None));
match &details {
Some(details) => commit.push((
col::ADMISSION_SEQ,
details.admission_seq.to_be_bytes().to_vec(),
None,
)),
None => log::error!(
target: LOG_TARGET,
"Missing or corrupt account index entry for statement {:?}",
HexDisplay::from(hash)
),
}
let banned = current_time < expiry.get_expiration_timestamp_secs();
if banned {
let purge_at = expiry
.get_expiration_timestamp_secs()
.min(current_time.saturating_add(submit_index.config.purge_after_sec));
commit.push((col::EXPIRED, hash.to_vec(), Some((hash, current_time).encode())));
commit.push((
col::INDEX_EVICTED,
evicted_index_key(purge_at, hash),
Some(INDEX_EMPTY_VALUE.to_vec()),
));
}
let data_len = details
.as_ref()
.map_or_else(|| statement.data_len(), |details| details.data_len);
commit.push(SubmitIndex::counters_op(
submit_index.statement_count.saturating_sub(1),
submit_index.total_size.saturating_sub(data_len),
submit_index.next_seq,
));
if let Err(e) = self.db.commit(commit) {
log::debug!(
target: LOG_TARGET,
"Error removing statement: database error {}, {:?}",
e,
HexDisplay::from(hash),
);
return Err(Error::Db(e.to_string()));
}
submit_index.apply_removal(
&account,
&PriorityKey { hash: *hash, expiry },
data_len,
banned,
);
self.query_index.write().note_remove(hash, &statement);
log::trace!(target: LOG_TARGET, "Expired statement {:?}", HexDisplay::from(hash));
}
Ok(true)
}
fn report_orphan_expiry_row(&self, expiry: u64, hash: &Hash) {
let _submit_index = self.submit_index.write();
let body = self.db.get_size(col::STATEMENTS, hash.as_slice());
let row = self.db.get(col::INDEX_BY_EXPIRY, &expiry_index_key(Expiry(expiry), hash));
match (body, row) {
(Ok(None), Ok(Some(_))) => log::error!(
target: LOG_TARGET,
"Orphan expiry index row for statement {:?}",
HexDisplay::from(hash)
),
(Err(e), _) | (_, Err(e)) => {
log::debug!(target: LOG_TARGET, "Error checking statement presence: {:?}", e)
},
_ => {},
}
}
fn register_replay(&self, enqueue: &mut dyn FnMut(u64) -> bool) -> Result<Option<u64>> {
let registered = {
let submit_index = self.submit_index.write();
let watermark = submit_index.next_seq;
enqueue(watermark).then_some(watermark)
}; Ok(registered)
}
fn replay_batch(
&self,
filter: &OptimizedTopicFilter,
cursor: u64,
watermark: u64,
) -> Result<ReplayBatch> {
let mut statements = Vec::new();
let mut chunk_bytes = 0usize;
let batch = StatementStore::admitted_statements(
self,
cursor,
watermark,
usize::MAX,
&mut |_, encoded, statement| {
if !filter.matches(statement) {
return FilterDecision::Skip;
}
if !statements.is_empty() && chunk_bytes + encoded.len() > REPLAY_CHUNK_RAW_BYTES {
return FilterDecision::Abort;
}
chunk_bytes += encoded.len();
statements.push(encoded.to_vec());
FilterDecision::Skip
},
)?;
Ok(ReplayBatch { statements, cursor: batch.cursor, done: batch.done })
}
}
impl MultiFilterSubscriptionApi for Arc<Store> {
fn create_subscription(&self) -> (SubscriptionHandle, MultiFilterEventStream) {
let inner =
Arc::new(parking_lot::Mutex::new(crate::subscription::SubscriptionHandleInner::new()));
let snapshot_provider: Arc<dyn ReplaySnapshotProvider> = Arc::new(Arc::downgrade(self));
let (sub_id, stream) = self.subscription_manager.subscribe_empty(snapshot_provider.clone());
let handle = SubscriptionHandle {
sub_id,
inner,
matchers: self.subscription_manager.matchers(),
snapshot_provider,
};
(handle, stream)
}
}
#[cfg(test)]
impl Store {
fn evicted_count(&self) -> usize {
self.submit_index.read().evicted_count
}
fn is_evicted(&self, hash: &Hash) -> bool {
self.db.get_size(col::EXPIRED, hash.as_slice()).ok().flatten().is_some()
}
fn index_has_topic(&self, topic: &Topic, hash: &Hash) -> bool {
self.index_set_contains(&IndexSet::Topic(*topic), hash).unwrap_or(false)
}
fn index_has_dec_key(&self, key: &Option<DecryptionKey>, hash: &Hash) -> bool {
self.index_set_contains(&IndexSet::DecKey(*key), hash).unwrap_or(false)
}
fn statement_count(&self) -> usize {
self.submit_index.read().statement_count
}
fn details_cached(&self, who: &AccountId) -> bool {
self.submit_index.read().account_statements.peek(who).is_some()
}
fn summary_cached(&self, who: &AccountId) -> bool {
self.submit_index.read().summaries.peek(who).is_some()
}
fn total_size(&self) -> usize {
self.submit_index.read().total_size
}
fn has_account(&self, who: &AccountId) -> bool {
self.next_account_from(who).expect("failed to read the account index") == Some(*who)
}
fn account_count(&self) -> usize {
self.count_accounts().expect("failed to count the accounts")
}
fn force_insert(&self, statement: &Statement) {
let hash = statement.hash();
let account = statement.account_id().expect("test statements are signed; qed");
let mut submit_index = self.submit_index.write();
let seq = submit_index.next_seq;
let details = EntryDetails {
channel: statement.channel(),
data_len: statement.data_len(),
admission_seq: seq,
};
let mut commit = vec![
(col::STATEMENTS, hash.to_vec(), Some(statement.encode())),
(col::ADMISSION_SEQ, seq.to_be_bytes().to_vec(), Some(hash.to_vec())),
];
commit.extend(statement_index_ops(&hash, statement, true));
commit.extend(account_index_ops(
&account,
Expiry(statement.expiry()),
&hash,
Some(&details),
));
commit.push(SubmitIndex::counters_op(
submit_index.statement_count + 1,
submit_index.total_size + statement.data_len(),
seq.saturating_add(1),
));
self.db.commit(commit).expect("failed to commit the statement");
let plan = InsertPlan { seq, evicted: Vec::new(), banned: Vec::new() };
submit_index.apply_insert(&account, None, hash, statement, &plan);
self.query_index.write().note_insert(hash, statement, seq);
}
}
#[cfg(test)]
mod tests {
use crate::{col, Store, KEY_VERSION};
use sc_keystore::Keystore;
use sp_core::{Decode, Encode, Pair};
use sp_statement_store::{
AccountId, Channel, DecryptionKey, FilterDecision, InvalidReason, OptimizedTopicFilter,
Proof, RejectionReason, Statement, StatementSource, StatementStore, SubmitResult, Topic,
};
type Extrinsic = sp_runtime::OpaqueExtrinsic;
type Hash = sp_core::H256;
type Hashing = sp_runtime::traits::BlakeTwo256;
type BlockNumber = u64;
type Header = sp_runtime::generic::Header<BlockNumber, Hashing>;
type Block = sp_runtime::generic::Block<Header, Extrinsic>;
const TEST_BEST_BLOCK_HASH: [u8; 32] = [1u8; 32];
const MAX_TEST_ACCOUNT_SEED: u64 = 64;
fn account_seed_table() -> &'static std::collections::BTreeMap<AccountId, u64> {
use std::sync::OnceLock;
static TABLE: OnceLock<std::collections::BTreeMap<AccountId, u64>> = OnceLock::new();
TABLE.get_or_init(|| {
let mut t = std::collections::BTreeMap::new();
for seed in 0..=MAX_TEST_ACCOUNT_SEED {
t.insert(account_keypair(seed).public().0, seed);
}
t
})
}
#[derive(Clone)]
pub(crate) struct TestClient;
pub(crate) type TestBackend = sc_client_api::in_mem::Backend<Block>;
impl sc_client_api::StorageProvider<Block, TestBackend> for TestClient {
fn storage(
&self,
_hash: Hash,
key: &sc_client_api::StorageKey,
) -> sp_blockchain::Result<Option<sc_client_api::StorageData>> {
use sp_statement_store::StatementAllowance;
assert_eq!(&key.0[0..21], b":statement_allowance:" as &[u8],);
let account_bytes: AccountId = key.0[21..53].try_into().unwrap();
let seed = account_seed_table().get(&account_bytes).copied();
let allowance = match seed {
Some(0) => return Ok(None),
Some(1) => StatementAllowance::new(1, 1000),
Some(2) => StatementAllowance::new(2, 1000),
Some(3) => StatementAllowance::new(3, 1000),
Some(4) => StatementAllowance::new(4, 1000),
Some(42) => StatementAllowance::new(42, (42 * crate::MAX_STATEMENT_SIZE) as u32),
Some(50) => StatementAllowance::new(0, 1000),
Some(51) => StatementAllowance::new(5, 0),
Some(_) | None => StatementAllowance::new(100, 1000),
};
Ok(Some(sc_client_api::StorageData(allowance.encode())))
}
fn storage_hash(
&self,
_hash: Hash,
_key: &sc_client_api::StorageKey,
) -> sp_blockchain::Result<Option<Hash>> {
unimplemented!()
}
fn storage_keys(
&self,
_hash: Hash,
_prefix: Option<&sc_client_api::StorageKey>,
_start_key: Option<&sc_client_api::StorageKey>,
) -> sp_blockchain::Result<
sc_client_api::backend::KeysIter<
<TestBackend as sc_client_api::Backend<Block>>::State,
Block,
>,
> {
unimplemented!()
}
fn storage_pairs(
&self,
_hash: Hash,
_prefix: Option<&sc_client_api::StorageKey>,
_start_key: Option<&sc_client_api::StorageKey>,
) -> sp_blockchain::Result<
sc_client_api::backend::PairsIter<
<TestBackend as sc_client_api::Backend<Block>>::State,
Block,
>,
> {
unimplemented!()
}
fn child_storage(
&self,
_hash: Hash,
_child_info: &sc_client_api::ChildInfo,
_key: &sc_client_api::StorageKey,
) -> sp_blockchain::Result<Option<sc_client_api::StorageData>> {
unimplemented!()
}
fn child_storage_keys(
&self,
_hash: Hash,
_child_info: sc_client_api::ChildInfo,
_prefix: Option<&sc_client_api::StorageKey>,
_start_key: Option<&sc_client_api::StorageKey>,
) -> sp_blockchain::Result<
sc_client_api::backend::KeysIter<
<TestBackend as sc_client_api::Backend<Block>>::State,
Block,
>,
> {
unimplemented!()
}
fn child_storage_hash(
&self,
_hash: Hash,
_child_info: &sc_client_api::ChildInfo,
_key: &sc_client_api::StorageKey,
) -> sp_blockchain::Result<Option<Hash>> {
unimplemented!()
}
fn closest_merkle_value(
&self,
_hash: Hash,
_key: &sc_client_api::StorageKey,
) -> sp_blockchain::Result<Option<sc_client_api::MerkleValue<Hash>>> {
unimplemented!()
}
fn child_closest_merkle_value(
&self,
_hash: Hash,
_child_info: &sc_client_api::ChildInfo,
_key: &sc_client_api::StorageKey,
) -> sp_blockchain::Result<Option<sc_client_api::MerkleValue<Hash>>> {
unimplemented!()
}
}
impl sp_blockchain::HeaderBackend<Block> for TestClient {
fn header(&self, _hash: Hash) -> sp_blockchain::Result<Option<Header>> {
unimplemented!()
}
fn info(&self) -> sp_blockchain::Info<Block> {
sp_blockchain::Info {
best_hash: TEST_BEST_BLOCK_HASH.into(),
best_number: 0,
genesis_hash: Default::default(),
finalized_hash: TEST_BEST_BLOCK_HASH.into(),
finalized_number: 1,
finalized_state: None,
number_leaves: 0,
block_gap: None,
}
}
fn status(&self, _hash: Hash) -> sp_blockchain::Result<sp_blockchain::BlockStatus> {
unimplemented!()
}
fn number(&self, _hash: Hash) -> sp_blockchain::Result<Option<BlockNumber>> {
unimplemented!()
}
fn hash(&self, _number: BlockNumber) -> sp_blockchain::Result<Option<Hash>> {
unimplemented!()
}
}
fn test_store() -> (Store, tempfile::TempDir) {
sp_tracing::init_for_tests();
let temp_dir = tempfile::Builder::new().tempdir().expect("Error creating test dir");
let client = std::sync::Arc::new(TestClient);
let mut path: std::path::PathBuf = temp_dir.path().into();
path.push("db");
let keystore = std::sync::Arc::new(sc_keystore::LocalKeystore::in_memory());
let store = Store::new::<Block, TestClient, TestBackend>(
&path,
Default::default(),
client,
keystore,
None,
Box::new(sp_core::testing::TaskExecutor::new()),
)
.unwrap();
(store, temp_dir) }
pub fn signed_statement(data: u8) -> Statement {
signed_statement_with_topics(data, &[], None)
}
fn signed_statement_with_topics(
data: u8,
topics: &[Topic],
dec_key: Option<DecryptionKey>,
) -> Statement {
let mut statement = Statement::new();
statement.set_plain_data(vec![data]);
statement.set_expiry(u64::MAX);
for i in 0..topics.len() {
statement.set_topic(i, topics[i]);
}
if let Some(key) = dec_key {
statement.set_decryption_key(key);
}
let kp = sp_core::ed25519::Pair::from_string("//Alice", None).unwrap();
statement.sign_ed25519_private(&kp);
statement
}
fn topic(data: u64) -> Topic {
let mut bytes = [0u8; 32];
bytes[0..8].copy_from_slice(&data.to_le_bytes());
Topic::from(bytes)
}
fn dec_key(data: u64) -> DecryptionKey {
let mut dec_key: DecryptionKey = Default::default();
dec_key[0..8].copy_from_slice(&data.to_le_bytes());
dec_key
}
fn account_keypair(seed: u64) -> sp_core::ed25519::Pair {
sp_core::ed25519::Pair::from_string(&format!("//StatementAccount{seed}"), None)
.expect("Derivation path is valid; qed")
}
fn account(id: u64) -> AccountId {
account_keypair(id).public().0
}
fn sign_with(stmt: &mut Statement, account_id: u64) {
stmt.sign_ed25519_private(&account_keypair(account_id));
}
fn channel(id: u64) -> Channel {
let mut channel: Channel = Default::default();
channel[0..8].copy_from_slice(&id.to_le_bytes());
channel
}
fn unsigned_statement(
account_id: u64,
priority: u32,
c: Option<u64>,
data_len: usize,
) -> Statement {
assert!(
account_id <= MAX_TEST_ACCOUNT_SEED,
"account_id {account_id} exceeds MAX_TEST_ACCOUNT_SEED ({MAX_TEST_ACCOUNT_SEED}); \
raise the constant if you need a wider range",
);
let mut statement = Statement::new();
let mut data = Vec::new();
data.resize(data_len, 0);
statement.set_plain_data(data);
statement.set_expiry_from_parts(u32::MAX, priority);
if let Some(c) = c {
statement.set_channel(channel(c));
}
statement
}
fn statement(account_id: u64, priority: u32, c: Option<u64>, data_len: usize) -> Statement {
let mut statement = unsigned_statement(account_id, priority, c, data_len);
sign_with(&mut statement, account_id);
statement
}
#[test]
fn submit_one() {
let (store, _temp) = test_store();
let statement0 = signed_statement(0);
assert_eq!(store.submit(statement0, StatementSource::Network), SubmitResult::New);
let statement1 = statement(1, 1, None, 0);
assert_eq!(store.submit(statement1, StatementSource::Network), SubmitResult::New);
}
#[test]
fn depleted_count_allowance_admits_nothing() {
let (store, _temp) = test_store();
let result = store.submit(statement(50, 1, None, 100), StatementSource::Network);
assert_eq!(result, SubmitResult::Rejected(RejectionReason::NoAllowance));
assert_eq!(store.statement_count(), 0);
assert_eq!(store.total_size(), 0);
}
#[test]
fn depleted_size_allowance_admits_nothing() {
let (store, _temp) = test_store();
let result = store.submit(statement(51, 1, None, 0), StatementSource::Network);
assert_eq!(result, SubmitResult::Rejected(RejectionReason::NoAllowance));
assert_eq!(store.statement_count(), 0);
assert_eq!(store.total_size(), 0);
}
#[test]
fn save_and_load_statements() {
let (store, temp) = test_store();
let statement0 = signed_statement(0);
let statement1 = signed_statement(1);
let statement2 = signed_statement(2);
assert_eq!(store.submit(statement0.clone(), StatementSource::Network), SubmitResult::New);
assert_eq!(store.submit(statement1.clone(), StatementSource::Network), SubmitResult::New);
assert_eq!(store.submit(statement2.clone(), StatementSource::Network), SubmitResult::New);
assert_eq!(store.statements().unwrap().len(), 3);
assert_eq!(store.broadcasts(&[]).unwrap().len(), 3);
assert_eq!(store.statement(&statement1.hash()).unwrap(), Some(statement1.clone()));
let keystore = store.keystore.clone();
drop(store);
let client = std::sync::Arc::new(TestClient);
let mut path: std::path::PathBuf = temp.path().into();
path.push("db");
let store = Store::new::<Block, TestClient, TestBackend>(
&path,
Default::default(),
client,
keystore,
None,
Box::new(sp_core::testing::TaskExecutor::new()),
)
.unwrap();
assert_eq!(store.statements().unwrap().len(), 3);
assert_eq!(store.broadcasts(&[]).unwrap().len(), 3);
assert_eq!(store.statement(&statement1.hash()).unwrap(), Some(statement1));
}
#[test]
fn admission_seq_is_monotonic_across_restarts() {
let (store, temp) = test_store();
let first = signed_statement(10);
let second = signed_statement(11);
assert_eq!(store.submit(first.clone(), StatementSource::Network), SubmitResult::New);
assert_eq!(store.submit(second.clone(), StatementSource::Network), SubmitResult::New);
assert_eq!(
store.db.get(col::ADMISSION_SEQ, &0u64.to_be_bytes()).unwrap(),
Some(first.hash().to_vec())
);
assert_eq!(
store.db.get(col::ADMISSION_SEQ, &1u64.to_be_bytes()).unwrap(),
Some(second.hash().to_vec())
);
store.remove(&second.hash()).unwrap();
assert_eq!(
store.db.get(col::ADMISSION_SEQ, &0u64.to_be_bytes()).unwrap(),
Some(first.hash().to_vec())
);
assert_eq!(store.db.get(col::ADMISSION_SEQ, &1u64.to_be_bytes()).unwrap(), None);
let keystore = store.keystore.clone();
drop(store);
let mut path: std::path::PathBuf = temp.path().into();
path.push("db");
let store = Store::new::<Block, TestClient, TestBackend>(
&path,
Default::default(),
std::sync::Arc::new(TestClient),
keystore,
None,
Box::new(sp_core::testing::TaskExecutor::new()),
)
.unwrap();
assert_eq!(store.submit_index.read().next_seq, 2);
let replay = store.replay_batch(&OptimizedTopicFilter::Any, 0, 2).unwrap();
assert_eq!(replay.statements, vec![first.encode()]);
assert!(replay.done);
let third = signed_statement(12);
assert_eq!(store.submit(third.clone(), StatementSource::Network), SubmitResult::New);
assert_eq!(store.db.get(col::ADMISSION_SEQ, &1u64.to_be_bytes()).unwrap(), None);
assert_eq!(
store.db.get(col::ADMISSION_SEQ, &2u64.to_be_bytes()).unwrap(),
Some(third.hash().to_vec())
);
}
#[test]
fn lagging_counters_row_does_not_reuse_admission_seqs() {
let (store, temp) = test_store();
let first = signed_statement(1);
let second = signed_statement(2);
assert_eq!(store.submit(first.clone(), StatementSource::Network), SubmitResult::New);
assert_eq!(store.submit(second.clone(), StatementSource::Network), SubmitResult::New);
let (statement_count, total_size, _) = <(u64, u64, u64)>::decode(
&mut store.db.get(col::META, crate::KEY_COUNTERS).unwrap().unwrap().as_slice(),
)
.unwrap();
store
.db
.commit([(
col::META,
crate::KEY_COUNTERS.to_vec(),
Some((statement_count, total_size, 0u64).encode()),
)])
.unwrap();
let keystore = store.keystore.clone();
drop(store);
let mut path: std::path::PathBuf = temp.path().into();
path.push("db");
let store = Store::new::<Block, TestClient, TestBackend>(
&path,
Default::default(),
std::sync::Arc::new(TestClient),
keystore,
None,
Box::new(sp_core::testing::TaskExecutor::new()),
)
.unwrap();
assert_eq!(store.submit_index.read().next_seq, 2);
let third = signed_statement(3);
assert_eq!(store.submit(third.clone(), StatementSource::Network), SubmitResult::New);
assert_eq!(
store.db.get(col::ADMISSION_SEQ, &0u64.to_be_bytes()).unwrap(),
Some(first.hash().to_vec())
);
assert_eq!(
store.db.get(col::ADMISSION_SEQ, &1u64.to_be_bytes()).unwrap(),
Some(second.hash().to_vec())
);
assert_eq!(
store.db.get(col::ADMISSION_SEQ, &2u64.to_be_bytes()).unwrap(),
Some(third.hash().to_vec())
);
}
#[test]
fn statement_without_admission_entry_is_restored_by_migration() {
let (store, temp) = test_store();
let statement = signed_statement_with_topics(20, &[topic(1)], None);
assert_eq!(store.submit(statement.clone(), StatementSource::Network), SubmitResult::New);
store
.db
.commit([
(col::ADMISSION_SEQ, 0u64.to_be_bytes().to_vec(), None),
(col::META, KEY_VERSION.to_vec(), Some(1u32.to_le_bytes().to_vec())),
])
.unwrap();
let filter = OptimizedTopicFilter::MatchAny([topic(1)].into_iter().collect());
assert!(store.replay_batch(&filter, 0, 1).unwrap().statements.is_empty());
let keystore = store.keystore.clone();
drop(store);
let mut path: std::path::PathBuf = temp.path().into();
path.push("db");
let store = Store::new::<Block, TestClient, TestBackend>(
&path,
Default::default(),
std::sync::Arc::new(TestClient),
keystore,
None,
Box::new(sp_core::testing::TaskExecutor::new()),
)
.unwrap();
let watermark = store.submit_index.read().next_seq;
assert_eq!(watermark, 1);
assert_eq!(
store.replay_batch(&filter, 0, watermark).unwrap().statements,
vec![statement.encode()]
);
}
#[test]
fn admission_cursor_resumes_from_the_middle_of_the_range() {
let (store, _temp) = test_store();
let first = signed_statement(1);
let second = signed_statement(2);
let third = signed_statement(3);
for statement in [&first, &second, &third] {
assert_eq!(
store.submit(statement.clone(), StatementSource::Network),
SubmitResult::New
);
}
let batch = store.replay_batch(&OptimizedTopicFilter::Any, 1, 3).unwrap();
assert_eq!(batch.statements, vec![second.encode(), third.encode()]);
assert_eq!(batch.cursor, 3);
assert!(batch.done);
}
#[test]
fn admission_cursor_replays_only_current_matching_admissions() {
let (store, _temp) = test_store();
let matching = signed_statement_with_topics(20, &[topic(1)], None);
let non_matching = signed_statement_with_topics(21, &[topic(2)], None);
let removed = signed_statement_with_topics(22, &[topic(1)], None);
assert_eq!(store.submit(matching.clone(), StatementSource::Network), SubmitResult::New);
assert_eq!(store.submit(non_matching, StatementSource::Network), SubmitResult::New);
assert_eq!(store.submit(removed.clone(), StatementSource::Network), SubmitResult::New);
store.remove(&removed.hash()).unwrap();
let replay = store
.replay_batch(&OptimizedTopicFilter::MatchAny([topic(1)].into_iter().collect()), 0, 3)
.unwrap();
assert_eq!(replay.statements, vec![matching.encode()]);
assert_eq!(replay.cursor, 3);
assert!(replay.done);
}
#[test]
fn admission_watermark_is_one_past_the_newest_admission() {
let (store, _temp) = test_store();
assert_eq!(store.admission_watermark().unwrap(), 0);
let first = signed_statement(1);
let second = signed_statement(2);
assert_eq!(store.submit(first.clone(), StatementSource::Network), SubmitResult::New);
assert_eq!(store.submit(second, StatementSource::Network), SubmitResult::New);
assert_eq!(store.admission_watermark().unwrap(), 2);
store.remove(&first.hash()).unwrap();
assert_eq!(store.admission_watermark().unwrap(), 2);
}
#[test]
fn admitted_statements_walk_applies_filter_decisions_and_resumes() {
let (store, _temp) = test_store();
let statements: Vec<_> = (1..=4).map(signed_statement).collect();
for statement in &statements {
assert_eq!(
store.submit(statement.clone(), StatementSource::Network),
SubmitResult::New
);
}
store.remove(&statements[1].hash()).unwrap();
let batch = store
.admitted_statements(0, 4, usize::MAX, &mut |hash, _encoded, _statement| {
if *hash == statements[0].hash() {
FilterDecision::Skip
} else if *hash == statements[3].hash() {
FilterDecision::Abort
} else {
FilterDecision::Take
}
})
.unwrap();
assert_eq!(batch.statements, vec![(statements[2].hash(), statements[2].clone())]);
assert_eq!(batch.cursor, 3);
assert!(!batch.done);
let batch = store
.admitted_statements(batch.cursor, 4, usize::MAX, &mut |_, _, _| FilterDecision::Take)
.unwrap();
assert_eq!(batch.statements, vec![(statements[3].hash(), statements[3].clone())]);
assert_eq!(batch.cursor, 4);
assert!(batch.done);
}
#[test]
fn admitted_statements_walk_stops_at_the_scan_limit() {
let (store, _temp) = test_store();
let statements: Vec<_> = (1..=4).map(signed_statement).collect();
for statement in &statements {
assert_eq!(
store.submit(statement.clone(), StatementSource::Network),
SubmitResult::New
);
}
let mut visited = 0;
let batch = store
.admitted_statements(0, 4, 3, &mut |_, _, _| {
visited += 1;
FilterDecision::Skip
})
.unwrap();
assert!(batch.statements.is_empty());
assert_eq!(visited, 3);
assert_eq!(batch.cursor, 3, "the cursor must sit after the last visited entry");
assert!(!batch.done);
let batch = store
.admitted_statements(batch.cursor, 4, 3, &mut |_, _, _| FilterDecision::Skip)
.unwrap();
assert!(batch.statements.is_empty());
assert_eq!(batch.cursor, 4);
assert!(batch.done);
}
#[test]
fn take_recent_statements_are_ordered_by_admission() {
let (store, _temp) = test_store();
let statements: Vec<_> = (1..=3).map(signed_statement).collect();
for statement in &statements {
assert_eq!(
store.submit(statement.clone(), StatementSource::Network),
SubmitResult::New
);
}
let recent = store.take_recent_statements().unwrap();
let expected: Vec<_> = statements
.into_iter()
.enumerate()
.map(|(seq, statement)| (seq as u64, statement.hash(), statement))
.collect();
assert_eq!(recent, expected);
}
#[test]
fn migrates_v1_database_to_on_disk_index() {
sp_tracing::init_for_tests();
let temp = tempfile::Builder::new().tempdir().expect("Error creating test dir");
let mut path: std::path::PathBuf = temp.path().into();
path.push("db");
let mut db_path = path.clone();
db_path.push("statements");
let addressed = signed_statement_with_topics(1, &[topic(1), topic(2)], Some(dec_key(9)));
let broadcast = signed_statement_with_topics(2, &[topic(1)], None);
let h_addressed = addressed.hash();
let h_broadcast = broadcast.hash();
let expired_hash = topic(999);
let expired_ts = 10_000_000_000u64;
{
let mut cfg = parity_db::Options::with_columns(&db_path, 3);
let statement_col = &mut cfg.columns[1];
statement_col.ref_counted = false;
statement_col.preimage = true;
statement_col.uniform = true;
let db = parity_db::Db::open_or_create(&cfg).unwrap();
db.commit([
(0u8, b"version".to_vec(), Some(1u32.to_le_bytes().to_vec())),
(1u8, h_addressed.to_vec(), Some(addressed.encode())),
(1u8, h_broadcast.to_vec(), Some(broadcast.encode())),
(2u8, expired_hash.to_vec(), Some((expired_hash, expired_ts).encode())),
])
.unwrap();
}
let open = |path: &std::path::Path| {
Store::new::<Block, TestClient, TestBackend>(
path,
Default::default(),
std::sync::Arc::new(TestClient),
std::sync::Arc::new(sc_keystore::LocalKeystore::in_memory()),
None,
Box::new(sp_core::testing::TaskExecutor::new()),
)
.unwrap()
};
let store = open(&path);
assert_eq!(store.statements().unwrap().len(), 2);
assert!(store.statement(&h_addressed).unwrap().is_some());
assert!(store.statement(&h_broadcast).unwrap().is_some());
assert!(store.index_has_topic(&topic(1), &h_addressed));
assert!(store.index_has_topic(&topic(1), &h_broadcast));
assert!(store.index_has_topic(&topic(2), &h_addressed));
assert!(store.index_has_dec_key(&Some(dec_key(9)), &h_addressed));
assert!(store.index_has_dec_key(&None, &h_broadcast));
let mut admitted = std::collections::HashSet::new();
let mut iter = store.db.iter(col::ADMISSION_SEQ).unwrap();
iter.seek_to_first().unwrap();
while let Some((_key, value)) = iter.next().unwrap() {
admitted.insert(value.as_slice().try_into().expect("admission hash is 32 bytes"));
}
assert_eq!(admitted, std::collections::HashSet::from([h_addressed, h_broadcast]));
assert_eq!(store.submit_index.read().next_seq, 2);
assert_eq!(store.broadcasts(&[topic(1)]).unwrap().len(), 1);
assert_eq!(store.posted(&[topic(1)], dec_key(9)).unwrap().len(), 1);
assert!(store.is_evicted(&expired_hash));
assert_eq!(store.evicted_count(), 1);
assert_eq!(store.statement_count(), 2);
assert_eq!(store.total_size(), 2);
assert!(store.has_account(&addressed.account_id().unwrap()));
assert_eq!(store.account_count(), 1);
drop(store);
let store = open(&path);
assert_eq!(store.statements().unwrap().len(), 2);
assert!(store.index_has_topic(&topic(1), &h_broadcast));
assert!(store.is_evicted(&expired_hash));
}
#[test]
fn interrupted_migration_resumes_preserving_admission_entries() {
sp_tracing::init_for_tests();
let temp = tempfile::Builder::new().tempdir().expect("Error creating test dir");
let mut path: std::path::PathBuf = temp.path().into();
path.push("db");
let mut db_path = path.clone();
db_path.push("statements");
let s1 = signed_statement_with_topics(1, &[topic(1)], None);
let s2 = statement(2, 5, Some(9), 200);
{
let mut cfg = parity_db::Options::with_columns(&db_path, col::COUNT);
let statement_col = &mut cfg.columns[col::STATEMENTS as usize];
statement_col.ref_counted = false;
statement_col.preimage = true;
statement_col.uniform = true;
for c in crate::BTREE_COLUMNS {
cfg.columns[c as usize].btree_index = true;
}
let db = parity_db::Db::open_or_create(&cfg).unwrap();
let mut commit: Vec<(u8, Vec<u8>, Option<Vec<u8>>)> = vec![
(col::META, b"version".to_vec(), Some(1u32.to_le_bytes().to_vec())),
(col::STATEMENTS, s1.hash().to_vec(), Some(s1.encode())),
(col::STATEMENTS, s2.hash().to_vec(), Some(s2.encode())),
(col::ADMISSION_SEQ, 7u64.to_be_bytes().to_vec(), Some(s1.hash().to_vec())),
];
commit.extend(crate::statement_index_ops(&s1.hash(), &s1, true));
db.commit(commit).unwrap();
}
let store = Store::new::<Block, TestClient, TestBackend>(
&path,
Default::default(),
std::sync::Arc::new(TestClient),
std::sync::Arc::new(sc_keystore::LocalKeystore::in_memory()),
None,
Box::new(sp_core::testing::TaskExecutor::new()),
)
.unwrap();
assert_eq!(store.statements().unwrap().len(), 2);
assert_eq!(store.statement_count(), 2);
assert_eq!(store.total_size(), 1 + 200);
assert!(store.has_account(&s1.account_id().unwrap()));
assert!(store.has_account(&account(2)));
assert!(store
.db
.get(
col::INDEX_BY_EXPIRY,
&crate::expiry_index_key(crate::Expiry(s2.expiry()), &s2.hash())
)
.unwrap()
.is_some());
assert_eq!(
store.db.get(col::ADMISSION_SEQ, &7u64.to_be_bytes()).unwrap(),
Some(s1.hash().to_vec())
);
assert_eq!(
store.db.get(col::ADMISSION_SEQ, &8u64.to_be_bytes()).unwrap(),
Some(s2.hash().to_vec())
);
assert_eq!(store.submit_index.read().next_seq, 9);
let replay = store.replay_batch(&OptimizedTopicFilter::Any, 7, 9).unwrap();
assert_eq!(replay.statements, vec![s1.encode(), s2.encode()]);
assert!(matches!(
store.submit(statement(2, 4, Some(9), 100), StatementSource::Network),
SubmitResult::Rejected(RejectionReason::ChannelPriorityTooLow { .. })
));
}
#[test]
fn counters_and_seq_survive_restart() {
let (store, temp) = test_store();
for i in 0..3u8 {
assert_eq!(
store.submit(signed_statement(i), StatementSource::Network),
SubmitResult::New
);
}
assert_eq!(
store.submit(statement(1, 1, None, 100), StatementSource::Network),
SubmitResult::New
);
assert_eq!(store.statement_count(), 4);
assert_eq!(store.total_size(), 3 + 100);
let keystore = store.keystore.clone();
drop(store);
let mut path: std::path::PathBuf = temp.path().into();
path.push("db");
let store = Store::new::<Block, TestClient, TestBackend>(
&path,
Default::default(),
std::sync::Arc::new(TestClient),
keystore,
None,
Box::new(sp_core::testing::TaskExecutor::new()),
)
.unwrap();
assert_eq!(store.statement_count(), 4);
assert_eq!(store.total_size(), 3 + 100);
assert_eq!(store.submit_index.read().next_seq, 4);
assert_eq!(store.account_count(), 2);
}
#[test]
fn resubmitting_live_statement_is_known_and_not_double_counted() {
let (store, _temp) = test_store();
let statement = statement(1, 1, None, 100);
assert_eq!(store.submit(statement.clone(), StatementSource::Local), SubmitResult::New);
assert_eq!(store.total_size(), 100);
assert_eq!(store.submit(statement.clone(), StatementSource::Local), SubmitResult::Known);
assert_eq!(store.submit(statement, StatementSource::Network), SubmitResult::Known);
assert_eq!(store.total_size(), 100);
assert_eq!(store.statement_count(), 1);
}
#[test]
fn eviction_clears_all_index_rows() {
let (mut store, _temp) = test_store();
store.set_time(0);
let mut stmt = unsigned_statement(1, 1, Some(7), 100);
stmt.set_topic(0, topic(3));
stmt.set_expiry_from_parts(500, 1);
sign_with(&mut stmt, 1);
let hash = stmt.hash();
let expiry = crate::Expiry(stmt.expiry());
let who = account(1);
assert_eq!(store.submit(stmt, StatementSource::Network), SubmitResult::New);
let account_key = crate::account_index_key(&who, expiry, &hash);
let expiry_key = crate::expiry_index_key(expiry, &hash);
assert!(store.db.get(col::INDEX_BY_ACCOUNT, &account_key).unwrap().is_some());
assert!(store.db.get(col::INDEX_BY_EXPIRY, &expiry_key).unwrap().is_some());
store.remove(&hash).unwrap();
assert!(store.db.get(col::INDEX_BY_ACCOUNT, &account_key).unwrap().is_none());
assert!(store.db.get(col::INDEX_BY_EXPIRY, &expiry_key).unwrap().is_none());
assert!(!store.index_has_topic(&topic(3), &hash));
assert_eq!(store.db.get(col::ADMISSION_SEQ, &0u64.to_be_bytes()).unwrap(), None);
assert!(store.is_evicted(&hash));
assert_eq!(store.statement_count(), 0);
assert_eq!(store.total_size(), 0);
}
#[test]
fn index_key_layouts_roundtrip() {
use crate::{
account_index_key, dec_key_index_key, dec_key_index_prefix, evicted_index_key,
expiry_index_key, hash_from_index_key, parse_account_index_key,
parse_dec_key_index_prefix, parse_time_index_key, prefix_from_index_key,
topic_index_key, Expiry,
};
let who = account(7);
let hash: crate::Hash = account(8);
let expiry = Expiry((123u64 << 32) | 45);
let key = account_index_key(&who, expiry, &hash);
assert_eq!(parse_account_index_key(&key), Some((who, expiry, hash)));
assert_eq!(parse_account_index_key(&key[..key.len() - 1]), None);
assert_eq!(parse_account_index_key(&[]), None);
let key = expiry_index_key(expiry, &hash);
assert_eq!(parse_time_index_key(&key), Some((expiry.0, hash)));
assert_eq!(parse_time_index_key(&key[1..]), None);
let key = evicted_index_key(789, &hash);
assert_eq!(parse_time_index_key(&key), Some((789, hash)));
assert!(expiry_index_key(Expiry(256), &hash) > expiry_index_key(Expiry(1), &hash));
assert!(evicted_index_key(256, &hash) > evicted_index_key(1, &hash));
let key = topic_index_key(&topic(3), &hash);
assert_eq!(hash_from_index_key(&key), Some(hash));
assert_eq!(prefix_from_index_key(&key), Some(&topic(3)[..]));
for dec_key in [None, Some(dec_key(5))] {
assert_eq!(parse_dec_key_index_prefix(&dec_key_index_prefix(&dec_key)), Some(dec_key));
let key = dec_key_index_key(&dec_key, &hash);
assert_eq!(hash_from_index_key(&key), Some(hash));
assert_eq!(
prefix_from_index_key(&key).and_then(parse_dec_key_index_prefix),
Some(dec_key)
);
}
}
#[test]
fn summary_fast_path_stays_coherent_with_disk() {
use crate::{DETAILS_CACHE_BUDGET, SUMMARY_CACHE_ACCOUNTS};
let (store, _temp) = test_store();
let source = StatementSource::Network;
assert!(SUMMARY_CACHE_ACCOUNTS >= 3, "three account summaries must fit the cache");
let per_filler_account = DETAILS_CACHE_BUDGET.div_ceil(2);
assert!(
per_filler_account <= 100,
"the filler accounts' allowance (100 statements, 1000 bytes of data) caps the cache \
budget this test can exercise",
);
let s5_1 = statement(5, 1, None, 100);
assert_eq!(store.submit(s5_1.clone(), source), SubmitResult::New);
assert!(store.details_cached(&account(5)));
for acc in [6u64, 7] {
for c in 1..=per_filler_account as u64 {
assert_eq!(store.submit(statement(acc, 1, Some(c), 10), source), SubmitResult::New);
}
}
assert!(!store.details_cached(&account(5)));
let s5_2 = statement(5, 2, None, 50);
assert_eq!(store.submit(s5_2.clone(), source), SubmitResult::New);
assert!(!store.details_cached(&account(5)));
let s5_3 = statement(5, 3, None, 900);
assert_eq!(store.submit(s5_3.clone(), source), SubmitResult::New);
assert!(store.details_cached(&account(5)));
assert!(!store.has_statement(&s5_1.hash()), "lowest priority must be evicted");
assert!(store.has_statement(&s5_2.hash()));
assert!(store.has_statement(&s5_3.hash()));
let filler_statements = 2 * per_filler_account;
assert_eq!(store.statement_count(), 2 + filler_statements);
assert_eq!(store.total_size(), 50 + 900 + filler_statements * 10);
}
#[test]
fn rejected_submission_caches_the_loaded_record() {
let (store, temp) = test_store();
let source = StatementSource::Network;
assert_eq!(store.submit(statement(5, 2, Some(1), 100), source), SubmitResult::New);
let keystore = store.keystore.clone();
drop(store);
let mut path: std::path::PathBuf = temp.path().into();
path.push("db");
let store = Store::new::<Block, TestClient, TestBackend>(
&path,
Default::default(),
std::sync::Arc::new(TestClient),
keystore,
None,
Box::new(sp_core::testing::TaskExecutor::new()),
)
.unwrap();
assert_eq!(
store.submit(statement(5, 3, None, 1500), source),
SubmitResult::Rejected(RejectionReason::DataTooLarge {
submitted_size: 1500,
available_size: 1000,
})
);
assert!(!store.details_cached(&account(5)));
assert!(!store.summary_cached(&account(5)));
assert!(matches!(
store.submit(statement(5, 1, Some(1), 100), source),
SubmitResult::Rejected(RejectionReason::ChannelPriorityTooLow { .. })
));
assert!(store.details_cached(&account(5)));
assert!(store.summary_cached(&account(5)));
}
#[test]
fn take_recent_statements_clears_index() {
let (store, _temp) = test_store();
let statement0 = signed_statement(0);
let statement1 = signed_statement(1);
let statement2 = signed_statement(2);
let statement3 = signed_statement(3);
let _ = store.submit(statement0.clone(), StatementSource::Local);
let _ = store.submit(statement1.clone(), StatementSource::Local);
let _ = store.submit(statement2.clone(), StatementSource::Local);
let recent1 = store.take_recent_statements().unwrap();
let (recent1_hashes, recent1_statements): (Vec<_>, Vec<_>) =
recent1.into_iter().map(|(_seq, hash, statement)| (hash, statement)).unzip();
let expected1 = vec![statement0, statement1, statement2];
assert!(expected1.iter().all(|s| recent1_hashes.contains(&s.hash())));
assert!(expected1.iter().all(|s| recent1_statements.contains(s)));
let recent2 = store.take_recent_statements().unwrap();
assert_eq!(recent2.len(), 0);
store.submit(statement3.clone(), StatementSource::Network);
let recent3 = store.take_recent_statements().unwrap();
let (recent3_hashes, recent3_statements): (Vec<_>, Vec<_>) =
recent3.into_iter().map(|(_seq, hash, statement)| (hash, statement)).unzip();
let expected3 = vec![statement3];
assert!(expected3.iter().all(|s| recent3_hashes.contains(&s.hash())));
assert!(expected3.iter().all(|s| recent3_statements.contains(s)));
assert_eq!(store.statements().unwrap().len(), 4);
}
#[test]
fn search_by_topic_and_key() {
let (store, _temp) = test_store();
let statement0 = signed_statement(0);
let statement1 = signed_statement_with_topics(1, &[topic(0)], None);
let statement2 = signed_statement_with_topics(2, &[topic(0), topic(1)], Some(dec_key(2)));
let statement3 = signed_statement_with_topics(3, &[topic(0), topic(1), topic(2)], None);
let statement4 =
signed_statement_with_topics(4, &[topic(0), topic(42), topic(2), topic(3)], None);
let statements = vec![statement0, statement1, statement2, statement3, statement4];
for s in &statements {
store.submit(s.clone(), StatementSource::Network);
}
let assert_topics = |topics: &[u64], key: Option<u64>, expected: &[u8]| {
let key = key.map(dec_key);
let topics: Vec<_> = topics.iter().map(|t| topic(*t)).collect();
let mut got_vals: Vec<_> = if let Some(key) = key {
store.posted(&topics, key).unwrap().into_iter().map(|d| d[0]).collect()
} else {
store.broadcasts(&topics).unwrap().into_iter().map(|d| d[0]).collect()
};
got_vals.sort();
assert_eq!(expected.to_vec(), got_vals);
};
assert_topics(&[], None, &[0, 1, 3, 4]);
assert_topics(&[], Some(2), &[2]);
assert_topics(&[0], None, &[1, 3, 4]);
assert_topics(&[1], None, &[3]);
assert_topics(&[2], None, &[3, 4]);
assert_topics(&[3], None, &[4]);
assert_topics(&[42], None, &[4]);
assert_topics(&[0, 1], None, &[3]);
assert_topics(&[0, 1], Some(2), &[2]);
assert_topics(&[0, 1, 99], Some(2), &[]);
assert_topics(&[1, 2], None, &[3]);
assert_topics(&[99], None, &[]);
assert_topics(&[0, 99], None, &[]);
assert_topics(&[0, 1, 2, 3, 42], None, &[]);
}
#[test]
fn constraints() {
let (store, _temp) = test_store();
store.submit_index.write().config.max_total_size = 3000;
let source = StatementSource::Network;
let ok = SubmitResult::New;
assert!(matches!(
store.submit(statement(1, 1, Some(1), 2000), source),
SubmitResult::Rejected(_)
));
assert_eq!(store.submit(statement(1, 1, Some(1), 500), source), ok);
assert!(matches!(
store.submit(statement(1, 1, Some(1), 200), source),
SubmitResult::Rejected(_)
));
assert_eq!(store.submit(statement(1, 2, Some(1), 600), source), ok);
assert!(matches!(
store.submit(statement(1, 1, Some(2), 100), source),
SubmitResult::Rejected(_)
));
assert_eq!(store.evicted_count(), 1);
let s2_prio1 = statement(2, 1, None, 500);
let s2_prio2 = statement(2, 2, None, 100);
assert_eq!(store.submit(s2_prio1.clone(), source), ok);
assert_eq!(store.submit(s2_prio2.clone(), source), ok);
assert!(matches!(
store.submit(statement(2, 1, None, 50), source),
SubmitResult::Rejected(RejectionReason::AccountFull { .. })
));
let s2_prio3 = statement(2, 3, None, 500);
assert_eq!(store.submit(s2_prio3.clone(), source), ok);
assert_eq!(store.evicted_count(), 2);
assert!(store.is_evicted(&s2_prio1.hash()));
assert!(store.statement(&s2_prio1.hash()).unwrap().is_none());
assert_eq!(store.submit(statement(2, 4, None, 1000), source), ok);
assert_eq!(store.evicted_count(), 4);
assert!(store.is_evicted(&s2_prio2.hash()));
assert!(store.is_evicted(&s2_prio3.hash()));
let s3_prio2 = statement(3, 2, Some(1), 300);
let s3_prio3 = statement(3, 3, Some(2), 300);
assert_eq!(store.submit(s3_prio2.clone(), source), ok);
assert_eq!(store.submit(s3_prio3.clone(), source), ok);
assert_eq!(store.submit(statement(3, 4, Some(3), 300), source), ok);
assert_eq!(store.submit(statement(3, 5, None, 500), source), ok);
assert_eq!(store.evicted_count(), 6);
assert!(store.is_evicted(&s3_prio2.hash()));
assert!(store.is_evicted(&s3_prio3.hash()));
assert_eq!(store.total_size(), 2400);
assert_eq!(store.statement_count(), 4);
assert!(matches!(
store.submit(statement(1, 1, None, 700), source),
SubmitResult::Rejected(_)
));
store.submit_index.write().config.max_total_statements = 4;
assert!(matches!(
store.submit(statement(1, 1, None, 100), source),
SubmitResult::Rejected(_)
));
let mut expected_statements = vec![
statement(1, 2, Some(1), 600).hash(),
statement(2, 4, None, 1000).hash(),
statement(3, 4, Some(3), 300).hash(),
statement(3, 5, None, 500).hash(),
];
expected_statements.sort();
let mut statements: Vec<_> =
store.statements().unwrap().into_iter().map(|(hash, _)| hash).collect();
statements.sort();
assert_eq!(expected_statements, statements);
}
#[test]
fn insufficient_remaining_account_bytes_is_data_too_large() {
let (store, _temp) = test_store();
let source = StatementSource::Network;
assert_eq!(store.submit(statement(4, 2, None, 900), source), SubmitResult::New);
assert_eq!(
store.submit(statement(4, 1, None, 200), source),
SubmitResult::Rejected(RejectionReason::DataTooLarge {
submitted_size: 200,
available_size: 100,
})
);
}
#[test]
fn max_statement_size_for_gossiping() {
let (store, _temp) = test_store();
store.submit_index.write().config.max_total_size = 42 * crate::MAX_STATEMENT_SIZE;
assert_eq!(
store.submit(
statement(42, 1, Some(1), crate::MAX_STATEMENT_SIZE - 500),
StatementSource::Local
),
SubmitResult::New
);
assert!(matches!(
store.submit(
statement(42, 2, Some(1), 2 * crate::MAX_STATEMENT_SIZE),
StatementSource::Local
),
SubmitResult::Invalid(_)
));
}
#[test]
fn expired_statements_are_purged() {
use super::DEFAULT_PURGE_AFTER_SEC;
let (mut store, temp) = test_store();
let mut statement = unsigned_statement(1, 1, Some(3), 100);
store.set_time(0);
statement.set_topic(0, topic(4));
sign_with(&mut statement, 1);
store.submit(statement.clone(), StatementSource::Network);
assert_eq!(store.statement_count(), 1);
store.remove(&statement.hash()).unwrap();
assert_eq!(store.statement_count(), 0);
assert_eq!(store.account_count(), 0);
store.set_time(DEFAULT_PURGE_AFTER_SEC + 1);
store.maintain();
assert_eq!(store.evicted_count(), 0);
let keystore = store.keystore.clone();
drop(store);
let client = std::sync::Arc::new(TestClient);
let mut path: std::path::PathBuf = temp.path().into();
path.push("db");
let store = Store::new::<Block, TestClient, TestBackend>(
&path,
Default::default(),
client,
keystore,
None,
Box::new(sp_core::testing::TaskExecutor::new()),
)
.unwrap();
assert_eq!(store.statements().unwrap().len(), 0);
assert_eq!(store.evicted_count(), 0);
}
#[test]
fn posted_clear_decrypts() {
let (store, _temp) = test_store();
let public = store
.keystore
.ed25519_generate_new(sp_core::crypto::key_types::STATEMENT, None)
.unwrap();
let statement1 = statement(1, 1, None, 100);
let mut statement2 = unsigned_statement(1, 2, None, 0);
let plain = b"The most valuable secret".to_vec();
statement2.encrypt(&plain, &public).unwrap();
sign_with(&mut statement2, 1);
store.submit(statement1, StatementSource::Network);
store.submit(statement2, StatementSource::Network);
let posted_clear = store.posted_clear(&[], public.into()).unwrap();
assert_eq!(posted_clear, vec![plain]);
}
#[test]
fn broadcasts_stmt_returns_encoded_statements() {
let (store, _tmp) = test_store();
let s0 = signed_statement_with_topics(0, &[], None);
let s1 = signed_statement_with_topics(1, &[topic(42)], None);
let s2 = signed_statement_with_topics(2, &[topic(42)], Some(dec_key(99)));
for s in [&s0, &s1, &s2] {
store.submit(s.clone(), StatementSource::Network);
}
let mut hashes: Vec<_> = store
.broadcasts_stmt(&[])
.unwrap()
.into_iter()
.map(|bytes| Statement::decode(&mut &bytes[..]).unwrap().hash())
.collect();
hashes.sort();
let expected_hashes = {
let mut e = vec![s0.hash(), s1.hash()];
e.sort();
e
};
assert_eq!(hashes, expected_hashes);
let got = store.broadcasts_stmt(&[topic(42)]).unwrap();
assert_eq!(got.len(), 1);
let st = Statement::decode(&mut &got[0][..]).unwrap();
assert_eq!(st.hash(), s1.hash());
}
#[test]
fn posted_stmt_returns_encoded_statements_for_dest() {
let (store, _tmp) = test_store();
let public1 = store
.keystore
.ed25519_generate_new(sp_core::crypto::key_types::STATEMENT, None)
.unwrap();
let dest: [u8; 32] = public1.into();
let public2 = store
.keystore
.ed25519_generate_new(sp_core::crypto::key_types::STATEMENT, None)
.unwrap();
let mut s_with_key = unsigned_statement(1, 1, None, 0);
let plain1 = b"The most valuable secret".to_vec();
s_with_key.encrypt(&plain1, &public1).unwrap();
sign_with(&mut s_with_key, 1);
let mut s_other_key = unsigned_statement(2, 2, None, 0);
let plain2 = b"The second most valuable secret".to_vec();
s_other_key.encrypt(&plain2, &public2).unwrap();
sign_with(&mut s_other_key, 2);
for s in [&s_with_key, &s_other_key] {
store.submit(s.clone(), StatementSource::Network);
}
let retrieved = store.posted_stmt(&[], dest).unwrap();
assert_eq!(retrieved.len(), 1, "Only one statement has dec_key=dest");
let returned_stmt = Statement::decode(&mut &retrieved[0][..]).unwrap();
assert_eq!(
returned_stmt.hash(),
s_with_key.hash(),
"Returned statement must match s_with_key"
);
}
#[test]
fn posted_clear_stmt_returns_statement_followed_by_plain_data() {
let (store, _tmp) = test_store();
let public1 = store
.keystore
.ed25519_generate_new(sp_core::crypto::key_types::STATEMENT, None)
.unwrap();
let dest: [u8; 32] = public1.into();
let public2 = store
.keystore
.ed25519_generate_new(sp_core::crypto::key_types::STATEMENT, None)
.unwrap();
let mut s_with_key = unsigned_statement(1, 1, None, 0);
let plain1 = b"The most valuable secret".to_vec();
s_with_key.encrypt(&plain1, &public1).unwrap();
sign_with(&mut s_with_key, 1);
let mut s_other_key = unsigned_statement(2, 2, None, 0);
let plain2 = b"The second most valuable secret".to_vec();
s_other_key.encrypt(&plain2, &public2).unwrap();
sign_with(&mut s_other_key, 2);
for s in [&s_with_key, &s_other_key] {
store.submit(s.clone(), StatementSource::Network);
}
let retrieved = store.posted_clear_stmt(&[], dest).unwrap();
assert_eq!(retrieved.len(), 1, "Only one statement has dec_key=dest");
let encoded_stmt = s_with_key.encode();
let stmt_len = encoded_stmt.len();
assert_eq!(&retrieved[0][..stmt_len], &encoded_stmt[..]);
let trailing = &retrieved[0][stmt_len..];
assert_eq!(trailing, &plain1[..]);
}
#[test]
fn posted_clear_returns_plain_data_for_dest_and_topics() {
let (store, _tmp) = test_store();
let public_dest = store
.keystore
.ed25519_generate_new(sp_core::crypto::key_types::STATEMENT, None)
.unwrap();
let dest: [u8; 32] = public_dest.into();
let public_other = store
.keystore
.ed25519_generate_new(sp_core::crypto::key_types::STATEMENT, None)
.unwrap();
let mut s_good = unsigned_statement(1, 1, None, 0);
let plaintext_good = b"The most valuable secret".to_vec();
s_good.encrypt(&plaintext_good, &public_dest).unwrap();
s_good.set_topic(0, topic(42));
sign_with(&mut s_good, 1);
let mut s_wrong_topic = unsigned_statement(2, 2, None, 0);
s_wrong_topic.encrypt(b"Wrong topic", &public_dest).unwrap();
s_wrong_topic.set_topic(0, topic(99));
sign_with(&mut s_wrong_topic, 2);
let mut s_other_dest = unsigned_statement(3, 3, None, 0);
s_other_dest.encrypt(b"Other dest", &public_other).unwrap();
s_other_dest.set_topic(0, topic(42));
sign_with(&mut s_other_dest, 3);
for s in [&s_good, &s_wrong_topic, &s_other_dest] {
store.submit(s.clone(), StatementSource::Network);
}
let retrieved = store.posted_clear(&[topic(42)], dest).unwrap();
assert_eq!(retrieved, vec![plaintext_good]);
}
#[test]
fn already_expired_statement_is_rejected() {
let (mut store, _temp) = test_store();
store.set_time(1000);
let mut expired_statement = unsigned_statement(1, 1, None, 100);
expired_statement.set_expiry_from_parts(500, 1);
sign_with(&mut expired_statement, 1);
assert_eq!(
store.submit(expired_statement, StatementSource::Network),
SubmitResult::Invalid(InvalidReason::AlreadyExpired)
);
assert_eq!(store.statements().unwrap().len(), 0);
let mut valid_statement = unsigned_statement(1, 1, None, 100);
valid_statement.set_expiry_from_parts(2000, 1);
sign_with(&mut valid_statement, 1);
assert_eq!(store.submit(valid_statement, StatementSource::Network), SubmitResult::New);
assert_eq!(store.statements().unwrap().len(), 1);
}
#[test]
fn remove_by_covers_various_situations() {
use sp_statement_store::{StatementSource, StatementStore, SubmitResult};
let (mut store, _temp) = test_store();
store.set_time(0);
let t42 = topic(42);
let k7 = dec_key(7);
let mut s_a1 = unsigned_statement(4, 10, Some(100), 100);
s_a1.set_topic(0, t42);
sign_with(&mut s_a1, 4);
let h_a1 = s_a1.hash();
let mut s_a2 = unsigned_statement(4, 20, Some(200), 150);
s_a2.set_decryption_key(k7);
sign_with(&mut s_a2, 4);
let h_a2 = s_a2.hash();
let s_a3 = statement(4, 30, None, 50);
let h_a3 = s_a3.hash();
let s_b1 = statement(3, 10, None, 100);
let h_b1 = s_b1.hash();
let mut s_b2 = unsigned_statement(3, 15, Some(300), 100);
s_b2.set_topic(0, t42);
s_b2.set_decryption_key(k7);
sign_with(&mut s_b2, 3);
let h_b2 = s_b2.hash();
for s in [&s_a1, &s_a2, &s_a3, &s_b1, &s_b2] {
assert_eq!(store.submit(s.clone(), StatementSource::Network), SubmitResult::New);
}
{
assert_eq!(store.statement_count(), 5, "all 5 should be present");
assert!(store.has_account(&account(4)));
assert!(store.has_account(&account(3)));
assert_eq!(store.total_size(), 100 + 150 + 50 + 100 + 100);
assert!(store.index_has_topic(&t42, &h_a1) && store.index_has_topic(&t42, &h_b2));
assert!(
store.index_has_dec_key(&Some(k7), &h_a2) &&
store.index_has_dec_key(&Some(k7), &h_b2)
);
}
store.remove_by(account(4)).expect("remove_by should succeed");
{
for h in [h_a1, h_a2, h_a3] {
assert!(store.statement(&h).unwrap().is_none(), "A's statement should be removed");
}
for h in [h_b1, h_b2] {
assert!(store.statement(&h).unwrap().is_some(), "B's statement should remain");
}
assert!(!store.has_account(&account(4)), "Account A must be gone");
assert!(store.has_account(&account(3)), "Account B must remain");
assert_eq!(store.statement_count(), 2);
assert_eq!(store.total_size(), 100 + 100);
assert!(store.is_evicted(&h_a1));
assert!(store.is_evicted(&h_a2));
assert!(store.is_evicted(&h_a3));
assert_eq!(store.evicted_count(), 3);
assert!(store.index_has_topic(&t42, &h_b2));
assert!(!store.index_has_topic(&t42, &h_a1));
assert!(store.index_has_dec_key(&Some(k7), &h_b2));
assert!(!store.index_has_dec_key(&Some(k7), &h_a2));
}
store.remove_by(account(4)).expect("second remove_by should be a no-op");
let purge_after = store.submit_index.read().config.purge_after_sec;
store.set_time(purge_after + 1);
store.maintain();
assert_eq!(store.evicted_count(), 0, "expired entries should be purged");
let s_new = statement(4, 40, None, 10);
assert_eq!(store.submit(s_new, StatementSource::Network), SubmitResult::New);
}
#[test]
fn check_expiration_expires_statements_past_current_time() {
let (mut store, _temp) = test_store();
store.set_time(100);
let mut expired_stmt = unsigned_statement(1, 1, None, 100);
expired_stmt.set_expiry_from_parts(500, 1);
sign_with(&mut expired_stmt, 1);
let expired_hash = expired_stmt.hash();
store.submit(expired_stmt, StatementSource::Network);
let valid_stmt = statement(2, 1, None, 100); let valid_hash = valid_stmt.hash();
store.submit(valid_stmt, StatementSource::Network);
assert_eq!(store.statement_count(), 2);
store.set_time(1000);
store.enforce_limits();
assert!(
!store.is_evicted(&expired_hash),
"Naturally expired statement must not be added to the expired map"
);
assert!(
!store.has_statement(&expired_hash),
"Expired statement should be removed from the store"
);
assert!(store.has_statement(&valid_hash), "Valid statement should still be stored");
assert!(!store.is_evicted(&valid_hash), "Valid statement should not be expired");
}
#[test]
fn allowance_sweep_resumes_from_cursor_across_calls() {
use std::time::Duration;
let (mut store, _temp) = test_store();
store.set_time(0);
for seed in 10u64..15 {
store.force_insert(&statement(seed, 1, None, 600));
store.force_insert(&statement(seed, 2, None, 600));
}
assert_eq!(store.statement_count(), 10);
assert!(store.submit_index.read().allowance_cursor.is_none());
store.enforce_limits_bounded(usize::MAX, 2, Duration::from_secs(3600));
assert_eq!(store.statement_count(), 8);
assert!(store.submit_index.read().allowance_cursor.is_some());
store.enforce_limits_bounded(usize::MAX, 2, Duration::from_secs(3600));
assert_eq!(store.statement_count(), 6);
assert!(store.submit_index.read().allowance_cursor.is_some());
store.enforce_limits_bounded(usize::MAX, 2, Duration::from_secs(3600));
assert_eq!(store.statement_count(), 5);
assert!(store.submit_index.read().allowance_cursor.is_none());
assert_eq!(store.known_accounts_count.load(std::sync::atomic::Ordering::Relaxed), 5);
store.enforce_limits();
assert_eq!(store.statement_count(), 5);
}
#[test]
fn allowance_eviction_budget_defers_the_residue_to_the_next_pass() {
use std::time::Duration;
let (mut store, _temp) = test_store();
store.set_time(0);
for priority in 1..=4 {
store.force_insert(&statement(1, priority, None, 100));
}
assert_eq!(store.statement_count(), 4);
store.enforce_limits_bounded(2, usize::MAX, Duration::from_secs(3600));
assert_eq!(store.statement_count(), 2);
store.enforce_limits_bounded(2, usize::MAX, Duration::from_secs(3600));
assert_eq!(store.statement_count(), 2);
assert!(store.submit_index.read().allowance_cursor.is_none());
store.enforce_limits_bounded(2, usize::MAX, Duration::from_secs(3600));
assert_eq!(store.statement_count(), 1);
}
#[test]
fn expiry_sweep_is_bounded_by_the_statement_budget() {
use std::time::Duration;
let (mut store, _temp) = test_store();
store.set_time(100);
for seed in 1u64..=5 {
let mut stmt = unsigned_statement(seed, 1, None, 100);
stmt.set_expiry_from_parts(200, 1);
sign_with(&mut stmt, seed);
assert_eq!(store.submit(stmt, StatementSource::Network), SubmitResult::New);
}
store.set_time(300);
store.enforce_limits_bounded(2, usize::MAX, Duration::from_secs(3600));
assert_eq!(store.statement_count(), 3);
store.enforce_limits_bounded(2, usize::MAX, Duration::from_secs(3600));
assert_eq!(store.statement_count(), 1);
store.enforce_limits_bounded(2, usize::MAX, Duration::from_secs(3600));
assert_eq!(store.statement_count(), 0);
assert_eq!(store.evicted_count(), 0);
}
#[test]
fn expiry_sweep_leaves_inconsistent_data_in_place() {
let (mut store, _temp) = test_store();
store.set_time(100);
let expiry = crate::Expiry(200u64 << 32 | 1);
let undecodable_hash = [0xAB_u8; 32];
let undecodable_key = crate::expiry_index_key(expiry, &undecodable_hash);
store
.db
.commit([
(col::STATEMENTS, undecodable_hash.to_vec(), Some(vec![0xFF])),
(col::INDEX_BY_EXPIRY, undecodable_key.clone(), Some(Vec::new())),
])
.unwrap();
store.set_time(300);
store.enforce_limits();
store.enforce_limits();
assert!(store.has_statement(&undecodable_hash));
assert!(store.db.get(col::INDEX_BY_EXPIRY, &undecodable_key).unwrap().is_some());
let orphan_hash = [0xCD_u8; 32];
let orphan_key = crate::expiry_index_key(expiry, &orphan_hash);
store
.db
.commit([(col::INDEX_BY_EXPIRY, orphan_key.clone(), Some(Vec::new()))])
.unwrap();
store.enforce_limits();
assert!(!store.has_statement(&orphan_hash));
assert!(store.db.get(col::INDEX_BY_EXPIRY, &orphan_key).unwrap().is_some());
}
#[test]
fn corrupt_account_index_key_does_not_end_account_enumeration() {
let (store, _temp) = test_store();
assert_eq!(
store.submit(statement(1, 1, None, 1), StatementSource::Network),
SubmitResult::New
);
assert_eq!(
store.submit(statement(2, 1, None, 1), StatementSource::Network),
SubmitResult::New
);
assert_eq!(store.account_count(), 2);
let (first, second) = {
let (a, b) = (account(1), account(2));
if a < b {
(a, b)
} else {
(b, a)
}
};
let mut between = first.to_vec();
between.extend_from_slice(&[0xFF; 41]);
store
.db
.commit([
(col::INDEX_BY_ACCOUNT, vec![0u8; 33], Some(Vec::new())),
(col::INDEX_BY_ACCOUNT, between, Some(Vec::new())),
])
.unwrap();
assert_eq!(store.account_count(), 2);
assert!(store.has_account(&first));
assert!(store.has_account(&second));
store.enforce_limits();
assert_eq!(store.known_accounts_count.load(std::sync::atomic::Ordering::Relaxed), 2);
}
#[test]
fn concurrent_remove_and_resubmit_keep_query_bookkeeping_consistent() {
use std::sync::Arc;
let (store, _temp) = test_store();
let store = Arc::new(store);
let mut stmt = unsigned_statement(1, 1, None, 100);
stmt.set_topic(0, topic(7));
sign_with(&mut stmt, 1);
let hash = stmt.hash();
for _ in 0..200 {
if !store.has_statement(&hash) {
assert_eq!(store.submit(stmt.clone(), StatementSource::Local), SubmitResult::New);
}
let remover = {
let store = store.clone();
std::thread::spawn(move || store.remove(&hash).expect("remove succeeds"))
};
let resubmitter = {
let store = store.clone();
let stmt = stmt.clone();
std::thread::spawn(move || store.submit(stmt, StatementSource::Local))
};
remover.join().expect("remover joins");
let _ = resubmitter.join().expect("resubmitter joins");
let query_index = store.query_index.read();
let present = store.has_statement(&hash);
assert_eq!(query_index.recent.contains_key(&hash), present);
assert_eq!(
query_index.topic_counts.get(&topic(7)).copied().unwrap_or(0),
present as usize
);
}
}
#[test]
fn check_expiration_handles_multiple_statements_per_account() {
let (mut store, _temp) = test_store();
store.set_time(100);
let mut stmt1 = unsigned_statement(42, 1, Some(1), 100);
stmt1.set_expiry_from_parts(200, 1); sign_with(&mut stmt1, 42);
let hash1 = stmt1.hash();
store.submit(stmt1, StatementSource::Network);
let mut stmt2 = unsigned_statement(42, 2, Some(2), 100);
stmt2.set_expiry_from_parts(300, 2); sign_with(&mut stmt2, 42);
let hash2 = stmt2.hash();
store.submit(stmt2, StatementSource::Network);
let mut stmt3 = unsigned_statement(42, 3, Some(3), 100);
stmt3.set_expiry_from_parts(500, 3); sign_with(&mut stmt3, 42);
let hash3 = stmt3.hash();
store.submit(stmt3, StatementSource::Network);
assert_eq!(store.statement_count(), 3);
store.set_time(250);
store.enforce_limits();
{
assert!(!store.is_evicted(&hash1), "stmt1 naturally expired, not in map");
assert!(!store.is_evicted(&hash2), "stmt2 should not be expired yet");
assert!(!store.is_evicted(&hash3), "stmt3 should not be expired yet");
assert_eq!(store.statement_count(), 2);
}
store.set_time(400);
store.enforce_limits();
{
assert!(!store.is_evicted(&hash1));
assert!(!store.is_evicted(&hash2), "stmt2 naturally expired, not in map");
assert!(!store.is_evicted(&hash3), "stmt3 should not be expired yet");
assert_eq!(store.statement_count(), 1);
}
store.set_time(600);
store.enforce_limits();
{
assert!(!store.is_evicted(&hash1));
assert!(!store.is_evicted(&hash2));
assert!(!store.is_evicted(&hash3), "stmt3 naturally expired, not in map");
assert_eq!(store.statement_count(), 0);
}
}
#[test]
fn check_expiration_does_nothing_when_no_expired_statements() {
let (mut store, _temp) = test_store();
store.set_time(1000);
let stmt = statement(1, 1, None, 100);
let hash = stmt.hash();
store.submit(stmt, StatementSource::Network);
store.enforce_limits();
assert!(store.has_statement(&hash));
assert!(!store.is_evicted(&hash));
assert_eq!(store.statement_count(), 1);
assert_eq!(store.evicted_count(), 0);
}
#[test]
fn check_expiration_correctly_updates_account_data() {
let (mut store, _temp) = test_store();
store.set_time(100);
let mut stmt = unsigned_statement(1, 1, Some(1), 100);
stmt.set_expiry_from_parts(200, 1);
sign_with(&mut stmt, 1);
let hash = stmt.hash();
store.submit(stmt, StatementSource::Network);
{
assert!(store.has_account(&account(1)));
assert_eq!(store.total_size(), 100);
}
store.set_time(300);
store.enforce_limits();
{
assert!(
!store.has_account(&account(1)),
"Account should be removed when all its statements expire"
);
assert_eq!(store.total_size(), 0, "Total size should be zero");
assert!(!store.is_evicted(&hash), "Naturally expired, not in map");
}
}
#[test]
fn check_expiration_clears_topic_and_key_indexes() {
let (mut store, _temp) = test_store();
store.set_time(100);
let mut stmt = unsigned_statement(1, 1, Some(1), 100);
stmt.set_expiry_from_parts(200, 1);
stmt.set_topic(0, topic(42));
stmt.set_decryption_key(dec_key(7));
sign_with(&mut stmt, 1);
let hash = stmt.hash();
store.submit(stmt, StatementSource::Network);
{
assert!(store.index_has_topic(&topic(42), &hash));
assert!(store.index_has_dec_key(&Some(dec_key(7)), &hash));
}
store.set_time(300);
store.enforce_limits();
{
assert!(!store.index_has_topic(&topic(42), &hash), "Topic index should be cleared");
assert!(
!store.index_has_dec_key(&Some(dec_key(7)), &hash),
"Decryption key index should be cleared"
);
assert!(!store.is_evicted(&hash), "Naturally expired, not in map");
}
}
#[test]
fn check_expiration_handles_empty_store() {
let (mut store, _temp) = test_store();
store.set_time(1000);
store.enforce_limits();
store.enforce_limits();
assert!(store.submit_index.read().allowance_cursor.is_none());
assert_eq!(store.statement_count(), 0);
assert_eq!(store.evicted_count(), 0);
}
#[test]
fn check_expiration_expires_properly_formatted_statements() {
let (mut store, _temp) = test_store();
store.set_time(1000);
let mut stmt = unsigned_statement(1, 1, None, 100);
stmt.set_expiry_from_parts(1001, 1); sign_with(&mut stmt, 1);
let hash = stmt.hash();
store.submit(stmt, StatementSource::Network);
assert_eq!(store.statement_count(), 1);
store.set_time(2000);
store.enforce_limits();
assert!(
!store.has_statement(&hash),
"Statement should be removed from the store after expiration"
);
assert!(!store.is_evicted(&hash), "Naturally expired, not in map");
}
#[test]
fn check_expiration_updates_database_columns() {
let (mut store, _temp) = test_store();
store.set_time(100);
let mut stmt = unsigned_statement(1, 1, None, 100);
stmt.set_expiry_from_parts(200, 1);
sign_with(&mut stmt, 1);
let hash = stmt.hash();
store.submit(stmt.clone(), StatementSource::Network);
let db_entry = store.db.get(col::STATEMENTS, &hash).unwrap();
assert!(db_entry.is_some(), "Statement should be in col::STATEMENTS after submit");
store.set_time(300);
store.enforce_limits();
{
assert_eq!(store.statement_count(), 0, "Statement should be removed from the store");
assert!(
!store.is_evicted(&hash),
"Naturally expired statement must not be in the expired map"
);
}
let db_entry = store.db.get(col::STATEMENTS, &hash).unwrap();
assert!(
db_entry.is_none(),
"Statement should be removed from col::STATEMENTS after expiration"
);
let expired_entry = store.db.get(col::EXPIRED, &hash).unwrap();
assert!(expired_entry.is_none(), "Naturally expired: not written to col::EXPIRED");
}
#[test]
fn enforce_allowances_evicts_excess_statements() {
let (mut store, _temp) = test_store();
store.set_time(0);
let s1 = statement(4, 10, None, 100); let s2 = statement(4, 20, None, 100);
let s3 = statement(4, 30, None, 100);
let s4 = statement(4, 40, None, 100);
let s5 = statement(4, 50, None, 100);
let h1 = s1.hash();
let h5 = s5.hash();
for statement in [&s1, &s2, &s3, &s4, &s5] {
store.force_insert(statement);
}
assert_eq!(store.statement_count(), 5);
assert_eq!(store.total_size(), 500);
store.enforce_limits();
assert_eq!(store.statement_count(), 4, "Should have 4 statements after eviction");
assert!(!store.has_statement(&h1), "Lowest priority should be evicted");
assert!(store.has_statement(&h5), "Highest priority should remain");
assert_eq!(store.total_size(), 400);
assert!(store.is_evicted(&h1));
}
#[test]
fn enforce_allowances_evicts_all_when_no_allowance_found() {
let (mut store, _temp) = test_store();
store.set_time(0);
let s1 = statement(0, 10, None, 100);
let s2 = statement(0, 20, None, 150);
let h1 = s1.hash();
let h2 = s2.hash();
store.force_insert(&s1);
store.force_insert(&s2);
assert_eq!(store.statement_count(), 2);
store.enforce_limits();
assert_eq!(store.statement_count(), 0, "All statements should be evicted");
assert!(!store.has_account(&account(0)), "Account should be removed");
assert!(store.is_evicted(&h1));
assert!(store.is_evicted(&h2));
}
#[test]
fn enforce_allowances_based_on_size() {
let (mut store, _temp) = test_store();
store.set_time(0);
let s1 = statement(2, 10, None, 600); let s2 = statement(2, 20, None, 600);
let h1 = s1.hash();
let h2 = s2.hash();
store.force_insert(&s1);
store.force_insert(&s2);
assert_eq!(store.total_size(), 1200);
store.enforce_limits();
assert_eq!(store.statement_count(), 1);
assert!(store.has_statement(&h2), "Higher priority should remain");
assert!(!store.has_statement(&h1), "Lower priority should be evicted");
assert_eq!(store.total_size(), 600);
}
#[test]
fn channel_replacement_only_higher_priority_succeeds() {
let (store, _temp) = test_store();
let source = StatementSource::Network;
let s1 = statement(1, 5, Some(1), 100);
let h1 = s1.hash();
assert_eq!(store.submit(s1, source), SubmitResult::New);
let result = store.submit(statement(1, 3, Some(1), 100), source);
assert!(
matches!(result, SubmitResult::Rejected(RejectionReason::ChannelPriorityTooLow { .. })),
"Lower priority should be rejected with ChannelPriorityTooLow, got: {result:?}"
);
let result = store.submit(statement(1, 5, Some(1), 101), source);
assert!(
matches!(result, SubmitResult::Rejected(RejectionReason::ChannelPriorityTooLow { .. })),
"Equal priority should be rejected with ChannelPriorityTooLow, got: {result:?}"
);
let s2 = statement(1, 10, Some(1), 200);
let h2 = s2.hash();
assert_eq!(store.submit(s2, source), SubmitResult::New);
{
assert_eq!(store.statement_count(), 1);
assert!(!store.has_statement(&h1), "Old channel message should be gone");
assert!(store.has_statement(&h2), "New channel message should exist");
assert!(store.is_evicted(&h1), "Old should be in expired");
assert_eq!(store.total_size(), 200);
}
}
#[test]
fn submit_rejects_malformed_statements() {
let (store, _temp) = test_store();
let mut base = Statement::new();
base.set_expiry(u64::MAX);
base.set_plain_data(vec![1]);
let ed_kp = sp_core::ed25519::Pair::from_string("//Alice", None).unwrap();
let sr_kp = sp_core::sr25519::Pair::from_string("//Alice", None).unwrap();
let ecdsa_kp = sp_core::ecdsa::Pair::from_string("//Alice", None).unwrap();
assert_eq!(
store.submit(base.clone(), StatementSource::Network),
SubmitResult::Invalid(InvalidReason::NoProof)
);
let bad_proofs = [
Proof::Ed25519 { signature: [0xAB; 64], signer: ed_kp.public().0 },
Proof::Sr25519 { signature: [0xCD; 64], signer: sr_kp.public().0 },
Proof::Secp256k1Ecdsa { signature: [0xEF; 65], signer: ecdsa_kp.public().0 },
];
for proof in bad_proofs {
let mut s = base.clone();
s.set_proof(proof);
assert_eq!(
store.submit(s, StatementSource::Network),
SubmitResult::Invalid(InvalidReason::BadProof)
);
}
let mut wrong_signer = base.clone();
wrong_signer.sign_ed25519_private(&ed_kp);
let alice_sig = match wrong_signer.proof().unwrap() {
Proof::Ed25519 { signature, .. } => *signature,
_ => panic!("expected Ed25519 proof after sign_ed25519_private"),
};
let bob_kp = sp_core::ed25519::Pair::from_string("//Bob", None).unwrap();
wrong_signer.set_proof(Proof::Ed25519 { signature: alice_sig, signer: bob_kp.public().0 });
assert_eq!(
store.submit(wrong_signer, StatementSource::Network),
SubmitResult::Invalid(InvalidReason::BadProof)
);
}
#[test]
fn channel_replacement_with_size_increase_evicts_others() {
let (store, _temp) = test_store();
let source = StatementSource::Network;
let s_ch = statement(3, 5, Some(1), 200);
let s_low = statement(3, 2, None, 300);
let s_mid = statement(3, 3, None, 300);
let h_ch = s_ch.hash();
let h_low = s_low.hash();
let h_mid = s_mid.hash();
assert_eq!(store.submit(s_ch, source), SubmitResult::New);
assert_eq!(store.submit(s_low, source), SubmitResult::New);
assert_eq!(store.submit(s_mid, source), SubmitResult::New);
assert_eq!(store.total_size(), 800);
let s_ch_big = statement(3, 10, Some(1), 600);
let h_ch_big = s_ch_big.hash();
assert_eq!(store.submit(s_ch_big, source), SubmitResult::New);
{
assert_eq!(store.statement_count(), 2);
assert!(!store.has_statement(&h_ch), "Old channel message replaced");
assert!(!store.has_statement(&h_low), "Priority 2 evicted to fit size");
assert!(store.has_statement(&h_mid), "Priority 3 should remain");
assert!(store.has_statement(&h_ch_big), "New channel message added");
assert_eq!(store.total_size(), 900); }
}
#[test]
fn subscription_reconnect_receives_current_state() {
use crate::StatementStoreSubscriptionApi;
use sp_statement_store::OptimizedTopicFilter;
let (store, _temp) = test_store();
let source = StatementSource::Local;
for i in 0..3u8 {
let res = store.submit(signed_statement(i), source);
assert_eq!(res, SubmitResult::New);
}
let (existing, sender, stream) =
store.subscribe_statement(OptimizedTopicFilter::Any).unwrap();
assert_eq!(existing.len(), 3, "First subscribe should return 3 existing statements");
drop(stream);
drop(sender);
for i in 3..5u8 {
assert_eq!(store.submit(signed_statement(i), source), SubmitResult::New);
}
let (existing, sender, stream) =
store.subscribe_statement(OptimizedTopicFilter::Any).unwrap();
assert_eq!(existing.len(), 5, "Re-subscribe should return all 5 current statements");
drop(stream);
drop(sender);
let hash_to_remove = signed_statement(0).hash();
store.remove(&hash_to_remove).unwrap();
let (existing, _sender, _stream) =
store.subscribe_statement(OptimizedTopicFilter::Any).unwrap();
assert_eq!(existing.len(), 4, "Re-subscribe after removal should return 4 statements");
}
#[test]
fn subscription_reconnect_with_topic_filter() {
use crate::StatementStoreSubscriptionApi;
use sp_statement_store::OptimizedTopicFilter;
let (store, _temp) = test_store();
let source = StatementSource::Local;
let topic_a = topic(1);
let topic_b = topic(2);
let s1 = signed_statement_with_topics(1, &[topic_a], None);
let s2 = signed_statement_with_topics(2, &[topic_b], None);
let s3 = signed_statement_with_topics(3, &[topic_a, topic_b], None);
assert_eq!(store.submit(s1, source), SubmitResult::New);
assert_eq!(store.submit(s2, source), SubmitResult::New);
assert_eq!(store.submit(s3, source), SubmitResult::New);
let filter_a = OptimizedTopicFilter::MatchAll(std::collections::HashSet::from([topic_a]));
let (existing, sender, stream) = store.subscribe_statement(filter_a.clone()).unwrap();
assert_eq!(existing.len(), 2, "MatchAll([A]) should match s1 and s3");
drop(sender);
drop(stream);
let s4 = signed_statement_with_topics(4, &[topic_a], None);
assert_eq!(store.submit(s4, source), SubmitResult::New);
let (existing, sender, stream) = store.subscribe_statement(filter_a).unwrap();
assert_eq!(existing.len(), 3, "Re-subscribe MatchAll([A]) should return s1, s3, s4");
drop(sender);
drop(stream);
let filter_b = OptimizedTopicFilter::MatchAll(std::collections::HashSet::from([topic_b]));
let (existing, _sender, _stream) = store.subscribe_statement(filter_b).unwrap();
assert_eq!(existing.len(), 2, "Re-subscribe MatchAll([B]) should return s2 and s3");
}
#[tokio::test]
async fn subscription_delivers_each_statement_exactly_once_across_boundary() {
use crate::StatementStoreSubscriptionApi;
use futures::StreamExt;
use sp_statement_store::{OptimizedTopicFilter, StatementEvent};
let (store, _temp) = test_store();
let source = StatementSource::Local;
let a = signed_statement(0);
let b = signed_statement(1);
assert_eq!(store.submit(a.clone(), source), SubmitResult::New);
assert_eq!(store.submit(b.clone(), source), SubmitResult::New);
let (existing, _sender, mut stream) =
store.subscribe_statement(OptimizedTopicFilter::Any).unwrap();
let mut snapshot: Vec<Statement> = existing
.iter()
.map(|bytes| Statement::decode(&mut &bytes[..]).unwrap())
.collect();
snapshot.sort_by_key(|s| s.hash());
let mut expected_snapshot = vec![a.clone(), b.clone()];
expected_snapshot.sort_by_key(|s| s.hash());
assert_eq!(
snapshot, expected_snapshot,
"snapshot must contain exactly the pre-existing statements"
);
let c = signed_statement(2);
assert_eq!(store.submit(c.clone(), source), SubmitResult::New);
let event = tokio::time::timeout(std::time::Duration::from_secs(5), stream.next())
.await
.expect("live statement should arrive within the timeout")
.expect("stream should yield an event");
let StatementEvent::NewStatements { statements, .. } = event;
let live: Vec<Statement> = statements
.iter()
.map(|bytes| Statement::decode(&mut &bytes.0[..]).unwrap())
.collect();
assert_eq!(
live,
vec![c.clone()],
"live stream must deliver exactly the post-subscribe statement"
);
assert!(
tokio::time::timeout(std::time::Duration::from_millis(500), stream.next())
.await
.is_err(),
"no further (duplicate) statements must be delivered"
);
}
#[test]
fn subscription_snapshot_deduplicates_multi_topic_match_any() {
use crate::StatementStoreSubscriptionApi;
use sp_statement_store::OptimizedTopicFilter;
let (store, _temp) = test_store();
let topic_a = topic(1);
let topic_b = topic(2);
let s = signed_statement_with_topics(1, &[topic_a, topic_b], None);
assert_eq!(store.submit(s, StatementSource::Local), SubmitResult::New);
let filter =
OptimizedTopicFilter::MatchAny(std::collections::HashSet::from([topic_a, topic_b]));
let (existing, _sender, _stream) = store.subscribe_statement(filter).unwrap();
assert_eq!(
existing.len(),
1,
"MatchAny snapshot must not duplicate a multi-topic statement"
);
}
#[tokio::test]
async fn subscription_match_all_delivers_exactly_once_across_boundary() {
use crate::StatementStoreSubscriptionApi;
use futures::StreamExt;
use sp_statement_store::{OptimizedTopicFilter, StatementEvent};
let (store, _temp) = test_store();
let source = StatementSource::Local;
let t = topic(7);
let a = signed_statement_with_topics(0, &[t], None);
let b = signed_statement_with_topics(1, &[t], None);
assert_eq!(store.submit(a, source), SubmitResult::New);
assert_eq!(store.submit(b, source), SubmitResult::New);
let filter = OptimizedTopicFilter::MatchAll(std::collections::HashSet::from([t]));
let (existing, _sender, mut stream) = store.subscribe_statement(filter).unwrap();
assert_eq!(
existing.len(),
2,
"MatchAll snapshot must contain both pre-existing statements"
);
let c = signed_statement_with_topics(2, &[t], None);
assert_eq!(store.submit(c.clone(), source), SubmitResult::New);
let event = tokio::time::timeout(std::time::Duration::from_secs(5), stream.next())
.await
.expect("live statement should arrive within the timeout")
.expect("stream should yield an event");
let StatementEvent::NewStatements { statements, .. } = event;
let live: Vec<Statement> = statements
.iter()
.map(|bytes| Statement::decode(&mut &bytes.0[..]).unwrap())
.collect();
assert_eq!(
live,
vec![c],
"MatchAll live stream must deliver exactly the post-subscribe statement"
);
assert!(
tokio::time::timeout(std::time::Duration::from_millis(500), stream.next())
.await
.is_err(),
"no further (duplicate) statements must be delivered"
);
}
#[tokio::test]
async fn subscription_no_loss_or_duplicate_under_concurrent_submits() {
use crate::StatementStoreSubscriptionApi;
use futures::StreamExt;
use sp_statement_store::{OptimizedTopicFilter, StatementEvent};
use std::collections::HashSet;
let (store, _temp) = test_store();
let store = std::sync::Arc::new(store);
let t = topic(9);
const N: u8 = 60;
let all: Vec<Statement> =
(0..N).map(|i| signed_statement_with_topics(i, &[t], None)).collect();
let all_hashes: HashSet<_> = all.iter().map(|s| s.hash()).collect();
let split = 10usize;
for s in &all[..split] {
assert_eq!(store.submit(s.clone(), StatementSource::Local), SubmitResult::New);
}
let store2 = store.clone();
let rest: Vec<Statement> = all[split..].to_vec();
let submitter = std::thread::spawn(move || {
for s in rest {
let _ = store2.submit(s, StatementSource::Local);
}
});
let filter = OptimizedTopicFilter::MatchAll(HashSet::from([t]));
let (existing, _sender, mut stream) = store.subscribe_statement(filter).unwrap();
submitter.join().unwrap();
let mut seen = existing
.iter()
.map(|b| Statement::decode(&mut &b[..]).unwrap().hash())
.collect::<Vec<_>>();
while seen.len() < N as usize {
match tokio::time::timeout(std::time::Duration::from_secs(5), stream.next()).await {
Ok(Some(StatementEvent::NewStatements { statements, .. })) => {
for b in statements {
seen.push(Statement::decode(&mut &b.0[..]).unwrap().hash());
}
},
_ => break,
}
}
let seen_set: HashSet<_> = seen.iter().copied().collect();
assert_eq!(
seen.len(),
N as usize,
"each statement must be delivered exactly once (got {} deliveries for {} statements — loss or duplicate)",
seen.len(),
N
);
assert_eq!(seen_set, all_hashes, "delivered set must equal submitted set");
}
mod multi_filter {
use super::*;
use crate::{
MultiFilterEventStream, MultiFilterSubscriptionApi, MultiFilterSubscriptionEvent,
};
use futures::StreamExt;
use sp_statement_store::OptimizedTopicFilter;
use std::{collections::HashSet, sync::Arc, time::Duration};
fn arc_test_store() -> (Arc<Store>, tempfile::TempDir) {
let (store, dir) = test_store();
(Arc::new(store), dir)
}
async fn drain_all(
stream: &mut MultiFilterEventStream,
idle: Duration,
) -> Vec<MultiFilterSubscriptionEvent> {
let mut events = Vec::new();
while let Ok(Some(event)) = tokio::time::timeout(idle, stream.next()).await {
events.push(event);
}
events
}
#[test]
fn replay_watermark_excludes_later_admissions() {
let (store, _dir) = test_store();
let before = signed_statement(1);
let after = signed_statement(2);
assert_eq!(store.submit(before.clone(), StatementSource::Local), SubmitResult::New);
let mut watermark = None;
assert!(store
.register_replay(&mut |captured| {
watermark = Some(captured);
true
})
.unwrap()
.is_some());
assert_eq!(store.submit(after.clone(), StatementSource::Local), SubmitResult::New);
let replay =
store.replay_batch(&OptimizedTopicFilter::Any, 0, watermark.unwrap()).unwrap();
let hashes: HashSet<_> = replay
.statements
.iter()
.map(|encoded| Statement::decode(&mut encoded.as_slice()).unwrap().hash())
.collect();
assert!(replay.done);
assert!(hashes.contains(&before.hash()));
assert!(!hashes.contains(&after.hash()));
}
#[tokio::test]
async fn add_filter_replays_snapshot_and_delivers_later_submissions_live() {
let (store, _dir) = arc_test_store();
let (handle, mut stream) = store.create_subscription();
const NUM_STATEMENTS: u8 = 50;
const NUM_PRE_FILTER: u8 = 20;
for i in 0..NUM_PRE_FILTER {
let stmt = signed_statement(i);
assert_eq!(store.submit(stmt, StatementSource::Local), SubmitResult::New);
}
let filter_id = handle.add_filter(OptimizedTopicFilter::Any).unwrap();
for i in NUM_PRE_FILTER..NUM_STATEMENTS {
let stmt = signed_statement(i);
assert_eq!(store.submit(stmt, StatementSource::Local), SubmitResult::New);
}
let mut snapshot_hashes: HashSet<[u8; 32]> = HashSet::new();
let mut live_with_filter: HashSet<[u8; 32]> = HashSet::new();
for event in drain_all(&mut stream, Duration::from_millis(300)).await {
match event {
MultiFilterSubscriptionEvent::ReplayStatements { statements, .. } => {
snapshot_hashes.extend(
statements
.iter()
.map(|bytes| Statement::decode(&mut &bytes[..]).unwrap().hash()),
);
},
MultiFilterSubscriptionEvent::NewStatement(event)
if event.matched_filter_ids.contains(&filter_id) =>
{
live_with_filter.insert(event.hash);
},
_ => {},
}
}
let expected_replayed: HashSet<[u8; 32]> =
(0..NUM_PRE_FILTER).map(|i| signed_statement(i).hash()).collect();
let expected_live: HashSet<[u8; 32]> =
(NUM_PRE_FILTER..NUM_STATEMENTS).map(|i| signed_statement(i).hash()).collect();
assert_eq!(snapshot_hashes, expected_replayed);
assert_eq!(live_with_filter, expected_live);
}
}
}