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