Skip to main content

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