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 /// Account has no allowance set.
163 NoAllowance,
164}
165
166impl RejectionReason {
167 /// Returns a short string label suitable for use in metrics.
168 pub fn label(&self) -> &'static str {
169 match self {
170 RejectionReason::DataTooLarge { .. } => "data_too_large",
171 RejectionReason::ChannelPriorityTooLow { .. } => "channel_priority_too_low",
172 RejectionReason::AccountFull { .. } => "account_full",
173 RejectionReason::StoreFull => "store_full",
174 RejectionReason::NoAllowance => "no_allowance",
175 }
176 }
177}
178
179/// Reason why a statement failed validation.
180#[derive(Debug, Clone, Eq, PartialEq)]
181#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
182#[cfg_attr(feature = "serde", serde(tag = "reason", rename_all = "camelCase"))]
183pub enum InvalidReason {
184 /// Statement has no proof.
185 NoProof,
186 /// Proof validation failed.
187 BadProof,
188 /// Statement exceeds max allowed statement size.
189 EncodingTooLarge {
190 /// The size of the submitted statement encoding.
191 submitted_size: usize,
192 /// The maximum allowed size.
193 max_size: usize,
194 },
195 /// Statement has already expired. The expiry field is in the past.
196 AlreadyExpired,
197}
198
199impl InvalidReason {
200 /// Returns a short string label suitable for use in metrics.
201 pub fn label(&self) -> &'static str {
202 match self {
203 InvalidReason::NoProof => "no_proof",
204 InvalidReason::BadProof => "bad_proof",
205 InvalidReason::EncodingTooLarge { .. } => "encoding_too_large",
206 InvalidReason::AlreadyExpired => "already_expired",
207 }
208 }
209}
210
211/// Statement submission outcome
212#[derive(Debug, Clone, Eq, PartialEq)]
213#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
214#[cfg_attr(feature = "serde", serde(tag = "status", rename_all = "camelCase"))]
215pub enum SubmitResult {
216 /// Statement was accepted as new.
217 New,
218 /// Statement was already known.
219 Known,
220 /// Statement was already known but has expired.
221 KnownExpired,
222 /// Statement was rejected because the store is full or priority is too low.
223 Rejected(RejectionReason),
224 /// Statement failed validation.
225 Invalid(InvalidReason),
226 /// Internal store error.
227 InternalError(Error),
228}
229
230/// Rejection reason as exposed by the spec-v2 `statement_unstable_submit` RPC.
231#[derive(Debug, Clone, Eq, PartialEq)]
232#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
233#[cfg_attr(
234 feature = "serde",
235 serde(tag = "reason", rename_all = "camelCase", rename_all_fields = "camelCase")
236)]
237pub enum SubmitRejectionReason {
238 /// Statement data exceeds the maximum allowed size for the account.
239 DataTooLarge {
240 /// The size of the submitted statement data.
241 submitted_size: usize,
242 /// Still available data size for the account.
243 available_size: usize,
244 },
245 /// Attempting to replace a channel message with lower or equal expiry.
246 ChannelPriorityTooLow {
247 /// The expiry of the submitted statement.
248 submitted_expiry: u64,
249 /// The minimum expiry of the existing channel message.
250 min_expiry: u64,
251 },
252 /// Account reached its statement limit and submitted expiry is too low to evict existing.
253 AccountFull {
254 /// The expiry of the submitted statement.
255 submitted_expiry: u64,
256 /// The minimum expiry of the existing statement.
257 min_expiry: u64,
258 },
259 /// The global statement store is full and cannot accept new statements.
260 StoreFull,
261 /// Account has no allowance set.
262 NoAllowance,
263}
264
265impl From<RejectionReason> for SubmitRejectionReason {
266 fn from(reason: RejectionReason) -> Self {
267 match reason {
268 RejectionReason::DataTooLarge { submitted_size, available_size } => {
269 SubmitRejectionReason::DataTooLarge { submitted_size, available_size }
270 },
271 RejectionReason::ChannelPriorityTooLow { submitted_expiry, min_expiry } => {
272 SubmitRejectionReason::ChannelPriorityTooLow { submitted_expiry, min_expiry }
273 },
274 RejectionReason::AccountFull { submitted_expiry, min_expiry } => {
275 SubmitRejectionReason::AccountFull { submitted_expiry, min_expiry }
276 },
277 RejectionReason::StoreFull => SubmitRejectionReason::StoreFull,
278 RejectionReason::NoAllowance => SubmitRejectionReason::NoAllowance,
279 }
280 }
281}
282
283/// Invalid reason as exposed by the spec-v2 `statement_unstable_submit` RPC.
284#[derive(Debug, Clone, Eq, PartialEq)]
285#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
286#[cfg_attr(
287 feature = "serde",
288 serde(tag = "reason", rename_all = "camelCase", rename_all_fields = "camelCase")
289)]
290pub enum SubmitInvalidReason {
291 /// Statement has no proof.
292 NoProof,
293 /// Proof validation failed.
294 BadProof,
295 /// Statement exceeds max allowed statement size.
296 EncodingTooLarge {
297 /// The size of the submitted statement encoding.
298 submitted_size: usize,
299 /// The maximum allowed size.
300 max_size: usize,
301 },
302 /// Statement has already expired. The expiry field is in the past.
303 AlreadyExpired,
304}
305
306impl From<InvalidReason> for SubmitInvalidReason {
307 fn from(reason: InvalidReason) -> Self {
308 match reason {
309 InvalidReason::NoProof => SubmitInvalidReason::NoProof,
310 InvalidReason::BadProof => SubmitInvalidReason::BadProof,
311 InvalidReason::EncodingTooLarge { submitted_size, max_size } => {
312 SubmitInvalidReason::EncodingTooLarge { submitted_size, max_size }
313 },
314 InvalidReason::AlreadyExpired => SubmitInvalidReason::AlreadyExpired,
315 }
316 }
317}
318
319/// Statement submission outcome exposed by the spec-v2 `statement_unstable_submit` RPC.
320#[derive(Debug, Clone, Eq, PartialEq)]
321#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
322#[cfg_attr(feature = "serde", serde(tag = "status", rename_all = "camelCase"))]
323pub enum SubmitOutcome {
324 /// Statement was accepted as new
325 New,
326 /// Statement was already known
327 Known,
328 /// Statement was rejected because the store is full or priority is too low
329 Rejected(SubmitRejectionReason),
330 /// Statement failed validation
331 Invalid(SubmitInvalidReason),
332}
333
334impl SubmitOutcome {
335 /// Converts a store submission result into the RPC-visible outcome
336 pub fn from_submit_result(result: SubmitResult) -> std::result::Result<Self, Error> {
337 match result {
338 SubmitResult::New => Ok(SubmitOutcome::New),
339 SubmitResult::Known => Ok(SubmitOutcome::Known),
340 // The store only returns `KnownExpired` for `Network` sources; the RPC path is `Local`.
341 SubmitResult::KnownExpired => {
342 Err(Error::Storage("unexpected KnownExpired on local submission".into()))
343 },
344 SubmitResult::Rejected(reason) => Ok(SubmitOutcome::Rejected(reason.into())),
345 SubmitResult::Invalid(reason) => Ok(SubmitOutcome::Invalid(reason.into())),
346 SubmitResult::InternalError(error) => Err(error),
347 }
348 }
349}
350
351/// An item returned by the statement subscription stream.
352#[derive(Debug, Clone, Eq, PartialEq)]
353#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
354#[cfg_attr(feature = "serde", serde(tag = "event", content = "data", rename_all = "camelCase"))]
355pub enum StatementEvent {
356 /// A batch of statements matching the subscription filter.
357 NewStatements {
358 /// A batch of statements matching the subscription filter, each entry is a SCALE-encoded
359 /// statement.
360 statements: Vec<Bytes>,
361 /// An optional count of how many more matching statements are in the store after this
362 /// batch. This guarantees to the client that it will receive at least this many more
363 /// statements in the subscription stream, but it may receive more if new statements are
364 /// added to the store that match the filter.
365 #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
366 remaining: Option<u32>,
367 },
368}
369
370/// Result type for `Error`
371pub type Result<T> = std::result::Result<T, Error>;
372
373/// Decision returned by the filter used in [`StatementStore::statements_by_hashes`].
374#[derive(Debug, Clone, Copy, PartialEq, Eq)]
375pub enum FilterDecision {
376 /// Skip this statement, continue to next.
377 Skip,
378 /// Take this statement, continue to next.
379 Take,
380 /// Stop iteration, return collected statements.
381 Abort,
382}
383
384/// A batch of statements read from the admission journal by
385/// [`StatementStore::admitted_statements`].
386#[derive(Debug)]
387pub struct AdmittedBatch {
388 /// The taken statements, oldest admission first.
389 pub statements: Vec<(Hash, Statement)>,
390 /// Admission sequence number to resume the walk from.
391 pub cursor: u64,
392 /// Whether the walk reached the watermark; always equals `cursor >= watermark`.
393 pub done: bool,
394}
395
396/// Statement store API.
397pub trait StatementStore: Send + Sync {
398 /// Return all statements.
399 fn statements(&self) -> Result<Vec<(Hash, Statement)>>;
400
401 /// Return recent statements with their admission sequence numbers, oldest admission
402 /// first, and clear the internal index.
403 ///
404 /// This consumes and clears the recently received statements,
405 /// allowing new statements to be collected from this point forward.
406 fn take_recent_statements(&self) -> Result<Vec<(u64, Hash, Statement)>>;
407
408 /// Get statement by hash.
409 fn statement(&self, hash: &Hash) -> Result<Option<Statement>>;
410
411 /// Check if statement exists in the store
412 ///
413 /// Fast index check without accessing the DB.
414 fn has_statement(&self, hash: &Hash) -> bool;
415
416 /// Fetch statements by their hashes with a filter callback.
417 ///
418 /// The callback receives (hash, encoded_bytes, decoded_statement) and returns:
419 /// - `Skip`: ignore this statement, continue to next
420 /// - `Take`: include this statement in the result, continue to next
421 /// - `Abort`: stop iteration, return collected statements so far
422 ///
423 /// Returns (statements, number_of_hashes_processed).
424 fn statements_by_hashes(
425 &self,
426 hashes: &[Hash],
427 filter: &mut dyn FnMut(&Hash, &[u8], &Statement) -> FilterDecision,
428 ) -> Result<(Vec<(Hash, Statement)>, usize)>;
429
430 /// One past the newest assigned admission sequence number.
431 ///
432 /// Every statement currently in the store was admitted below this boundary, so it
433 /// serves as the watermark for a subsequent [`Self::admitted_statements`] walk.
434 fn admission_watermark(&self) -> Result<u64>;
435
436 /// Walk the admission journal from `cursor` (inclusive) towards `watermark`
437 /// (exclusive), collecting statements through a filter callback.
438 ///
439 /// The callback receives (hash, encoded_bytes, decoded_statement) and returns:
440 /// - `Skip`: leave this statement out of the batch, continue to next
441 /// - `Take`: include this statement in the batch, continue to next
442 /// - `Abort`: stop the walk before this statement
443 ///
444 /// Sequence numbers whose statement has left the store are passed over. The returned
445 /// cursor sits after every visited statement except an `Abort`ed one, which the next
446 /// walk revisits first.
447 ///
448 /// A call visits at most `scan_limit` journal entries, passed-over ones included, so
449 /// its cost stays bounded even when the filter takes nothing. `scan_limit` must be
450 /// positive: a walk that visits no entries cannot advance the cursor. A walk stopped
451 /// by the limit returns a partial cursor to resume from, exactly like an `Abort`.
452 fn admitted_statements(
453 &self,
454 cursor: u64,
455 watermark: u64,
456 scan_limit: usize,
457 filter: &mut dyn FnMut(&Hash, &[u8], &Statement) -> FilterDecision,
458 ) -> Result<AdmittedBatch>;
459
460 /// Return the data of all known statements which include all topics and have no `DecryptionKey`
461 /// field.
462 fn broadcasts(&self, match_all_topics: &[Topic]) -> Result<Vec<Vec<u8>>>;
463
464 /// Return the data of all known statements whose decryption key is identified as `dest` (this
465 /// will generally be the public key or a hash thereof for symmetric ciphers, or a hash of the
466 /// private key for symmetric ciphers).
467 fn posted(&self, match_all_topics: &[Topic], dest: [u8; 32]) -> Result<Vec<Vec<u8>>>;
468
469 /// Return the decrypted data of all known statements whose decryption key is identified as
470 /// `dest`. The key must be available to the client.
471 fn posted_clear(&self, match_all_topics: &[Topic], dest: [u8; 32]) -> Result<Vec<Vec<u8>>>;
472
473 /// Return all known statements which include all topics and have no `DecryptionKey`
474 /// field.
475 fn broadcasts_stmt(&self, match_all_topics: &[Topic]) -> Result<Vec<Vec<u8>>>;
476
477 /// Return all known statements whose decryption key is identified as `dest` (this
478 /// will generally be the public key or a hash thereof for symmetric ciphers, or a hash of the
479 /// private key for symmetric ciphers).
480 fn posted_stmt(&self, match_all_topics: &[Topic], dest: [u8; 32]) -> Result<Vec<Vec<u8>>>;
481
482 /// Return the statement and the decrypted data of all known statements whose decryption key is
483 /// identified as `dest`. The key must be available to the client.
484 ///
485 /// The result is for each statement: the SCALE-encoded statement concatenated to the
486 /// decrypted data.
487 fn posted_clear_stmt(&self, match_all_topics: &[Topic], dest: [u8; 32])
488 -> Result<Vec<Vec<u8>>>;
489
490 /// Submit a statement.
491 fn submit(&self, statement: Statement, source: StatementSource) -> SubmitResult;
492
493 /// Remove a statement from the store.
494 fn remove(&self, hash: &Hash) -> Result<()>;
495
496 /// Remove all statements authored by `who`.
497 fn remove_by(&self, who: [u8; 32]) -> Result<()>;
498}