referrerpolicy=no-referrer-when-downgrade

sp_statement_store/
store_api.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18pub use crate::runtime_api::StatementSource;
19use crate::{Hash, Statement, Topic, MAX_ANY_TOPICS, MAX_TOPICS};
20use sp_core::{bounded_vec::BoundedVec, Bytes, ConstU32};
21use std::collections::HashSet;
22
23/// Identifier for a filter attached to a multi-filter subscription
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
25pub struct FilterId(u64);
26
27impl FilterId {
28	/// Creates a filter id from its numeric representation
29	pub fn new(id: u64) -> Self {
30		FilterId(id)
31	}
32
33	/// Returns the numeric representation of this filter id
34	pub fn as_u64(&self) -> u64 {
35		self.0
36	}
37}
38
39/// Live statement event emitted by a multi-filter subscription
40#[derive(Debug, Clone)]
41pub struct LiveStatementEvent {
42	/// Hash of the statement
43	pub hash: Hash,
44	/// SCALE-encoded statement bytes
45	pub encoded: Vec<u8>,
46	/// Filter ids that matched the statement
47	pub matched_filter_ids: Vec<FilterId>,
48}
49
50/// Statement store error.
51#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
53pub enum Error {
54	/// Database error.
55	#[error("Database error: {0:?}")]
56	Db(String),
57	/// Decoding error
58	#[error("Decoding error: {0:?}")]
59	Decode(String),
60	/// Error reading from storage.
61	#[error("Storage error: {0:?}")]
62	Storage(String),
63	/// Invalid configuration.
64	#[error("Invalid configuration: {0}")]
65	InvalidConfig(String),
66}
67
68/// Filter for subscribing to statements with different topics.
69#[derive(Debug, Clone)]
70#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
71#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
72pub enum TopicFilter {
73	/// Matches all topics.
74	Any,
75	/// Matches only statements including all of the given topics.
76	/// Bytes are expected to be a 32-byte topic. Up to [`MAX_TOPICS`] topics can be provided.
77	MatchAll(BoundedVec<Topic, ConstU32<{ MAX_TOPICS as u32 }>>),
78	/// Matches statements including any of the given topics.
79	/// Bytes are expected to be a 32-byte topic. Up to [`MAX_ANY_TOPICS`] topics can be provided.
80	MatchAny(BoundedVec<Topic, ConstU32<{ MAX_ANY_TOPICS as u32 }>>),
81}
82
83/// Topic filter for statement subscriptions, optimized for matching.
84#[derive(Clone, Debug)]
85pub enum OptimizedTopicFilter {
86	/// Matches all topics.
87	Any,
88	/// Matches only statements including all of the given topics.
89	/// Up to `4` topics can be provided.
90	MatchAll(HashSet<Topic>),
91	/// Matches statements including any of the given topics.
92	/// Up to `128` topics can be provided.
93	MatchAny(HashSet<Topic>),
94}
95
96impl OptimizedTopicFilter {
97	/// Check if the statement matches the filter.
98	pub fn matches(&self, statement: &Statement) -> bool {
99		match self {
100			OptimizedTopicFilter::Any => true,
101			OptimizedTopicFilter::MatchAll(topics) => {
102				topics.iter().all(|topic| statement.topics().contains(topic))
103			},
104			OptimizedTopicFilter::MatchAny(topics) => {
105				statement.topics().iter().any(|topic| topics.contains(topic))
106			},
107		}
108	}
109}
110
111// Convert TopicFilter to CheckedTopicFilter.
112impl From<TopicFilter> for OptimizedTopicFilter {
113	fn from(filter: TopicFilter) -> Self {
114		match filter {
115			TopicFilter::Any => OptimizedTopicFilter::Any,
116			TopicFilter::MatchAll(topics) => {
117				let mut parsed_topics = HashSet::with_capacity(topics.len());
118				for topic in topics {
119					parsed_topics.insert(topic);
120				}
121				OptimizedTopicFilter::MatchAll(parsed_topics)
122			},
123			TopicFilter::MatchAny(topics) => {
124				let mut parsed_topics = HashSet::with_capacity(topics.len());
125				for topic in topics {
126					parsed_topics.insert(topic);
127				}
128				OptimizedTopicFilter::MatchAny(parsed_topics)
129			},
130		}
131	}
132}
133
134/// Reason why a statement was rejected from the store.
135#[derive(Debug, Clone, Eq, PartialEq)]
136#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
137#[cfg_attr(feature = "serde", serde(tag = "reason", rename_all = "camelCase"))]
138pub enum RejectionReason {
139	/// Statement data exceeds the maximum allowed size for the account.
140	DataTooLarge {
141		/// The size of the submitted statement data.
142		submitted_size: usize,
143		/// Still available data size for the account.
144		available_size: usize,
145	},
146	/// Attempting to replace a channel message with lower or equal expiry.
147	ChannelPriorityTooLow {
148		/// The expiry of the submitted statement.
149		submitted_expiry: u64,
150		/// The minimum expiry of the existing channel message.
151		min_expiry: u64,
152	},
153	/// Account reached its statement limit and submitted expiry is too low to evict existing.
154	AccountFull {
155		/// The expiry of the submitted statement.
156		submitted_expiry: u64,
157		/// The minimum expiry of the existing statement.
158		min_expiry: u64,
159	},
160	/// The global statement store is full and cannot accept new statements.
161	StoreFull,
162	/// The size limit of the retention track the statement would be kept on is reached.
163	TrackFull,
164	/// Account has no allowance set.
165	NoAllowance,
166}
167
168impl RejectionReason {
169	/// Returns a short string label suitable for use in metrics.
170	pub fn label(&self) -> &'static str {
171		match self {
172			RejectionReason::DataTooLarge { .. } => "data_too_large",
173			RejectionReason::ChannelPriorityTooLow { .. } => "channel_priority_too_low",
174			RejectionReason::AccountFull { .. } => "account_full",
175			RejectionReason::StoreFull => "store_full",
176			RejectionReason::TrackFull => "track_full",
177			RejectionReason::NoAllowance => "no_allowance",
178		}
179	}
180}
181
182/// Reason why a statement failed validation.
183#[derive(Debug, Clone, Eq, PartialEq)]
184#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
185#[cfg_attr(feature = "serde", serde(tag = "reason", rename_all = "camelCase"))]
186pub enum InvalidReason {
187	/// Statement has no proof.
188	NoProof,
189	/// Proof validation failed.
190	BadProof,
191	/// Statement exceeds max allowed statement size.
192	EncodingTooLarge {
193		/// The size of the submitted statement encoding.
194		submitted_size: usize,
195		/// The maximum allowed size.
196		max_size: usize,
197	},
198	/// Statement has already expired. The expiry field is in the past.
199	AlreadyExpired,
200}
201
202impl InvalidReason {
203	/// Returns a short string label suitable for use in metrics.
204	pub fn label(&self) -> &'static str {
205		match self {
206			InvalidReason::NoProof => "no_proof",
207			InvalidReason::BadProof => "bad_proof",
208			InvalidReason::EncodingTooLarge { .. } => "encoding_too_large",
209			InvalidReason::AlreadyExpired => "already_expired",
210		}
211	}
212}
213
214/// Statement submission outcome
215#[derive(Debug, Clone, Eq, PartialEq)]
216#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
217#[cfg_attr(feature = "serde", serde(tag = "status", rename_all = "camelCase"))]
218pub enum SubmitResult {
219	/// Statement was accepted as new.
220	New,
221	/// Statement was already known.
222	Known,
223	/// Statement was already known but has expired.
224	KnownExpired,
225	/// Statement was rejected because the store is full or priority is too low.
226	Rejected(RejectionReason),
227	/// Statement failed validation.
228	Invalid(InvalidReason),
229	/// Internal store error.
230	InternalError(Error),
231}
232
233/// Rejection reason as exposed by the spec-v2 `statement_unstable_submit` RPC.
234#[derive(Debug, Clone, Eq, PartialEq)]
235#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
236#[cfg_attr(
237	feature = "serde",
238	serde(tag = "reason", rename_all = "camelCase", rename_all_fields = "camelCase")
239)]
240pub enum SubmitRejectionReason {
241	/// Statement data exceeds the maximum allowed size for the account.
242	DataTooLarge {
243		/// The size of the submitted statement data.
244		submitted_size: usize,
245		/// Still available data size for the account.
246		available_size: usize,
247	},
248	/// Attempting to replace a channel message with lower or equal expiry.
249	ChannelPriorityTooLow {
250		/// The expiry of the submitted statement.
251		submitted_expiry: u64,
252		/// The minimum expiry of the existing channel message.
253		min_expiry: u64,
254	},
255	/// Account reached its statement limit and submitted expiry is too low to evict existing.
256	AccountFull {
257		/// The expiry of the submitted statement.
258		submitted_expiry: u64,
259		/// The minimum expiry of the existing statement.
260		min_expiry: u64,
261	},
262	/// The global statement store is full and cannot accept new statements.
263	StoreFull,
264	/// Account has no allowance set.
265	NoAllowance,
266}
267
268impl From<RejectionReason> for SubmitRejectionReason {
269	fn from(reason: RejectionReason) -> Self {
270		match reason {
271			RejectionReason::DataTooLarge { submitted_size, available_size } => {
272				SubmitRejectionReason::DataTooLarge { submitted_size, available_size }
273			},
274			RejectionReason::ChannelPriorityTooLow { submitted_expiry, min_expiry } => {
275				SubmitRejectionReason::ChannelPriorityTooLow { submitted_expiry, min_expiry }
276			},
277			RejectionReason::AccountFull { submitted_expiry, min_expiry } => {
278				SubmitRejectionReason::AccountFull { submitted_expiry, min_expiry }
279			},
280			// The RPC spec knows no tracks: a full track is a full store to the submitter.
281			RejectionReason::StoreFull | RejectionReason::TrackFull => {
282				SubmitRejectionReason::StoreFull
283			},
284			RejectionReason::NoAllowance => SubmitRejectionReason::NoAllowance,
285		}
286	}
287}
288
289/// Invalid reason as exposed by the spec-v2 `statement_unstable_submit` RPC.
290#[derive(Debug, Clone, Eq, PartialEq)]
291#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
292#[cfg_attr(
293	feature = "serde",
294	serde(tag = "reason", rename_all = "camelCase", rename_all_fields = "camelCase")
295)]
296pub enum SubmitInvalidReason {
297	/// Statement has no proof.
298	NoProof,
299	/// Proof validation failed.
300	BadProof,
301	/// Statement exceeds max allowed statement size.
302	EncodingTooLarge {
303		/// The size of the submitted statement encoding.
304		submitted_size: usize,
305		/// The maximum allowed size.
306		max_size: usize,
307	},
308	/// Statement has already expired. The expiry field is in the past.
309	AlreadyExpired,
310}
311
312impl From<InvalidReason> for SubmitInvalidReason {
313	fn from(reason: InvalidReason) -> Self {
314		match reason {
315			InvalidReason::NoProof => SubmitInvalidReason::NoProof,
316			InvalidReason::BadProof => SubmitInvalidReason::BadProof,
317			InvalidReason::EncodingTooLarge { submitted_size, max_size } => {
318				SubmitInvalidReason::EncodingTooLarge { submitted_size, max_size }
319			},
320			InvalidReason::AlreadyExpired => SubmitInvalidReason::AlreadyExpired,
321		}
322	}
323}
324
325/// Statement submission outcome exposed by the spec-v2 `statement_unstable_submit` RPC.
326#[derive(Debug, Clone, Eq, PartialEq)]
327#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
328#[cfg_attr(feature = "serde", serde(tag = "status", rename_all = "camelCase"))]
329pub enum SubmitOutcome {
330	/// Statement was accepted as new
331	New,
332	/// Statement was already known
333	Known,
334	/// Statement was rejected because the store is full or priority is too low
335	Rejected(SubmitRejectionReason),
336	/// Statement failed validation
337	Invalid(SubmitInvalidReason),
338}
339
340impl SubmitOutcome {
341	/// Converts a store submission result into the RPC-visible outcome
342	pub fn from_submit_result(result: SubmitResult) -> std::result::Result<Self, Error> {
343		match result {
344			SubmitResult::New => Ok(SubmitOutcome::New),
345			SubmitResult::Known => Ok(SubmitOutcome::Known),
346			// The store only returns `KnownExpired` for `Network` sources; the RPC path is `Local`.
347			SubmitResult::KnownExpired => {
348				Err(Error::Storage("unexpected KnownExpired on local submission".into()))
349			},
350			SubmitResult::Rejected(reason) => Ok(SubmitOutcome::Rejected(reason.into())),
351			SubmitResult::Invalid(reason) => Ok(SubmitOutcome::Invalid(reason.into())),
352			SubmitResult::InternalError(error) => Err(error),
353		}
354	}
355}
356
357/// An item returned by the statement subscription stream.
358#[derive(Debug, Clone, Eq, PartialEq)]
359#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
360#[cfg_attr(feature = "serde", serde(tag = "event", content = "data", rename_all = "camelCase"))]
361pub enum StatementEvent {
362	/// A batch of statements matching the subscription filter.
363	NewStatements {
364		/// A batch of statements matching the subscription filter, each entry is a SCALE-encoded
365		/// statement.
366		statements: Vec<Bytes>,
367		/// An optional count of how many more matching statements are in the store after this
368		/// batch. This guarantees to the client that it will receive at least this many more
369		/// statements in the subscription stream, but it may receive more if new statements are
370		/// added to the store that match the filter.
371		#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
372		remaining: Option<u32>,
373	},
374}
375
376/// Result type for `Error`
377pub type Result<T> = std::result::Result<T, Error>;
378
379/// Decision returned by the filter used in [`StatementStore::statements_by_hashes`].
380#[derive(Debug, Clone, Copy, PartialEq, Eq)]
381pub enum FilterDecision {
382	/// Skip this statement, continue to next.
383	Skip,
384	/// Take this statement, continue to next.
385	Take,
386	/// Stop iteration, return collected statements.
387	Abort,
388}
389
390/// A batch of statements read from the admission journal by
391/// [`StatementStore::admitted_statements`].
392#[derive(Debug)]
393pub struct AdmittedBatch {
394	/// The taken statements, oldest admission first.
395	pub statements: Vec<(Hash, Statement)>,
396	/// Admission sequence number to resume the walk from.
397	pub cursor: u64,
398	/// Whether the walk reached the watermark; always equals `cursor >= watermark`.
399	pub done: bool,
400}
401
402/// Statement store API.
403pub trait StatementStore: Send + Sync {
404	/// Return all statements.
405	fn statements(&self) -> Result<Vec<(Hash, Statement)>>;
406
407	/// Return recent statements with their admission sequence numbers, oldest admission
408	/// first, and clear the internal index.
409	///
410	/// This consumes and clears the recently received statements,
411	/// allowing new statements to be collected from this point forward.
412	fn take_recent_statements(&self) -> Result<Vec<(u64, Hash, Statement)>>;
413
414	/// Get statement by hash.
415	fn statement(&self, hash: &Hash) -> Result<Option<Statement>>;
416
417	/// Check if statement exists in the store
418	///
419	/// Fast index check without accessing the DB.
420	fn has_statement(&self, hash: &Hash) -> bool;
421
422	/// Fetch statements by their hashes with a filter callback.
423	///
424	/// The callback receives (hash, encoded_bytes, decoded_statement) and returns:
425	/// - `Skip`: ignore this statement, continue to next
426	/// - `Take`: include this statement in the result, continue to next
427	/// - `Abort`: stop iteration, return collected statements so far
428	///
429	/// Returns (statements, number_of_hashes_processed). A hash absent from the store or whose
430	/// statement fails to decode counts as processed without reaching the callback, the hash an
431	/// `Abort` stopped on does not.
432	fn statements_by_hashes(
433		&self,
434		hashes: &[Hash],
435		filter: &mut dyn FnMut(&Hash, &[u8], &Statement) -> FilterDecision,
436	) -> Result<(Vec<(Hash, Statement)>, usize)>;
437
438	/// One past the newest assigned admission sequence number.
439	///
440	/// Every statement currently in the store was admitted below this boundary, so it
441	/// serves as the watermark for a subsequent [`Self::admitted_statements`] walk.
442	fn admission_watermark(&self) -> Result<u64>;
443
444	/// Walk the admission journal from `cursor` (inclusive) towards `watermark`
445	/// (exclusive), collecting statements through a filter callback.
446	///
447	/// The callback receives (hash, encoded_bytes, decoded_statement) and returns:
448	/// - `Skip`: leave this statement out of the batch, continue to next
449	/// - `Take`: include this statement in the batch, continue to next
450	/// - `Abort`: stop the walk before this statement
451	///
452	/// Sequence numbers whose statement has left the store are passed over. The returned
453	/// cursor sits after every visited statement except an `Abort`ed one, which the next
454	/// walk revisits first.
455	///
456	/// A call visits at most `scan_limit` journal entries, passed-over ones included, so
457	/// its cost stays bounded even when the filter takes nothing. `scan_limit` must be
458	/// positive: a walk that visits no entries cannot advance the cursor. A walk stopped
459	/// by the limit returns a partial cursor to resume from, exactly like an `Abort`.
460	fn admitted_statements(
461		&self,
462		cursor: u64,
463		watermark: u64,
464		scan_limit: usize,
465		filter: &mut dyn FnMut(&Hash, &[u8], &Statement) -> FilterDecision,
466	) -> Result<AdmittedBatch>;
467
468	/// Return the data of all known statements which include all topics and have no `DecryptionKey`
469	/// field.
470	fn broadcasts(&self, match_all_topics: &[Topic]) -> Result<Vec<Vec<u8>>>;
471
472	/// Return the data of all known statements whose decryption key is identified as `dest` (this
473	/// will generally be the public key or a hash thereof for symmetric ciphers, or a hash of the
474	/// private key for symmetric ciphers).
475	fn posted(&self, match_all_topics: &[Topic], dest: [u8; 32]) -> Result<Vec<Vec<u8>>>;
476
477	/// Return the decrypted data of all known statements whose decryption key is identified as
478	/// `dest`. The key must be available to the client.
479	fn posted_clear(&self, match_all_topics: &[Topic], dest: [u8; 32]) -> Result<Vec<Vec<u8>>>;
480
481	/// Return all known statements which include all topics and have no `DecryptionKey`
482	/// field.
483	fn broadcasts_stmt(&self, match_all_topics: &[Topic]) -> Result<Vec<Vec<u8>>>;
484
485	/// Return all known statements whose decryption key is identified as `dest` (this
486	/// will generally be the public key or a hash thereof for symmetric ciphers, or a hash of the
487	/// private key for symmetric ciphers).
488	fn posted_stmt(&self, match_all_topics: &[Topic], dest: [u8; 32]) -> Result<Vec<Vec<u8>>>;
489
490	/// Return the statement and the decrypted data of all known statements whose decryption key is
491	/// identified as `dest`. The key must be available to the client.
492	///
493	/// The result is for each statement: the SCALE-encoded statement concatenated to the
494	/// decrypted data.
495	fn posted_clear_stmt(&self, match_all_topics: &[Topic], dest: [u8; 32])
496		-> Result<Vec<Vec<u8>>>;
497
498	/// Submit a statement.
499	fn submit(&self, statement: Statement, source: StatementSource) -> SubmitResult;
500
501	/// Remove a statement from the store.
502	fn remove(&self, hash: &Hash) -> Result<()>;
503
504	/// Remove all statements authored by `who`.
505	fn remove_by(&self, who: [u8; 32]) -> Result<()>;
506
507	/// Topics across all live statement subscriptions, for advertising topic affinity.
508	///
509	/// Defaults to empty for stores without a subscription manager.
510	fn subscription_topics(&self) -> HashSet<Topic> {
511		HashSet::new()
512	}
513}