referrerpolicy=no-referrer-when-downgrade

sp_statement_store/
lib.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
18#![cfg_attr(not(feature = "std"), no_std)]
19#![warn(missing_docs)]
20
21//! A crate which contains statement-store primitives.
22
23extern crate alloc;
24
25use alloc::{format, string::String, vec::Vec};
26use codec::{Compact, Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
27use core::{ops::Deref, str::FromStr};
28use scale_info::{build::Fields, Path, Type, TypeInfo};
29use sp_application_crypto::RuntimeAppPublic;
30#[cfg(feature = "std")]
31use sp_core::Pair;
32
33/// Statement topic.
34///
35/// A 32-byte topic identifier that serializes as a hex string (like `sp_core::Bytes`).
36#[derive(
37	Clone,
38	Copy,
39	Debug,
40	Default,
41	PartialEq,
42	Eq,
43	PartialOrd,
44	Ord,
45	Hash,
46	Encode,
47	Decode,
48	DecodeWithMemTracking,
49	MaxEncodedLen,
50	TypeInfo,
51)]
52pub struct Topic(pub [u8; 32]);
53
54#[cfg(feature = "serde")]
55impl serde::Serialize for Topic {
56	fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
57	where
58		S: serde::Serializer,
59	{
60		sp_core::bytes::serialize(&self.0, serializer)
61	}
62}
63
64#[cfg(feature = "serde")]
65impl<'de> serde::Deserialize<'de> for Topic {
66	fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
67	where
68		D: serde::Deserializer<'de>,
69	{
70		let mut arr = [0u8; 32];
71		sp_core::bytes::deserialize_check_len(
72			deserializer,
73			sp_core::bytes::ExpectedLen::Exact(&mut arr[..]),
74		)?;
75		Ok(Topic(arr))
76	}
77}
78
79impl From<[u8; 32]> for Topic {
80	fn from(inner: [u8; 32]) -> Self {
81		Topic(inner)
82	}
83}
84
85impl From<Topic> for [u8; 32] {
86	fn from(topic: Topic) -> Self {
87		topic.0
88	}
89}
90
91impl AsRef<[u8; 32]> for Topic {
92	fn as_ref(&self) -> &[u8; 32] {
93		&self.0
94	}
95}
96
97impl AsRef<[u8]> for Topic {
98	fn as_ref(&self) -> &[u8] {
99		&self.0
100	}
101}
102
103impl Deref for Topic {
104	type Target = [u8; 32];
105
106	fn deref(&self) -> &Self::Target {
107		&self.0
108	}
109}
110
111impl FromStr for Topic {
112	type Err = String;
113
114	/// Parse a topic from exactly 64 hex digits (32 bytes), with an optional `0x` prefix.
115	fn from_str(input: &str) -> core::result::Result<Self, Self::Err> {
116		array_bytes::hex2array(input)
117			.map(Topic)
118			.map_err(|e| format!("invalid topic '{input}': {e:?}"))
119	}
120}
121
122/// Decryption key identifier.
123pub type DecryptionKey = [u8; 32];
124/// Statement hash.
125pub type Hash = [u8; 32];
126/// Block hash.
127pub type BlockHash = [u8; 32];
128/// Account id
129pub type AccountId = [u8; 32];
130/// Identifier of a per-account communication channel, used for message replacement.
131///
132/// A channel is unique per `(account, channel)` pair: a new statement on an existing channel
133/// replaces the previous one from the same account when it has a strictly higher expiry (see
134/// [`Statement::channel`]). The 32 bytes are opaque to the store — it does not prescribe how a
135/// channel id is generated. A statement with no channel is subject only to priority-based eviction.
136pub type Channel = [u8; 32];
137
138/// Total number of topic fields allowed in a statement and in `MatchAll` filters.
139pub const MAX_TOPICS: usize = 4;
140/// `MatchAny` allows to provide a list of topics match against. This is the maximum number of
141/// topics allowed.
142pub const MAX_ANY_TOPICS: usize = 128;
143
144/// Per-account statement allowance: the resource budget an account may consume in the store.
145///
146/// The allowance is enforced on two axes at once — a maximum number of statements
147/// ([`max_count`](Self::max_count)) and a maximum total data size in bytes
148/// ([`max_size`](Self::max_size)). Because the binding constraint is primarily size, an account
149/// may spend its budget as either a few large statements or many small ones, up to whichever
150/// limit it hits first. When a submission would exceed either limit, the account's
151/// lowest-priority statements are evicted to make room.
152///
153/// Allowances are not fixed in this crate: they are held in chain state under
154/// [`STATEMENT_ALLOWANCE_PREFIX`] (keyed by [`statement_allowance_key`]) and granted or revoked by
155/// the runtime via [`increase_allowance_by`] / [`decrease_allowance_by`]; the store reads the
156/// current value with [`get_allowance`] when validating a submission. An account with no allowance
157/// (or a depleted one) cannot store statements.
158#[derive(
159	Clone,
160	Default,
161	PartialEq,
162	Eq,
163	Encode,
164	Decode,
165	DecodeWithMemTracking,
166	Debug,
167	TypeInfo,
168	MaxEncodedLen,
169)]
170pub struct StatementAllowance {
171	/// Maximum number of statements allowed
172	pub max_count: u32,
173	/// Maximum total size of statements in bytes
174	pub max_size: u32,
175}
176
177impl StatementAllowance {
178	/// Create a new statement allowance.
179	pub fn new(max_count: u32, max_size: u32) -> Self {
180		Self { max_count, max_size }
181	}
182
183	/// Saturating addition of statement allowances.
184	pub const fn saturating_add(self, rhs: StatementAllowance) -> StatementAllowance {
185		StatementAllowance {
186			max_count: self.max_count.saturating_add(rhs.max_count),
187			max_size: self.max_size.saturating_add(rhs.max_size),
188		}
189	}
190
191	/// Saturating subtraction of statement allowances.
192	pub const fn saturating_sub(self, rhs: StatementAllowance) -> StatementAllowance {
193		StatementAllowance {
194			max_count: self.max_count.saturating_sub(rhs.max_count),
195			max_size: self.max_size.saturating_sub(rhs.max_size),
196		}
197	}
198
199	/// Returns `true` if the allowance is exhausted on either axis — that is, if `max_count` or
200	/// `max_size` has reached zero.
201	pub fn is_depleted(&self) -> bool {
202		self.max_count == 0 || self.max_size == 0
203	}
204}
205
206/// Storage key prefix for per-account statement allowances.
207pub const STATEMENT_ALLOWANCE_PREFIX: &[u8] = b":statement_allowance:";
208
209/// Constructs a per-account statement allowance storage key.
210///
211/// # Arguments
212/// * `account_id` - Account identifier as byte slice
213///
214/// # Returns
215/// Storage key: `":statement_allowance:" ++ account_id`
216pub fn statement_allowance_key(account_id: impl AsRef<[u8]>) -> Vec<u8> {
217	let mut key = STATEMENT_ALLOWANCE_PREFIX.to_vec();
218	key.extend_from_slice(account_id.as_ref());
219	key
220}
221
222/// Increase the statement allowance by the given amount.
223pub fn increase_allowance_by(account_id: impl AsRef<[u8]>, by: StatementAllowance) {
224	let key = statement_allowance_key(account_id);
225	let mut allowance: StatementAllowance = frame_support::storage::unhashed::get_or_default(&key);
226	allowance = allowance.saturating_add(by);
227	frame_support::storage::unhashed::put(&key, &allowance);
228}
229
230/// Decrease the statement allowance by the given amount.
231pub fn decrease_allowance_by(account_id: impl AsRef<[u8]>, by: StatementAllowance) {
232	let key = statement_allowance_key(account_id);
233	let mut allowance: StatementAllowance = frame_support::storage::unhashed::get_or_default(&key);
234	allowance = allowance.saturating_sub(by);
235	if allowance.is_depleted() {
236		frame_support::storage::unhashed::kill(&key);
237	} else {
238		frame_support::storage::unhashed::put(&key, &allowance);
239	}
240}
241
242/// Get the statement allowance for the given account.
243pub fn get_allowance(account_id: impl AsRef<[u8]>) -> StatementAllowance {
244	let key = statement_allowance_key(account_id);
245	frame_support::storage::unhashed::get_or_default(&key)
246}
247
248pub use event::{
249	AddFilterResponse, LimitReachedResult, LimitReachedTag, NewStatementEntry, SubscribeEvent,
250};
251#[cfg(feature = "std")]
252pub use store_api::{
253	AdmittedBatch, Error, FilterDecision, FilterId, InvalidReason, LiveStatementEvent,
254	OptimizedTopicFilter, RejectionReason, Result, StatementEvent, StatementSource, StatementStore,
255	SubmitInvalidReason, SubmitOutcome, SubmitRejectionReason, SubmitResult, TopicFilter,
256};
257
258#[cfg(feature = "std")]
259mod ecies;
260mod event;
261pub mod runtime_api;
262#[cfg(feature = "std")]
263mod store_api;
264
265mod sr25519 {
266	mod app_sr25519 {
267		use sp_application_crypto::{app_crypto, key_types::STATEMENT, sr25519};
268		app_crypto!(sr25519, STATEMENT);
269	}
270	pub type Public = app_sr25519::Public;
271}
272
273/// Statement-store specific ed25519 crypto primitives.
274pub mod ed25519 {
275	mod app_ed25519 {
276		use sp_application_crypto::{app_crypto, ed25519, key_types::STATEMENT};
277		app_crypto!(ed25519, STATEMENT);
278	}
279	/// Statement-store specific ed25519 public key.
280	pub type Public = app_ed25519::Public;
281	/// Statement-store specific ed25519 key pair.
282	#[cfg(feature = "std")]
283	pub type Pair = app_ed25519::Pair;
284}
285
286mod ecdsa {
287	mod app_ecdsa {
288		use sp_application_crypto::{app_crypto, ecdsa, key_types::STATEMENT};
289		app_crypto!(ecdsa, STATEMENT);
290	}
291	pub type Public = app_ecdsa::Public;
292}
293
294/// Returns blake2-256 hash for the encoded statement.
295#[cfg(feature = "std")]
296pub fn hash_encoded(data: &[u8]) -> [u8; 32] {
297	sp_crypto_hashing::blake2_256(data)
298}
299
300/// Statement proof.
301#[derive(
302	Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo, Debug, Clone, PartialEq, Eq,
303)]
304pub enum Proof {
305	/// Sr25519 Signature.
306	Sr25519 {
307		/// Signature.
308		signature: [u8; 64],
309		/// Public key.
310		signer: [u8; 32],
311	},
312	/// Ed25519 Signature.
313	Ed25519 {
314		/// Signature.
315		signature: [u8; 64],
316		/// Public key.
317		signer: [u8; 32],
318	},
319	/// Secp256k1 Signature.
320	Secp256k1Ecdsa {
321		/// Signature.
322		signature: [u8; 65],
323		/// Public key.
324		signer: [u8; 33],
325	},
326}
327
328impl Proof {
329	/// Return account id for the proof creator.
330	pub fn account_id(&self) -> AccountId {
331		match self {
332			Proof::Sr25519 { signer, .. } => *signer,
333			Proof::Ed25519 { signer, .. } => *signer,
334			Proof::Secp256k1Ecdsa { signer, .. } => {
335				<sp_runtime::traits::BlakeTwo256 as sp_core::Hasher>::hash(signer).into()
336			},
337		}
338	}
339}
340
341/// Statement attributes. Each statement is a list of 0 or more fields. Fields may only appear once
342/// and in the order declared here.
343#[derive(Encode, Decode, TypeInfo, Debug, Clone, PartialEq, Eq)]
344#[repr(u8)]
345pub enum Field {
346	/// Statement proof.
347	AuthenticityProof(Proof) = 0,
348	/// An identifier for the key that `Data` field may be decrypted with.
349	DecryptionKey(DecryptionKey) = 1,
350	/// Expiry of the statement. See [`Statement::expiry`] for details on the format.
351	Expiry(u64) = 2,
352	/// Account channel to use. Only one message per `(account, channel)` pair is allowed.
353	Channel(Channel) = 3,
354	/// First statement topic.
355	Topic1(Topic) = 4,
356	/// Second statement topic.
357	Topic2(Topic) = 5,
358	/// Third statement topic.
359	Topic3(Topic) = 6,
360	/// Fourth statement topic.
361	Topic4(Topic) = 7,
362	/// Additional data.
363	Data(Vec<u8>) = 8,
364}
365
366impl Field {
367	fn discriminant(&self) -> u8 {
368		// This is safe for repr(u8)
369		// see https://doc.rust-lang.org/reference/items/enumerations.html#pointer-casting
370		unsafe { *(self as *const Self as *const u8) }
371	}
372}
373
374/// Statement structure.
375#[derive(DecodeWithMemTracking, Debug, Clone, PartialEq, Eq, Default)]
376pub struct Statement {
377	/// Proof used for authorizing the statement.
378	proof: Option<Proof>,
379	/// An identifier for the key that `Data` field may be decrypted with.
380	#[deprecated(note = "Experimental feature, may be removed/changed in future releases")]
381	decryption_key: Option<DecryptionKey>,
382	/// Used for identifying a distinct communication channel, only a message per channel is
383	/// stored.
384	///
385	/// This can be used to implement message replacement, submitting a new message with a
386	/// different topic/data on the same channel and a greater expiry replaces the previous one.
387	///
388	/// If the new statement data is bigger than the old one, submitting a statement with the same
389	/// channel does not guarantee that **ONLY** the old one will be replaced, as it might not fit
390	/// in the account quota. In that case, other statements from the same account with the lowest
391	/// expiry will be removed.
392	channel: Option<Channel>,
393	/// Message expiry, used for determining which statements to keep.
394	///
395	/// The most significant 32 bits represents the expiration timestamp (in seconds since
396	/// UNIX epoch) after which the statement gets removed. These ensure that statements with a
397	/// higher expiration time have a higher priority.
398	/// The lower 32 bits represents an arbitrary sequence number used to order statements with the
399	/// same expiration time.
400	///
401	/// Higher values indicate a higher priority.
402	/// This is used in two cases:
403	/// 1) When an account exceeds its quota and some statements need to be removed. Statements
404	///    with the lowest `expiry` are removed first.
405	/// 2) When multiple statements are submitted on the same channel, the one with the highest
406	///    expiry replaces the one with the same channel.
407	expiry: u64,
408	/// Number of topics present.
409	num_topics: u8,
410	/// Topics, used for querying and filtering statements.
411	topics: [Topic; MAX_TOPICS],
412	/// Statement data.
413	data: Option<Vec<u8>>,
414}
415
416/// Note: The `TypeInfo` implementation reflects the actual encoding format (`Vec<Field>`)
417/// rather than the struct fields, since `Statement` has custom `Encode`/`Decode` implementations.
418impl TypeInfo for Statement {
419	type Identity = Self;
420
421	fn type_info() -> Type {
422		// Statement encodes as Vec<Field>, so we report the same type info
423		Type::builder()
424			.path(Path::new("Statement", module_path!()))
425			.docs(&["Statement structure"])
426			.composite(Fields::unnamed().field(|f| f.ty::<Vec<Field>>()))
427	}
428}
429
430impl Decode for Statement {
431	fn decode<I: codec::Input>(input: &mut I) -> core::result::Result<Self, codec::Error> {
432		// Encoding matches that of Vec<Field>. Basically this just means accepting that there
433		// will be a prefix of vector length.
434		let num_fields: codec::Compact<u32> = Decode::decode(input)?;
435		let mut tag = 0;
436		let mut statement = Statement::new();
437		for i in 0..num_fields.into() {
438			let field: Field = Decode::decode(input)?;
439			if i > 0 && field.discriminant() <= tag {
440				return Err("Invalid field order or duplicate fields".into());
441			}
442			tag = field.discriminant();
443			match field {
444				Field::AuthenticityProof(p) => statement.set_proof(p),
445				Field::DecryptionKey(key) => statement.set_decryption_key(key),
446				Field::Expiry(p) => statement.set_expiry(p),
447				Field::Channel(c) => statement.set_channel(c),
448				Field::Topic1(t) => statement.set_topic(0, t),
449				Field::Topic2(t) => statement.set_topic(1, t),
450				Field::Topic3(t) => statement.set_topic(2, t),
451				Field::Topic4(t) => statement.set_topic(3, t),
452				Field::Data(data) => statement.set_plain_data(data),
453			}
454		}
455		Ok(statement)
456	}
457}
458
459impl Encode for Statement {
460	fn encode(&self) -> Vec<u8> {
461		self.encoded(false)
462	}
463}
464
465#[derive(Clone, Copy, PartialEq, Eq, Debug)]
466/// Result returned by `Statement::verify_signature`
467pub enum SignatureVerificationResult {
468	/// Signature is valid and matches this account id.
469	Valid(AccountId),
470	/// Signature has failed verification.
471	Invalid,
472	/// No signature in the proof or no proof.
473	NoSignature,
474}
475
476impl Statement {
477	/// Create a new empty statement with no proof.
478	pub fn new() -> Statement {
479		Default::default()
480	}
481
482	/// Create a new statement with a proof.
483	pub fn new_with_proof(proof: Proof) -> Statement {
484		let mut statement = Self::new();
485		statement.set_proof(proof);
486		statement
487	}
488
489	/// Sign with a key that matches given public key in the keystore.
490	///
491	/// Returns `true` if signing worked (private key present etc).
492	///
493	/// NOTE: This can only be called from the runtime.
494	pub fn sign_sr25519_public(&mut self, key: &sr25519::Public) -> bool {
495		let to_sign = self.signature_material();
496		if let Some(signature) = key.sign(&to_sign) {
497			let proof = Proof::Sr25519 {
498				signature: signature.into_inner().into(),
499				signer: key.clone().into_inner().into(),
500			};
501			self.set_proof(proof);
502			true
503		} else {
504			false
505		}
506	}
507
508	/// Returns slice of all topics set in the statement.
509	pub fn topics(&self) -> &[Topic] {
510		&self.topics[..self.num_topics as usize]
511	}
512
513	/// Sign with a given private key and add the signature proof field.
514	#[cfg(feature = "std")]
515	pub fn sign_sr25519_private(&mut self, key: &sp_core::sr25519::Pair) {
516		let to_sign = self.signature_material();
517		let proof =
518			Proof::Sr25519 { signature: key.sign(&to_sign).into(), signer: key.public().into() };
519		self.set_proof(proof);
520	}
521
522	/// Sign with a key that matches given public key in the keystore.
523	///
524	/// Returns `true` if signing worked (private key present etc).
525	///
526	/// NOTE: This can only be called from the runtime.
527	pub fn sign_ed25519_public(&mut self, key: &ed25519::Public) -> bool {
528		let to_sign = self.signature_material();
529		if let Some(signature) = key.sign(&to_sign) {
530			let proof = Proof::Ed25519 {
531				signature: signature.into_inner().into(),
532				signer: key.clone().into_inner().into(),
533			};
534			self.set_proof(proof);
535			true
536		} else {
537			false
538		}
539	}
540
541	/// Sign with a given private key and add the signature proof field.
542	#[cfg(feature = "std")]
543	pub fn sign_ed25519_private(&mut self, key: &sp_core::ed25519::Pair) {
544		let to_sign = self.signature_material();
545		let proof =
546			Proof::Ed25519 { signature: key.sign(&to_sign).into(), signer: key.public().into() };
547		self.set_proof(proof);
548	}
549
550	/// Sign with a key that matches given public key in the keystore.
551	///
552	/// Returns `true` if signing worked (private key present etc).
553	///
554	/// NOTE: This can only be called from the runtime.
555	pub fn sign_ecdsa_public(&mut self, key: &ecdsa::Public) -> bool {
556		let to_sign = self.signature_material();
557		if let Some(signature) = key.sign(&to_sign) {
558			let proof = Proof::Secp256k1Ecdsa {
559				signature: signature.into_inner().into(),
560				signer: key.clone().into_inner().0,
561			};
562			self.set_proof(proof);
563			true
564		} else {
565			false
566		}
567	}
568
569	/// Sign with a given private key and add the signature proof field.
570	#[cfg(feature = "std")]
571	pub fn sign_ecdsa_private(&mut self, key: &sp_core::ecdsa::Pair) {
572		let to_sign = self.signature_material();
573		let proof =
574			Proof::Secp256k1Ecdsa { signature: key.sign(&to_sign).into(), signer: key.public().0 };
575		self.set_proof(proof);
576	}
577
578	/// Verify the proof's signature over the statement's signature material (all fields except the
579	/// proof itself).
580	///
581	/// Returns [`SignatureVerificationResult::NoSignature`] when there is no proof. On success the
582	/// returned account is the signer for sr25519/ed25519, but for ECDSA it is the BLAKE2-256 hash
583	/// of the signer key, not the key itself.
584	pub fn verify_signature(&self) -> SignatureVerificationResult {
585		use sp_runtime::traits::Verify;
586
587		match self.proof() {
588			None => SignatureVerificationResult::NoSignature,
589			Some(Proof::Sr25519 { signature, signer }) => {
590				let to_sign = self.signature_material();
591				let signature = sp_core::sr25519::Signature::from(*signature);
592				let public = sp_core::sr25519::Public::from(*signer);
593				if signature.verify(to_sign.as_slice(), &public) {
594					SignatureVerificationResult::Valid(*signer)
595				} else {
596					SignatureVerificationResult::Invalid
597				}
598			},
599			Some(Proof::Ed25519 { signature, signer }) => {
600				let to_sign = self.signature_material();
601				let signature = sp_core::ed25519::Signature::from(*signature);
602				let public = sp_core::ed25519::Public::from(*signer);
603				if signature.verify(to_sign.as_slice(), &public) {
604					SignatureVerificationResult::Valid(*signer)
605				} else {
606					SignatureVerificationResult::Invalid
607				}
608			},
609			Some(Proof::Secp256k1Ecdsa { signature, signer }) => {
610				let to_sign = self.signature_material();
611				let signature = sp_core::ecdsa::Signature::from(*signature);
612				let public = sp_core::ecdsa::Public::from(*signer);
613				if signature.verify(to_sign.as_slice(), &public) {
614					let sender_hash =
615						<sp_runtime::traits::BlakeTwo256 as sp_core::Hasher>::hash(signer);
616					SignatureVerificationResult::Valid(sender_hash.into())
617				} else {
618					SignatureVerificationResult::Invalid
619				}
620			},
621		}
622	}
623
624	/// The statement's hash: the BLAKE2-256 hash of its SCALE encoding.
625	///
626	/// This is the statement's identity for deduplication and indexing across the store and
627	/// network. It covers the full encoding (including the proof), so changing any field changes
628	/// the hash.
629	#[cfg(feature = "std")]
630	pub fn hash(&self) -> [u8; 32] {
631		self.using_encoded(hash_encoded)
632	}
633
634	/// Returns a topic by topic index.
635	pub fn topic(&self, index: usize) -> Option<Topic> {
636		if index < self.num_topics as usize {
637			Some(self.topics[index])
638		} else {
639			None
640		}
641	}
642
643	/// Returns decryption key if any.
644	#[allow(deprecated)]
645	pub fn decryption_key(&self) -> Option<DecryptionKey> {
646		self.decryption_key
647	}
648
649	/// Consume the statement and return its data field (see [`data`](Self::data)).
650	pub fn into_data(self) -> Option<Vec<u8>> {
651		self.data
652	}
653
654	/// Get a reference to the statement proof, if any.
655	pub fn proof(&self) -> Option<&Proof> {
656		self.proof.as_ref()
657	}
658
659	/// Get proof account id, if any
660	pub fn account_id(&self) -> Option<AccountId> {
661		self.proof.as_ref().map(Proof::account_id)
662	}
663
664	/// Returns the statement's data field, if any. The bytes are plaintext when set via
665	/// [`set_plain_data`](Self::set_plain_data) or ciphertext when set via
666	/// [`encrypt`](Self::encrypt).
667	pub fn data(&self) -> Option<&Vec<u8>> {
668		self.data.as_ref()
669	}
670
671	/// Length in bytes of the statement's data field (`0` if absent).
672	///
673	/// This is the size the per-account quota ([`StatementAllowance::max_size`]) is measured
674	/// against — the data length, not the full SCALE-encoded statement size.
675	pub fn data_len(&self) -> usize {
676		self.data().map_or(0, Vec::len)
677	}
678
679	/// Get channel, if any.
680	pub fn channel(&self) -> Option<Channel> {
681		self.channel
682	}
683
684	/// Get expiry.
685	pub fn expiry(&self) -> u64 {
686		self.expiry
687	}
688
689	/// Get expiration timestamp in seconds.
690	///
691	/// The expiration timestamp in seconds is stored in the most significant 32 bits of the expiry
692	/// field.
693	pub fn get_expiration_timestamp_secs(&self) -> u32 {
694		(self.expiry >> 32) as u32
695	}
696
697	/// Whether the statement has expired at `now_secs`, a unix timestamp in seconds.
698	///
699	/// Expired once `now_secs` reaches the expiration timestamp.
700	pub fn is_expired(&self, now_secs: u64) -> bool {
701		now_secs >= u64::from(self.get_expiration_timestamp_secs())
702	}
703
704	/// Return encoded fields that can be signed to construct or verify a proof
705	fn signature_material(&self) -> Vec<u8> {
706		self.encoded(true)
707	}
708
709	/// Remove the proof of this statement.
710	pub fn remove_proof(&mut self) {
711		self.proof = None;
712	}
713
714	/// Set statement proof. Any existing proof is overwritten.
715	pub fn set_proof(&mut self, proof: Proof) {
716		self.proof = Some(proof)
717	}
718
719	/// Set statement expiry.
720	pub fn set_expiry(&mut self, expiry: u64) {
721		self.expiry = expiry;
722	}
723
724	/// Set statement expiry from its parts. See [`Statement::expiry`] for details on the format.
725	pub fn set_expiry_from_parts(&mut self, expiration_timestamp_secs: u32, sequence_number: u32) {
726		self.expiry = (expiration_timestamp_secs as u64) << 32 | sequence_number as u64;
727	}
728
729	/// Set statement channel.
730	pub fn set_channel(&mut self, channel: Channel) {
731		self.channel = Some(channel)
732	}
733
734	/// Set topic by index. Does nothing if `index` is at or beyond [`MAX_TOPICS`].
735	///
736	/// Grows the statement's topic count so that `index` becomes addressable.
737	pub fn set_topic(&mut self, index: usize, topic: Topic) {
738		if index < MAX_TOPICS {
739			self.topics[index] = topic;
740			self.num_topics = self.num_topics.max(index as u8 + 1);
741		}
742	}
743
744	/// Set decryption key.
745	#[allow(deprecated)]
746	pub fn set_decryption_key(&mut self, key: DecryptionKey) {
747		self.decryption_key = Some(key);
748	}
749
750	/// Set unencrypted statement data.
751	pub fn set_plain_data(&mut self, data: Vec<u8>) {
752		self.data = Some(data)
753	}
754
755	/// Estimate the encoded size for preallocation.
756	///
757	/// Returns a close approximation of the SCALE-encoded size without actually performing the
758	/// encoding. Uses max_encoded_len() for type sizes:
759	/// - Compact length prefix: max_encoded_len() bytes
760	/// - Proof field: 1 (tag) + max_encoded_len()
761	/// - DecryptionKey: 1 (tag) + max_encoded_len()
762	/// - Expiry: 1 (tag) + max_encoded_len()
763	/// - Channel: 1 (tag) + max_encoded_len()
764	/// - Each topic: 1 (tag) + max_encoded_len()
765	/// - Data: 1 (tag) + max_encoded_len() (compact len) + data.len()
766	#[allow(deprecated)]
767	fn estimated_encoded_size(&self, for_signing: bool) -> usize {
768		let proof_size =
769			if !for_signing && self.proof.is_some() { 1 + Proof::max_encoded_len() } else { 0 };
770		let decryption_key_size =
771			if self.decryption_key.is_some() { 1 + DecryptionKey::max_encoded_len() } else { 0 };
772		let expiry_size = 1 + u64::max_encoded_len();
773		let channel_size = if self.channel.is_some() { 1 + Channel::max_encoded_len() } else { 0 };
774		let topics_size = self.num_topics as usize * (1 + Topic::max_encoded_len());
775		let data_size = self
776			.data
777			.as_ref()
778			.map_or(0, |d| 1 + Compact::<u32>::max_encoded_len() + d.len());
779		let compact_prefix_size = if !for_signing { Compact::<u32>::max_encoded_len() } else { 0 };
780
781		compact_prefix_size +
782			proof_size +
783			decryption_key_size +
784			expiry_size +
785			channel_size +
786			topics_size +
787			data_size
788	}
789
790	#[allow(deprecated)]
791	fn encoded(&self, for_signing: bool) -> Vec<u8> {
792		// Encoding matches that of Vec<Field>. Basically this just means accepting that there
793		// will be a prefix of vector length.
794		// Expiry field is always present.
795		let num_fields = if !for_signing && self.proof.is_some() { 2 } else { 1 } +
796			if self.decryption_key.is_some() { 1 } else { 0 } +
797			if self.channel.is_some() { 1 } else { 0 } +
798			if self.data.is_some() { 1 } else { 0 } +
799			self.num_topics as u32;
800
801		let mut output = Vec::with_capacity(self.estimated_encoded_size(for_signing));
802		// When encoding signature payload, the length prefix is omitted.
803		// This is so that the signature for encoded statement can potentially be derived without
804		// needing to re-encode the statement.
805		if !for_signing {
806			let compact_len = codec::Compact::<u32>(num_fields);
807			compact_len.encode_to(&mut output);
808
809			if let Some(proof) = &self.proof {
810				0u8.encode_to(&mut output);
811				proof.encode_to(&mut output);
812			}
813		}
814		if let Some(decryption_key) = &self.decryption_key {
815			1u8.encode_to(&mut output);
816			decryption_key.encode_to(&mut output);
817		}
818
819		2u8.encode_to(&mut output);
820		self.expiry().encode_to(&mut output);
821
822		if let Some(channel) = &self.channel {
823			3u8.encode_to(&mut output);
824			channel.encode_to(&mut output);
825		}
826		for t in 0..self.num_topics {
827			(4u8 + t).encode_to(&mut output);
828			self.topics[t as usize].encode_to(&mut output);
829		}
830		if let Some(data) = &self.data {
831			8u8.encode_to(&mut output);
832			data.encode_to(&mut output);
833		}
834		output
835	}
836
837	/// Encrypt `data` to `key` (ECIES) and store both the ciphertext and the matching decryption
838	/// key on the statement.
839	///
840	/// Note: encryption is experimental (the decryption-key field is deprecated).
841	#[allow(deprecated)]
842	#[cfg(feature = "std")]
843	pub fn encrypt(
844		&mut self,
845		data: &[u8],
846		key: &sp_core::ed25519::Public,
847	) -> core::result::Result<(), ecies::Error> {
848		let encrypted = ecies::encrypt_ed25519(key, data)?;
849		self.data = Some(encrypted);
850		self.decryption_key = Some((*key).into());
851		Ok(())
852	}
853
854	/// Decrypt the statement's data with the given private key (ECIES).
855	///
856	/// Returns `Ok(None)` if the statement has no data; errors if the data was not encrypted to
857	/// this key.
858	#[cfg(feature = "std")]
859	pub fn decrypt_private(
860		&self,
861		key: &sp_core::ed25519::Pair,
862	) -> core::result::Result<Option<Vec<u8>>, ecies::Error> {
863		self.data.as_ref().map(|d| ecies::decrypt_ed25519(key, d)).transpose()
864	}
865}
866
867#[cfg(test)]
868mod test {
869	use crate::{
870		hash_encoded, Field, Proof, SignatureVerificationResult, Statement, Topic, MAX_TOPICS,
871	};
872	use codec::{Decode, Encode, MaxEncodedLen};
873	use core::str::FromStr;
874	use scale_info::{MetaType, TypeInfo};
875	use sp_application_crypto::Pair;
876	use sp_core::sr25519;
877
878	#[test]
879	fn topic_from_str_accepts_either_case_with_optional_prefix() {
880		let expected = Topic([0xAB; 32]);
881		let lower = "ab".repeat(32);
882		let upper = "AB".repeat(32);
883		assert_eq!(Topic::from_str(&lower), Ok(expected));
884		assert_eq!(Topic::from_str(&upper), Ok(expected));
885		assert_eq!(Topic::from_str(&format!("0x{lower}")), Ok(expected));
886		assert_eq!(Topic::from_str(&format!("0x{upper}")), Ok(expected));
887	}
888
889	#[test]
890	fn topic_from_str_rejects_empty_wrong_length_and_bad_hex() {
891		assert!(Topic::from_str("").is_err());
892		assert!(Topic::from_str("0xdead").is_err());
893		assert!(Topic::from_str(&"zz".repeat(32)).is_err());
894	}
895
896	#[test]
897	fn statement_encoding_matches_vec() {
898		let mut statement = Statement::new();
899		assert!(statement.proof().is_none());
900		let proof = Proof::Sr25519 { signature: [42u8; 64], signer: [24u8; 32] };
901
902		let decryption_key = [0xde; 32];
903		let topic1: Topic = [0x01; 32].into();
904		let topic2: Topic = [0x02; 32].into();
905		let data = vec![55, 99];
906		let expiry = 999;
907		let channel = [0xcc; 32];
908
909		statement.set_proof(proof.clone());
910		statement.set_decryption_key(decryption_key);
911		statement.set_expiry(expiry);
912		statement.set_channel(channel);
913		statement.set_topic(0, topic1);
914		statement.set_topic(1, topic2);
915		statement.set_plain_data(data.clone());
916
917		statement.set_topic(5, [0x55; 32].into());
918		assert_eq!(statement.topic(5), None);
919
920		let fields = vec![
921			Field::AuthenticityProof(proof.clone()),
922			Field::DecryptionKey(decryption_key),
923			Field::Expiry(expiry),
924			Field::Channel(channel),
925			Field::Topic1(topic1),
926			Field::Topic2(topic2),
927			Field::Data(data.clone()),
928		];
929
930		let encoded = statement.encode();
931		assert_eq!(statement.hash(), hash_encoded(&encoded));
932		assert_eq!(encoded, fields.encode());
933
934		let decoded = Statement::decode(&mut encoded.as_slice()).unwrap();
935		assert_eq!(decoded, statement);
936	}
937
938	#[test]
939	fn decode_checks_fields() {
940		let topic1: Topic = [0x01; 32].into();
941		let topic2: Topic = [0x02; 32].into();
942		let priority = 999;
943
944		let dup_topic1 = vec![
945			Field::Expiry(priority),
946			Field::Topic1(topic1),
947			Field::Topic1(topic1),
948			Field::Topic2(topic2),
949		]
950		.encode();
951		assert!(Statement::decode(&mut dup_topic1.as_slice()).is_err());
952
953		let topic1_before_expiry =
954			vec![Field::Topic1(topic1), Field::Expiry(priority), Field::Topic2(topic2)].encode();
955		assert!(Statement::decode(&mut topic1_before_expiry.as_slice()).is_err());
956
957		let dup_expiry = vec![Field::Expiry(1), Field::Expiry(2)].encode();
958		assert!(Statement::decode(&mut dup_expiry.as_slice()).is_err());
959
960		let dup_data = vec![Field::Data(vec![1]), Field::Data(vec![2])].encode();
961		assert!(Statement::decode(&mut dup_data.as_slice()).is_err());
962
963		let data_before_expiry = vec![Field::Data(vec![1]), Field::Expiry(42)].encode();
964		assert!(Statement::decode(&mut data_before_expiry.as_slice()).is_err());
965
966		let channel_before_expiry = vec![Field::Channel([0; 32]), Field::Expiry(1)].encode();
967		assert!(Statement::decode(&mut channel_before_expiry.as_slice()).is_err());
968
969		let topic2_before_topic1 =
970			vec![Field::Expiry(1), Field::Topic2(topic1), Field::Topic1(topic2)].encode();
971		assert!(Statement::decode(&mut topic2_before_topic1.as_slice()).is_err());
972	}
973
974	#[test]
975	fn decode_rejects_malformed_bytes() {
976		assert!(Statement::decode(&mut &[][..]).is_err());
977
978		// Take a valid encoded statement and corrupt it in different ways
979		let valid = vec![Field::Expiry(42)].encode();
980		let decoded = Statement::decode(&mut valid.as_slice()).unwrap();
981		assert_eq!(decoded.expiry(), 42);
982
983		// Truncate to just the length prefix
984		assert!(Statement::decode(&mut &valid[..1][..]).is_err());
985
986		// Replace field discriminant with invalid value (Field only has 0..=8)
987		let mut invalid_discriminant = valid.clone();
988		invalid_discriminant[1] = 9;
989		assert!(Statement::decode(&mut invalid_discriminant.as_slice()).is_err());
990
991		invalid_discriminant[1] = 255;
992		assert!(Statement::decode(&mut invalid_discriminant.as_slice()).is_err());
993
994		// Truncate the Expiry payload (need 8 bytes for u64, provide fewer)
995		assert!(Statement::decode(&mut &valid[..5][..]).is_err());
996
997		// Encode a statement with Proof, then corrupt the Proof variant
998		let with_proof = vec![
999			Field::AuthenticityProof(Proof::Sr25519 { signature: [0u8; 64], signer: [0u8; 32] }),
1000			Field::Expiry(42),
1001		]
1002		.encode();
1003		assert!(Statement::decode(&mut with_proof.as_slice()).is_ok());
1004
1005		let mut invalid_proof_variant = with_proof.clone();
1006		invalid_proof_variant[2] = 99;
1007		assert!(Statement::decode(&mut invalid_proof_variant.as_slice()).is_err());
1008
1009		// Truncate the Proof payload
1010		assert!(Statement::decode(&mut &with_proof[..6][..]).is_err());
1011
1012		// Claim more fields than actually present
1013		let mut inflated_count = valid.clone();
1014		inflated_count[0] = 5 << 2; // change field count from 1 to 5
1015		assert!(Statement::decode(&mut inflated_count.as_slice()).is_err());
1016	}
1017
1018	#[test]
1019	fn sign_and_verify() {
1020		let mut statement = Statement::new();
1021		statement.set_plain_data(vec![42]);
1022
1023		let sr25519_kp = sp_core::sr25519::Pair::from_string("//Alice", None).unwrap();
1024		let ed25519_kp = sp_core::ed25519::Pair::from_string("//Alice", None).unwrap();
1025		let secp256k1_kp = sp_core::ecdsa::Pair::from_string("//Alice", None).unwrap();
1026
1027		statement.sign_sr25519_private(&sr25519_kp);
1028		assert_eq!(
1029			statement.verify_signature(),
1030			SignatureVerificationResult::Valid(sr25519_kp.public().0)
1031		);
1032
1033		statement.sign_ed25519_private(&ed25519_kp);
1034		assert_eq!(
1035			statement.verify_signature(),
1036			SignatureVerificationResult::Valid(ed25519_kp.public().0)
1037		);
1038
1039		statement.sign_ecdsa_private(&secp256k1_kp);
1040		assert_eq!(
1041			statement.verify_signature(),
1042			SignatureVerificationResult::Valid(sp_crypto_hashing::blake2_256(
1043				&secp256k1_kp.public().0
1044			))
1045		);
1046
1047		// set an invalid Sr25519 signature
1048		statement.set_proof(Proof::Sr25519 { signature: [0u8; 64], signer: [0u8; 32] });
1049		assert_eq!(statement.verify_signature(), SignatureVerificationResult::Invalid);
1050
1051		// set an invalid Ed25519 signature
1052		statement.set_proof(Proof::Ed25519 { signature: [0xAB; 64], signer: [0xCD; 32] });
1053		assert_eq!(statement.verify_signature(), SignatureVerificationResult::Invalid);
1054
1055		// set an invalid Secp256k1Ecdsa signature
1056		statement.set_proof(Proof::Secp256k1Ecdsa { signature: [0u8; 65], signer: [0u8; 33] });
1057		assert_eq!(statement.verify_signature(), SignatureVerificationResult::Invalid);
1058
1059		statement.remove_proof();
1060		assert_eq!(statement.verify_signature(), SignatureVerificationResult::NoSignature);
1061	}
1062
1063	#[test]
1064	fn encrypt_decrypt() {
1065		let mut statement = Statement::new();
1066		let (pair, _) = sp_core::ed25519::Pair::generate();
1067		let plain = b"test data".to_vec();
1068
1069		// let sr25519_kp = sp_core::sr25519::Pair::from_string("//Alice", None).unwrap();
1070		statement.encrypt(&plain, &pair.public()).unwrap();
1071		assert_ne!(plain.as_slice(), statement.data().unwrap().as_slice());
1072
1073		let decrypted = statement.decrypt_private(&pair).unwrap();
1074		assert_eq!(decrypted, Some(plain));
1075	}
1076
1077	#[test]
1078	fn check_matches() {
1079		let mut statement = Statement::new();
1080		let topic1: Topic = [0x01; 32].into();
1081		let topic2: Topic = [0x02; 32].into();
1082		let topic3: Topic = [0x03; 32].into();
1083
1084		statement.set_topic(0, topic1);
1085		statement.set_topic(1, topic2);
1086
1087		let filter_any = crate::OptimizedTopicFilter::Any;
1088		assert!(filter_any.matches(&statement));
1089
1090		let filter_all =
1091			crate::OptimizedTopicFilter::MatchAll([topic1, topic2].iter().cloned().collect());
1092		assert!(filter_all.matches(&statement));
1093
1094		let filter_all_fail =
1095			crate::OptimizedTopicFilter::MatchAll([topic1, topic3].iter().cloned().collect());
1096		assert!(!filter_all_fail.matches(&statement));
1097
1098		let filter_any_match =
1099			crate::OptimizedTopicFilter::MatchAny([topic2, topic3].iter().cloned().collect());
1100		assert!(filter_any_match.matches(&statement));
1101
1102		let filter_any_fail =
1103			crate::OptimizedTopicFilter::MatchAny([topic3].iter().cloned().collect());
1104		assert!(!filter_any_fail.matches(&statement));
1105	}
1106
1107	#[test]
1108	fn statement_type_info_matches_encoding() {
1109		// Statement has custom Encode/Decode that encodes as Vec<Field>.
1110		// Verify that TypeInfo reflects this by containing a reference to Vec<Field>.
1111		let statement_type = Statement::type_info();
1112		let vec_field_meta = MetaType::new::<Vec<Field>>();
1113
1114		// The Statement type should be a composite with one unnamed field of type Vec<Field>
1115		match statement_type.type_def {
1116			scale_info::TypeDef::Composite(composite) => {
1117				assert_eq!(composite.fields.len(), 1, "Statement should have exactly one field");
1118				let field = &composite.fields[0];
1119				assert!(field.name.is_none(), "Field should be unnamed (newtype pattern)");
1120				assert_eq!(field.ty, vec_field_meta, "Statement's inner type should be Vec<Field>");
1121			},
1122			_ => panic!("Statement TypeInfo should be a Composite"),
1123		}
1124	}
1125
1126	#[test]
1127	fn measure_hash_30_000_statements() {
1128		use std::time::Instant;
1129		const NUM_STATEMENTS: usize = 30_000;
1130		let (keyring, _) = sr25519::Pair::generate();
1131
1132		// Create 2000 statements with varying data
1133		let statements: Vec<Statement> = (0..NUM_STATEMENTS)
1134			.map(|i| {
1135				let mut statement = Statement::new();
1136
1137				statement.set_expiry(i as u64);
1138				statement.set_topic(0, [(i % 256) as u8; 32].into());
1139				statement.set_plain_data(vec![i as u8; 512]);
1140				statement.sign_sr25519_private(&keyring);
1141
1142				statement.sign_sr25519_private(&keyring);
1143				statement
1144			})
1145			.collect();
1146		// Measure time to hash all statements
1147		let start = Instant::now();
1148		let hashes: Vec<[u8; 32]> = statements.iter().map(|s| s.hash()).collect();
1149		let elapsed = start.elapsed();
1150		println!("Time to hash {} statements: {:?}", NUM_STATEMENTS, elapsed);
1151		println!("Average time per statement: {:?}", elapsed / NUM_STATEMENTS as u32);
1152		// Verify hashes are unique
1153		let unique_hashes: std::collections::HashSet<_> = hashes.iter().collect();
1154		assert_eq!(unique_hashes.len(), NUM_STATEMENTS);
1155	}
1156
1157	#[test]
1158	fn estimated_encoded_size_is_sufficient() {
1159		// Allow some overhead due to using max_encoded_len() approximations.
1160		const MAX_ACCEPTED_OVERHEAD: usize = 33;
1161
1162		// Use Secp256k1Ecdsa: with sig=65 + signer=33 bytes, it is the worst-case proof payload
1163		let proof = Proof::Secp256k1Ecdsa { signature: [42u8; 65], signer: [24u8; 33] };
1164		let decryption_key = [0xde; 32];
1165		let data = vec![55; 1000];
1166		let expiry = 999;
1167		let channel = [0xcc; 32];
1168
1169		// Test with all fields populated
1170		let mut statement = Statement::new();
1171		statement.set_proof(proof);
1172		statement.set_decryption_key(decryption_key);
1173		statement.set_expiry(expiry);
1174		statement.set_channel(channel);
1175		for i in 0..MAX_TOPICS {
1176			statement.set_topic(i, [i as u8; 32].into());
1177		}
1178		statement.set_plain_data(data);
1179
1180		let encoded = statement.encode();
1181		let estimated = statement.estimated_encoded_size(false);
1182		assert!(
1183			estimated >= encoded.len(),
1184			"estimated_encoded_size ({}) should be >= actual encoded length ({})",
1185			estimated,
1186			encoded.len()
1187		);
1188		let overhead = estimated - encoded.len();
1189		assert!(
1190			overhead <= MAX_ACCEPTED_OVERHEAD,
1191			"estimated overhead ({}) should be small, estimated: {}, actual: {}",
1192			overhead,
1193			estimated,
1194			encoded.len()
1195		);
1196
1197		// Test for_signing = true (no proof, no compact prefix)
1198		let signing_payload = statement.encoded(true);
1199		let signing_estimated = statement.estimated_encoded_size(true);
1200		assert!(
1201			signing_estimated >= signing_payload.len(),
1202			"estimated_encoded_size for signing ({}) should be >= actual signing payload length ({})",
1203			signing_estimated,
1204			signing_payload.len()
1205		);
1206		let signing_overhead = signing_estimated - signing_payload.len();
1207		assert!(
1208			signing_overhead <= MAX_ACCEPTED_OVERHEAD,
1209			"signing overhead ({}) should be small, estimated: {}, actual: {}",
1210			signing_overhead,
1211			signing_estimated,
1212			signing_payload.len()
1213		);
1214
1215		// Test with minimal statement (empty)
1216		let empty_statement = Statement::new();
1217		let empty_encoded = empty_statement.encode();
1218		let empty_estimated = empty_statement.estimated_encoded_size(false);
1219		assert!(
1220			empty_estimated >= empty_encoded.len(),
1221			"estimated_encoded_size for empty ({}) should be >= actual encoded length ({})",
1222			empty_estimated,
1223			empty_encoded.len()
1224		);
1225		let empty_overhead = empty_estimated - empty_encoded.len();
1226		assert!(
1227			empty_overhead <= MAX_ACCEPTED_OVERHEAD,
1228			"empty overhead ({}) should be minimal, estimated: {}, actual: {}",
1229			empty_overhead,
1230			empty_estimated,
1231			empty_encoded.len()
1232		);
1233	}
1234
1235	// Wire-format regression tests.
1236	//
1237	// `Proof::OnChain` was removed in favour of cryptographic-only proofs.
1238	// These tests pin the SCALE encoding of the surviving variants so that any future reordering,
1239	// renaming, or payload change is caught immediately.
1240
1241	/// Canonical fixture: a `Statement` with three topics, a channel, an expiry,
1242	/// and a 4-byte payload. Used by every wire-format test in this section.
1243	fn populate_canonical_fixture(stmt: &mut Statement) {
1244		stmt.set_topic(0, [0x01; 32].into());
1245		stmt.set_topic(1, [0x02; 32].into());
1246		stmt.set_topic(2, [0x03; 32].into());
1247		stmt.set_channel([0xcc; 32]);
1248		stmt.set_expiry_from_parts(0x7fff_ffff, 0xabcd_1234);
1249		stmt.set_plain_data(vec![0xde, 0xad, 0xbe, 0xef]);
1250	}
1251
1252	/// The "tail" of every canonical fixture: everything after the optional
1253	/// `AuthenticityProof` field. Pulled out so each variant fixture is one
1254	/// short, reviewable block.
1255	fn canonical_tail() -> Vec<u8> {
1256		let mut v = Vec::new();
1257		v.push(0x02); // Field::Expiry discriminant
1258		v.extend_from_slice(&[0x34, 0x12, 0xcd, 0xab, 0xff, 0xff, 0xff, 0x7f]); // u64 LE
1259		v.push(0x03); // Field::Channel discriminant
1260		v.extend_from_slice(&[0xcc; 32]);
1261		v.push(0x04); // Field::Topic1 discriminant
1262		v.extend_from_slice(&[0x01; 32]);
1263		v.push(0x05); // Field::Topic2 discriminant
1264		v.extend_from_slice(&[0x02; 32]);
1265		v.push(0x06); // Field::Topic3 discriminant
1266		v.extend_from_slice(&[0x03; 32]);
1267		v.push(0x08); // Field::Data discriminant
1268		v.push(0x10); // Compact<u32> = 4
1269		v.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef]);
1270		v
1271	}
1272
1273	/// Pinned byte fixture for `Proof::Sr25519` statements.
1274	#[test]
1275	fn wire_format_sr25519_pinned() {
1276		let mut stmt = Statement::new();
1277		populate_canonical_fixture(&mut stmt);
1278		stmt.set_proof(Proof::Sr25519 { signature: [0x11; 64], signer: [0xAA; 32] });
1279
1280		let mut expected = Vec::new();
1281		expected.push(0x1c); // Compact<u32> = 7 fields (7 << 2)
1282		expected.push(0x00); // Field::AuthenticityProof discriminant
1283		expected.push(0x00); // Proof::Sr25519 discriminant
1284		expected.extend_from_slice(&[0x11; 64]); // signature
1285		expected.extend_from_slice(&[0xAA; 32]); // signer
1286		expected.extend(canonical_tail());
1287
1288		assert_eq!(stmt.encode(), expected, "Sr25519 wire format drifted");
1289		assert_eq!(expected.len(), 246);
1290		// Round-trip
1291		assert_eq!(Statement::decode(&mut expected.as_slice()).unwrap(), stmt);
1292	}
1293
1294	/// `Proof::OnChain` byte sequences are rejected on decode.
1295	#[test]
1296	fn wire_format_legacy_onchain_proof_is_rejected() {
1297		// Hand-crafted SCALE: 2 fields = [AuthenticityProof(OnChain { ... }), Expiry].
1298		let mut legacy = Vec::new();
1299		legacy.push(0x08); // Compact<u32> = 2 fields
1300		legacy.push(0x00); // Field::AuthenticityProof discriminant
1301		legacy.push(0x03); // Proof variant discriminant 3 (the old OnChain slot)
1302		legacy.extend_from_slice(&[0xdd; 32]); // who
1303		legacy.extend_from_slice(&[0xee; 32]); // block_hash
1304		legacy.extend_from_slice(&[0xbe, 0xba, 0xfe, 0xca, 0xef, 0xbe, 0xad, 0xde]); // event_index
1305		legacy.push(0x02); // Field::Expiry
1306		legacy.extend_from_slice(&[0x2a, 0, 0, 0, 0, 0, 0, 0]); // 42 as u64 LE
1307
1308		assert!(
1309			Statement::decode(&mut legacy.as_slice()).is_err(),
1310			"legacy OnChain bytes must no longer decode into a Statement",
1311		);
1312
1313		// And the same payload with the discriminant moved into the survivor
1314		// range still works — proving the rejection is specifically about the
1315		// removed slot, not a wholesale break of the codec.
1316		let mut survivor = Vec::new();
1317		survivor.push(0x08);
1318		survivor.push(0x00);
1319		survivor.push(0x00); // Proof::Sr25519 — survivor variant
1320		survivor.extend_from_slice(&[0xdd; 64]);
1321		survivor.extend_from_slice(&[0xee; 32]);
1322		survivor.push(0x02);
1323		survivor.extend_from_slice(&[0x2a, 0, 0, 0, 0, 0, 0, 0]);
1324		assert!(
1325			Statement::decode(&mut survivor.as_slice()).is_ok(),
1326			"surviving variant in the same byte layout must still decode",
1327		);
1328	}
1329
1330	/// `Proof::max_encoded_len()` reflects the three-variant enum.
1331	#[test]
1332	fn proof_max_encoded_len_after_onchain_removal() {
1333		assert_eq!(
1334			Proof::max_encoded_len(),
1335			1 + 65 + 33,
1336			"max_encoded_len must equal Secp256k1Ecdsa's payload + 1-byte discriminant",
1337		);
1338	}
1339}