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}