1#![allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)]
7
8use codec::{Decode, Encode};
9use scale_info::{form::PortableForm, TypeDef, Variant};
10use sp_core::hashing::blake2_256;
11
12use crate::codec::decode::{compact_u128, Cursor};
13use crate::codec::encode::compact;
14use crate::codec::value::Value;
15use crate::error::CoreError;
16use crate::keys::{is_hashed_crypto, ss58_from_public};
17use crate::runtime::Runtime;
18
19pub struct TxParams {
21 pub era: Value,
24 pub nonce: u64,
25 pub tip: u128,
26 pub tip_asset_id: Option<u128>,
27 pub genesis_hash: [u8; 32],
28 pub era_block_hash: [u8; 32],
29 pub metadata_hash: Option<[u8; 32]>,
31}
32
33const PAYLOAD_FIELDS: &[(&str, &str, Slot)] = &[
40 ("era", "CheckMortality", Slot::Extrinsic),
41 ("era", "CheckEra", Slot::Extrinsic),
42 ("nonce", "CheckNonce", Slot::Extrinsic),
43 ("tip", "ChargeTransactionPayment", Slot::Extrinsic),
44 ("asset_id", "ChargeAssetTxPayment", Slot::Extrinsic),
45 ("mode", "CheckMetadataHash", Slot::Extrinsic),
46 ("spec_version", "CheckSpecVersion", Slot::AdditionalSigned),
47 (
48 "transaction_version",
49 "CheckTxVersion",
50 Slot::AdditionalSigned,
51 ),
52 ("genesis_hash", "CheckGenesis", Slot::AdditionalSigned),
53 ("block_hash", "CheckMortality", Slot::AdditionalSigned),
54 ("block_hash", "CheckEra", Slot::AdditionalSigned),
55 ("metadata_hash", "CheckMetadataHash", Slot::AdditionalSigned),
56];
57
58#[derive(Debug, PartialEq, Clone, Copy)]
59enum Slot {
60 Extrinsic,
61 AdditionalSigned,
62}
63
64impl Runtime {
65 fn variant_fixed_byte_len(&self, variant: &Variant<PortableForm>) -> Option<usize> {
69 let [field] = variant.fields.as_slice() else {
70 return None;
71 };
72 let TypeDef::Array(array) = &self.resolve(field.ty.id).ok()?.type_def else {
73 return None;
74 };
75 matches!(
76 self.resolve(array.type_param.id).ok()?.type_def,
77 TypeDef::Primitive(scale_info::TypeDefPrimitive::U8)
78 )
79 .then_some(array.len as usize)
80 }
81
82 fn encode_payload_section(&self, slot: Slot, params: &TxParams) -> Result<Vec<u8>, CoreError> {
91 let mut out = Vec::new();
92 for ext in &self.extrinsic.signed_extensions {
93 let ty = match slot {
94 Slot::Extrinsic => ext.ty,
95 Slot::AdditionalSigned => ext.additional_signed,
96 };
97 let field = PAYLOAD_FIELDS
98 .iter()
99 .find(|(_, identifier, field_slot)| {
100 *identifier == ext.identifier && *field_slot == slot
101 })
102 .map(|(field, _, _)| *field);
103 match field {
104 Some(field) => {
105 let value = self.payload_field_value(field, ty, params)?;
106 self.encode_id(ty, &value, &mut out)?;
107 }
108 None => {
109 if !self.type_is_zero_sized(ty)? {
110 return Err(CoreError::Codec(format!(
111 "signed extension {} contributes non-zero {slot:?} data with no known encoder",
112 ext.identifier
113 )));
114 }
115 }
116 }
117 }
118 Ok(out)
119 }
120
121 fn type_is_zero_sized(&self, id: u32) -> Result<bool, CoreError> {
125 self.type_is_zero_sized_inner(id, 0)
126 }
127
128 fn type_is_zero_sized_inner(&self, id: u32, depth: usize) -> Result<bool, CoreError> {
129 if depth > 32 {
131 return Ok(false);
132 }
133 Ok(match &self.resolve(id)?.type_def {
134 TypeDef::Composite(c) => {
135 let mut zero_sized = true;
136 for f in &c.fields {
137 if !self.type_is_zero_sized_inner(f.ty.id, depth + 1)? {
138 zero_sized = false;
139 break;
140 }
141 }
142 zero_sized
143 }
144 TypeDef::Tuple(t) => {
145 let mut zero_sized = true;
146 for f in &t.fields {
147 if !self.type_is_zero_sized_inner(f.id, depth + 1)? {
148 zero_sized = false;
149 break;
150 }
151 }
152 zero_sized
153 }
154 TypeDef::Array(a) => {
155 a.len == 0 || self.type_is_zero_sized_inner(a.type_param.id, depth + 1)?
156 }
157 _ => false,
158 })
159 }
160
161 fn payload_field_value(
163 &self,
164 field: &str,
165 ty: u32,
166 params: &TxParams,
167 ) -> Result<Value, CoreError> {
168 Ok(match field {
169 "era" => params.era.clone(),
170 "nonce" => Value::Uint(u128::from(params.nonce)),
171 "tip" => Value::Uint(params.tip),
172 "asset_id" => Value::record(vec![
173 ("tip".into(), Value::Uint(params.tip)),
174 (
175 "asset_id".into(),
176 match params.tip_asset_id {
177 Some(id) => Value::Uint(id),
178 None => Value::Null,
179 },
180 ),
181 ]),
182 "mode" => {
183 let mode = if params.metadata_hash.is_some() {
184 "Enabled"
185 } else {
186 "Disabled"
187 };
188 match &self.resolve(ty)?.type_def {
191 TypeDef::Composite(c) => {
192 let name = c
193 .fields
194 .first()
195 .and_then(|f| f.name.clone())
196 .unwrap_or_else(|| "mode".into());
197 Value::Dict(vec![(Value::Str(name), Value::str(mode))])
198 }
199 _ => Value::str(mode),
200 }
201 }
202 "spec_version" => Value::Uint(u128::from(self.spec_version)),
203 "transaction_version" => Value::Uint(u128::from(self.transaction_version)),
204 "genesis_hash" => Value::Bytes(params.genesis_hash.to_vec()),
205 "block_hash" => Value::Bytes(params.era_block_hash.to_vec()),
206 "metadata_hash" => match params.metadata_hash {
207 Some(hash) => Value::Bytes(hash.to_vec()),
208 None => Value::Null,
209 },
210 other => return Err(CoreError::Codec(format!("unknown payload field {other:?}"))),
211 })
212 }
213
214 pub fn signature_payload_parts(
219 &self,
220 params: &TxParams,
221 ) -> Result<(Vec<u8>, Vec<u8>), CoreError> {
222 if params.metadata_hash.is_some()
223 && !self
224 .extrinsic
225 .signed_extensions
226 .iter()
227 .any(|e| e.identifier == "CheckMetadataHash")
228 {
229 return Err(CoreError::Codec(
230 "this runtime does not declare CheckMetadataHash".into(),
231 ));
232 }
233 Ok((
234 self.encode_payload_section(Slot::Extrinsic, params)?,
235 self.encode_payload_section(Slot::AdditionalSigned, params)?,
236 ))
237 }
238
239 pub fn signature_payload(
242 &self,
243 call_data: &[u8],
244 params: &TxParams,
245 ) -> Result<Vec<u8>, CoreError> {
246 let (extra, additional) = self.signature_payload_parts(params)?;
247 let mut data = call_data.to_vec();
248 data.extend_from_slice(&extra);
249 data.extend_from_slice(&additional);
250 if data.len() > 256 {
251 return Ok(blake2_256(&data).to_vec());
252 }
253 Ok(data)
254 }
255
256 pub fn encode_signed_extrinsic(
262 &self,
263 call_data: &[u8],
264 public_key: [u8; 32],
265 signature: &[u8],
266 signature_version: u8,
267 params: &TxParams,
268 ) -> Result<(Vec<u8>, [u8; 32]), CoreError> {
269 let pq = if crate::keys::is_pq_crypto(signature_version) {
270 let (account, profile, encoded) = if crate::keys::is_falcon_crypto(signature_version) {
271 let address = subtensor_hashed::falcon::SignerAddress::from_bytes(signature)
272 .map_err(|_| {
273 CoreError::Codec("invalid Falcon authorization envelope".into())
274 })?;
275 (address.account, address.algorithm, address.encode())
276 } else {
277 let address = subtensor_hashed::mldsa::SignerAddress::from_bytes(signature)
278 .map_err(|_| {
279 CoreError::Codec("invalid ML-DSA authorization envelope".into())
280 })?;
281 (address.account, address.algorithm, address.encode())
282 };
283 if !self.supports_mldsa_profile(profile) {
284 return Err(CoreError::Codec(
285 "runtime does not advertise this post-quantum account profile".into(),
286 ));
287 }
288 if account != public_key || crate::keys::pq_profile(signature_version) != Some(profile)
289 {
290 return Err(CoreError::Codec(
291 "post-quantum proof account does not match signer".into(),
292 ));
293 }
294 Some(encoded)
295 } else {
296 None
297 };
298 let proof = if is_hashed_crypto(signature_version) {
299 if signature.len() != 136 {
300 return Err(CoreError::Codec(
301 "hashed authorization proof must be 136 bytes".into(),
302 ));
303 }
304 if !self.pallets.iter().any(|p| p.name == "HashedAccounts") {
305 return Err(CoreError::Codec(
306 "runtime does not support hashed accounts".into(),
307 ));
308 }
309 Some(
310 subtensor_hashed::Proof::decode(&mut &signature[..])
311 .map_err(|_| CoreError::Codec("invalid hashed authorization proof".into()))?,
312 )
313 } else {
314 None
315 };
316 let (address, signature, signature_version) = if let Some(address) = &pq {
317 (
318 Value::record(vec![("Raw".into(), Value::Bytes(address.clone()))]),
319 subtensor_hashed::mldsa::CARRIER_SIGNATURE.as_slice(),
320 0,
321 )
322 } else if let Some(proof) = &proof {
323 let scheme = if signature_version == crate::keys::CRYPTO_HASHED {
324 subtensor_hashed::Scheme::Sr25519
325 } else {
326 subtensor_hashed::Scheme::Ed25519
327 };
328 let address = subtensor_hashed::SignerAddress {
329 version: subtensor_hashed::VERSION,
330 scheme,
331 account: public_key,
332 generation: proof.generation,
333 public_key: proof.public_key,
334 next_commitment: proof.next_commitment,
335 };
336 (
337 Value::record(vec![("Raw".into(), Value::Bytes(address.encode()))]),
338 proof.signature.as_slice(),
339 if signature_version == crate::keys::CRYPTO_HASHED {
340 1
341 } else {
342 0
343 },
344 )
345 } else {
346 (
347 Value::Bytes(public_key.to_vec()),
348 signature,
349 signature_version,
350 )
351 };
352 if self.extrinsic.version != 4 {
353 return Err(CoreError::Codec(format!(
354 "extrinsic version {} not supported",
355 self.extrinsic.version
356 )));
357 }
358 let mut body = vec![0x80 | self.extrinsic.version];
359 let address_type = self
360 .extrinsic
361 .address_type
362 .ok_or_else(|| CoreError::Codec("runtime metadata has no address type".into()))?;
363 self.encode_id(address_type, &address, &mut body)?;
364 if let Some(signature_type) = self.extrinsic.signature_type {
370 if let TypeDef::Variant(variant) = &self.resolve(signature_type)?.type_def {
371 let chosen = variant
372 .variants
373 .iter()
374 .find(|v| v.index == signature_version)
375 .ok_or_else(|| {
376 CoreError::Codec(format!(
377 "no signature variant with index {signature_version} in the runtime's signature enum"
378 ))
379 })?;
380 if let Some(expected) = self.variant_fixed_byte_len(chosen) {
381 if signature.len() != expected {
382 return Err(CoreError::Codec(format!(
383 "signature is {} bytes but variant {} expects {expected}",
384 signature.len(),
385 chosen.name
386 )));
387 }
388 }
389 body.push(signature_version);
390 }
391 }
392 body.extend_from_slice(signature);
393 body.extend_from_slice(&self.encode_payload_section(Slot::Extrinsic, params)?);
394 body.extend_from_slice(call_data);
395
396 let mut out = Vec::with_capacity(body.len().saturating_add(4));
397 compact(body.len() as u128, &mut out)?;
398 out.extend_from_slice(&body);
399 Ok((out.clone(), blake2_256(&out)))
400 }
401
402 pub fn supports_mldsa_profile(&self, profile: u8) -> bool {
405 if subtensor_hashed::falcon::is_account_profile(profile) {
406 return self.supports_registration_variant("Falcon")
407 && self
408 .constant("HashedAccounts", "FalconAccountProfiles")
409 .is_some_and(|constant| constant.value.contains(&profile));
410 }
411 self.supports_mldsa_accounts()
412 && match profile {
413 subtensor_hashed::mldsa::ALGORITHM => true,
414 subtensor_hashed::mldsa::FIXED_ALGORITHM => self
415 .constant("HashedAccounts", "MldsaAccountProfiles")
416 .is_some_and(|constant| constant.value.contains(&profile)),
417 _ => false,
418 }
419 }
420
421 pub fn supports_mldsa_accounts(&self) -> bool {
422 self.supports_registration_variant("MlDsa65")
423 }
424
425 fn supports_registration_variant(&self, variant: &str) -> bool {
426 self.pallets
427 .iter()
428 .filter(|p| p.name == "HashedAccounts")
429 .filter_map(|p| p.calls_type)
430 .any(|id| {
431 let Ok(ty) = self.resolve(id) else {
432 return false;
433 };
434 let TypeDef::Variant(calls) = &ty.type_def else {
435 return false;
436 };
437 calls.variants.iter().any(|call| {
438 let [field] = call.fields.as_slice() else {
439 return false;
440 };
441 if call.name != "register" || field.name.as_deref() != Some("descriptor") {
442 return false;
443 }
444 let Ok(descriptor) = self.resolve(field.ty.id) else {
445 return false;
446 };
447 matches!(&descriptor.type_def, TypeDef::Variant(schemes)
448 if schemes.variants.iter().any(|scheme|
449 scheme.name == variant && scheme.fields.len() == 1)
450 && schemes.variants.iter().any(|scheme|
451 scheme.name == "Classical" && scheme.fields.len() == 1))
452 })
453 })
454 }
455
456 pub fn decode_extrinsic(&self, data: &[u8], strict: bool) -> Result<Value, CoreError> {
460 let hash = blake2_256(data);
461 let mut cursor = Cursor::new(data);
462 cursor.strict = strict;
463 let length = compact_u128(&mut cursor)?;
464 let body_len = usize::try_from(length)
468 .map_err(|_| CoreError::Codec("extrinsic length does not fit usize".into()))?;
469 let body = cursor.take(body_len)?;
470 let mut body_cursor = Cursor::new(body);
471 body_cursor.strict = strict;
472
473 let version_byte = body_cursor.byte()?;
474 let signed = version_byte & 0x80 != 0;
475 let general = version_byte == 0x45;
476 let version = version_byte & 0x7f;
477 if signed && version != 4 {
480 return Err(CoreError::Codec(format!(
481 "signed extrinsic version {version} not supported"
482 )));
483 }
484 if !matches!(version_byte, 4 | 5 | 0x84 | 0x45) {
485 return Err(CoreError::Codec(format!(
486 "unsupported extrinsic format {version_byte}"
487 )));
488 }
489 if general && body_cursor.byte()? != 0 {
490 return Err(CoreError::Codec(
491 "unsupported general extension version".into(),
492 ));
493 }
494
495 let mut fields: Vec<(String, Value)> = vec![
496 ("extrinsic_hash".into(), Value::hex(&hash)),
497 (
498 "extrinsic_length".into(),
499 Value::Int(i128::try_from(length).unwrap_or(0)),
500 ),
501 ];
502 if signed {
503 let address_type = self
504 .extrinsic
505 .address_type
506 .ok_or_else(|| CoreError::Codec("runtime metadata has no address type".into()))?;
507 fields.push((
508 "address".into(),
509 self.decode_id(address_type, &mut body_cursor)?,
510 ));
511 let signature_type = self
512 .extrinsic
513 .signature_type
514 .ok_or_else(|| CoreError::Codec("runtime metadata has no signature type".into()))?;
515 fields.push((
516 "signature".into(),
517 self.decode_id(signature_type, &mut body_cursor)?,
518 ));
519 }
520 if signed || general {
521 for ext in &self.extrinsic.signed_extensions {
522 let field = PAYLOAD_FIELDS
523 .iter()
524 .find(|(_, identifier, slot)| {
525 *identifier == ext.identifier && *slot == Slot::Extrinsic
526 })
527 .map(|(field, _, _)| (*field).to_string())
528 .unwrap_or_else(|| ext.identifier.clone());
529 let value = self.decode_id(ext.ty, &mut body_cursor)?;
530 if matches!(&value, Value::Dict(entries) if entries.is_empty()) {
532 continue;
533 }
534 fields.push((field, value));
535 }
536 }
537 let call = self.decode_call_value(&mut body_cursor)?;
538 fields.push(("call".into(), call));
539 if body_cursor.remaining() != 0 {
542 return Err(CoreError::Codec(format!(
543 "{} undecoded bytes remain inside the extrinsic body",
544 body_cursor.remaining()
545 )));
546 }
547 if strict && cursor.remaining() != 0 {
548 return Err(CoreError::Codec(format!(
549 "{} undecoded bytes remain after the extrinsic",
550 cursor.remaining()
551 )));
552 }
553 Ok(Value::record(fields))
554 }
555}
556
557pub fn era_birth(period: u64, current: u64) -> u64 {
559 let calculated = period.next_power_of_two().clamp(4, 1 << 16);
560 let quantize_factor = (calculated >> 12).max(1);
561 let phase = (current % calculated) / quantize_factor * quantize_factor;
562 (current.max(phase) - phase) / calculated * calculated + phase
563}
564
565pub fn multisig_account_id(
571 signatories: &[[u8; 32]],
572 threshold: u16,
573) -> Result<([u8; 32], Vec<[u8; 32]>), CoreError> {
574 if signatories.is_empty() {
575 return Err(CoreError::Codec(
576 "multisig needs at least one signatory".into(),
577 ));
578 }
579 let mut sorted = signatories.to_vec();
580 sorted.sort_unstable();
581 let mut data = b"modlpy/utilisuba".to_vec();
582 compact(sorted.len() as u128, &mut data)?;
583 for signatory in &sorted {
584 data.extend_from_slice(signatory);
585 }
586 data.extend_from_slice(&threshold.to_le_bytes());
587 Ok((blake2_256(&data), sorted))
588}
589
590pub fn safe_multisig_account_id(
593 signatories: &[[u8; 32]],
594 threshold: u16,
595) -> Result<([u8; 32], Vec<[u8; 32]>), CoreError> {
596 let max_signatories = subtensor_hashed::safe::MAX_MULTISIG_SIGNATORIES as usize;
597 if !(2..=max_signatories).contains(&signatories.len()) {
598 return Err(CoreError::Codec(format!(
599 "safe multisig requires 2 to {max_signatories} signatories"
600 )));
601 }
602 if threshold == 0 || usize::from(threshold) > signatories.len() {
603 return Err(CoreError::Codec("invalid multisig threshold".into()));
604 }
605 let mut sorted = signatories.to_vec();
606 sorted.sort_unstable();
607 if sorted.windows(2).any(|pair| pair[0] == pair[1]) {
608 return Err(CoreError::Codec("duplicate multisig signatory".into()));
609 }
610 let account = subtensor_hashed::safe::safe_derived_account(
611 subtensor_hashed::safe::MULTISIG_DOMAIN,
612 &(sorted.as_slice(), threshold),
613 )
614 .ok_or_else(|| CoreError::Codec("safe multisig derivation exhausted its search".into()))?;
615 Ok((account, sorted))
616}
617
618pub fn multisig_ss58(account_id: [u8; 32], ss58_format: u16) -> String {
620 ss58_from_public(account_id, ss58_format)
621}
622
623#[cfg(test)]
624mod safe_multisig_tests {
625 #![allow(clippy::unwrap_used, clippy::expect_used)]
626 use super::{multisig_account_id, safe_multisig_account_id};
627
628 #[test]
629 fn safe_mode_is_order_independent_and_separate_from_legacy() {
630 let members = [[2; 32], [1; 32], [3; 32]];
631 let (account, sorted) = safe_multisig_account_id(&members, 2).expect("valid group");
632 assert_eq!(sorted, vec![[1; 32], [2; 32], [3; 32]]);
633 assert_eq!(
634 safe_multisig_account_id(&sorted, 2)
635 .expect("sorted group")
636 .0,
637 account
638 );
639 assert!(subtensor_hashed::mldsa::rejects_legacy_public_keys(
640 &account
641 ));
642 assert_ne!(
643 multisig_account_id(&members, 2).expect("legacy group").0,
644 account
645 );
646 assert_ne!(
647 safe_multisig_account_id(&members, 1)
648 .expect("other threshold")
649 .0,
650 account
651 );
652 }
653
654 #[test]
655 fn safe_mode_rejects_invalid_member_manifests() {
656 assert!(safe_multisig_account_id(&[], 1).is_err());
657 assert!(safe_multisig_account_id(&[[1; 32]], 1).is_err());
658 assert!(safe_multisig_account_id(&[[1; 32], [1; 32]], 2).is_err());
659 assert!(safe_multisig_account_id(&[[1; 32], [2; 32]], 0).is_err());
660 assert!(safe_multisig_account_id(&[[1; 32], [2; 32]], 3).is_err());
661 }
662
663 #[test]
664 fn safe_mode_enforces_runtime_signer_count_boundaries() {
665 let max = subtensor_hashed::safe::MAX_MULTISIG_SIGNATORIES as usize;
666 let members: Vec<_> = (0..=max)
667 .map(|index| {
668 let mut account = [0u8; 32];
669 account[..8].copy_from_slice(&(index as u64).to_le_bytes());
670 account
671 })
672 .collect();
673 for count in [0, 1, max + 1] {
674 assert!(safe_multisig_account_id(&members[..count], 1).is_err());
675 }
676 for count in [2, max] {
677 for threshold in [1, count as u16] {
678 let (account, sorted) =
679 safe_multisig_account_id(&members[..count], threshold).expect("valid group");
680 assert_eq!(sorted.len(), count);
681 assert!(subtensor_hashed::mldsa::rejects_legacy_public_keys(
682 &account
683 ));
684 assert_eq!(
685 safe_multisig_account_id(&sorted, threshold)
686 .expect("sorted group")
687 .0,
688 account
689 );
690 }
691 }
692 }
693}
694
695#[cfg(test)]
696pub(crate) mod hashed_tests {
697 #![allow(clippy::unwrap_used, clippy::expect_used)]
698 use codec::{Decode, Encode};
699 use frame_metadata::{RuntimeMetadata, RuntimeMetadataPrefixed};
700
701 use super::*;
702 use crate::keys::Keypair;
703 use crate::keys::{CRYPTO_HASHED, CRYPTO_HASHED_ED25519};
704
705 pub(crate) fn runtime(hashed: bool) -> Runtime {
706 let fixture: serde_json::Value =
707 serde_json::from_str(include_str!("../../../python/tests/fixtures/golden.json"))
708 .unwrap();
709 let encoded = hex::decode(
710 fixture["metadata"]["v15_hex"]
711 .as_str()
712 .unwrap()
713 .trim_start_matches("0x"),
714 )
715 .unwrap();
716 let metadata = Option::<Vec<u8>>::decode(&mut &encoded[..])
717 .unwrap()
718 .unwrap();
719 let mut metadata = RuntimeMetadataPrefixed::decode(&mut &metadata[..]).unwrap();
720 if hashed {
721 let RuntimeMetadata::V15(inner) = &mut metadata.1 else {
722 panic!("v15 fixture")
723 };
724 let mut pallet = inner.pallets[0].clone();
727 pallet.name = "HashedAccounts".into();
728 pallet.index = 250;
729 inner.pallets.push(pallet);
730 }
731 Runtime::parse(&metadata.encode(), 1, 1, 42).unwrap()
732 }
733
734 fn params() -> TxParams {
735 TxParams {
736 era: Value::str("00"),
737 nonce: 4,
738 tip: 0,
739 tip_asset_id: None,
740 genesis_hash: [8; 32],
741 era_block_hash: [8; 32],
742 metadata_hash: None,
743 }
744 }
745
746 #[test]
747 fn hashed_v4_roundtrip_has_standard_signature_and_real_call() {
748 use sp_core::Pair;
749 for crypto in [CRYPTO_HASHED, CRYPTO_HASHED_ED25519] {
750 let runtime = runtime(true);
751 let call = runtime
752 .compose_call(
753 "System",
754 "remark",
755 &Value::record(vec![("remark".into(), Value::Bytes(vec![7; 300]))]),
756 )
757 .unwrap();
758 let key = Keypair::from_seed(&[4; 32], crypto)
759 .unwrap()
760 .at_generation(12)
761 .unwrap();
762 let (extra, additional) = runtime.signature_payload_parts(¶ms()).unwrap();
763 let mut payload = call.clone();
764 payload.extend_from_slice(&extra);
765 payload.extend_from_slice(&additional);
766 let proof_bytes = key.sign_hashed(&payload).unwrap();
767 let proof = subtensor_hashed::Proof::decode(&mut proof_bytes.as_slice()).unwrap();
768 let (encoded, _) = runtime
769 .encode_signed_extrinsic(
770 &call,
771 key.public_key_bytes(),
772 &proof_bytes,
773 crypto,
774 ¶ms(),
775 )
776 .unwrap();
777 let address = subtensor_hashed::SignerAddress {
778 version: 1,
779 account: key.public_key_bytes(),
780 scheme: if crypto == CRYPTO_HASHED {
781 subtensor_hashed::Scheme::Sr25519
782 } else {
783 subtensor_hashed::Scheme::Ed25519
784 },
785 generation: proof.generation,
786 public_key: proof.public_key,
787 next_commitment: proof.next_commitment,
788 };
789 let payload = address.signing_hash(&payload);
790 let valid = if crypto == CRYPTO_HASHED {
791 sp_core::sr25519::Pair::verify(
792 &sp_core::sr25519::Signature::from_raw(proof.signature),
793 payload,
794 &sp_core::sr25519::Public::from_raw(proof.public_key),
795 )
796 } else {
797 sp_core::ed25519::Pair::verify(
798 &sp_core::ed25519::Signature::from_raw(proof.signature),
799 payload,
800 &sp_core::ed25519::Public::from_raw(proof.public_key),
801 )
802 };
803 assert!(valid);
804 let mut body = Cursor::new(&encoded);
805 let _ = compact_u128(&mut body).unwrap();
806 assert_eq!(body.byte().unwrap(), 0x84);
807 let decoded = runtime.decode_extrinsic(&encoded, true).unwrap();
808 let Value::Dict(fields) = decoded else {
809 panic!("record")
810 };
811 assert!(fields.iter().any(|(name, _)| name == &Value::str("call")));
813 assert_eq!(body.byte().unwrap(), 2); assert_eq!(compact_u128(&mut body).unwrap(), 106);
815 assert_eq!(body.take(106).unwrap(), address.encode());
816 assert!(encoded.ends_with(&call));
817 let mut malformed = encoded;
818 malformed.push(0);
819 assert!(runtime.decode_extrinsic(&malformed, true).is_err());
820 assert!(runtime
821 .encode_signed_extrinsic(
822 &call,
823 key.public_key_bytes(),
824 &proof_bytes[..64],
825 crypto,
826 ¶ms()
827 )
828 .is_err());
829 }
830 }
831
832 #[test]
833 fn legacy_metadata_cannot_silently_accept_hashed_authorization() {
834 let runtime = runtime(false);
835 assert!(runtime
836 .encode_signed_extrinsic(&[0, 0], [0; 32], &[0; 136], CRYPTO_HASHED, ¶ms())
837 .is_err());
838 }
839
840 pub(crate) fn runtime_mldsa() -> Runtime {
841 let mut runtime = runtime(true);
842 assert!(!runtime.supports_mldsa_accounts());
843 let pallet = runtime
844 .pallets
845 .iter_mut()
846 .find(|p| p.name == "HashedAccounts")
847 .unwrap();
848 let source = pallet.calls_type.unwrap();
849 let mut calls = runtime.types.resolve(source).unwrap().clone();
850 let TypeDef::Variant(variants) = &mut calls.type_def else {
851 panic!("call variants")
852 };
853 let mut marker = variants.variants[0].clone();
856 marker.name = "register".into();
857 marker.index = 0;
858 marker.fields.truncate(1);
859 marker.fields[0].name = Some("descriptor".into());
860 let mut registration = calls.clone();
861 let TypeDef::Variant(schemes) = &mut registration.type_def else {
862 panic!("registration variants")
863 };
864 let mut scheme = marker.clone();
865 scheme.name = "Classical".into();
866 scheme.fields[0].name = None;
867 schemes.variants = vec![scheme.clone()];
868 scheme.name = "MlDsa65".into();
869 scheme.index = 1;
870 schemes.variants.push(scheme);
871 let registration_id = runtime.types.types.len() as u32;
872 runtime.types.types.push(scale_info::PortableType {
873 id: registration_id,
874 ty: registration,
875 });
876 marker.fields[0].ty = registration_id.into();
877 let TypeDef::Variant(variants) = &mut calls.type_def else {
878 panic!("call variants")
879 };
880 variants.variants = vec![marker];
881 let id = runtime.types.types.len() as u32;
882 runtime
883 .types
884 .types
885 .push(scale_info::PortableType { id, ty: calls });
886 pallet.calls_type = Some(id);
887 pallet.constants.push(crate::runtime::ConstantInfo {
888 name: "MldsaAccountProfiles".into(),
889 ty: 0,
890 value: vec![4, 5],
891 docs: vec![],
892 });
893 assert!(runtime.supports_mldsa_accounts());
894 runtime
895 }
896
897 pub(crate) fn runtime_falcon() -> Runtime {
898 let mut runtime = runtime_mldsa();
899 let pallet = runtime
900 .pallets
901 .iter_mut()
902 .find(|p| p.name == "HashedAccounts")
903 .unwrap();
904 let call_id = pallet.calls_type.unwrap();
905 pallet.constants.push(crate::runtime::ConstantInfo {
906 name: "FalconAccountProfiles".into(),
907 ty: 0,
908 value: vec![6, 7, 8, 9],
909 docs: vec![],
910 });
911 let TypeDef::Variant(calls) = &runtime.types.resolve(call_id).unwrap().type_def else {
912 panic!("calls")
913 };
914 let descriptor_id = calls
915 .variants
916 .iter()
917 .find(|call| call.name == "register")
918 .unwrap()
919 .fields[0]
920 .ty
921 .id;
922 let descriptor = &mut runtime.types.types[descriptor_id as usize].ty;
923 let TypeDef::Variant(variants) = &mut descriptor.type_def else {
924 panic!("descriptor")
925 };
926 let mut falcon = variants.variants[1].clone();
927 falcon.name = "Falcon".into();
928 falcon.index = 2;
929 variants.variants.push(falcon);
930 runtime
931 }
932
933 #[test]
934 fn fixed_profile_requires_advertised_runtime_support_before_encoding() {
935 let mut runtime = runtime_mldsa();
936 assert!(runtime.supports_mldsa_profile(4));
937 assert!(runtime.supports_mldsa_profile(5));
938 runtime
939 .pallets
940 .iter_mut()
941 .find(|p| p.name == "HashedAccounts")
942 .unwrap()
943 .constants
944 .retain(|c| c.name != "MldsaAccountProfiles");
945 assert!(runtime.supports_mldsa_profile(4));
946 assert!(!runtime.supports_mldsa_profile(5));
947 let fixed = Keypair::from_seed(&[23; 32], crate::keys::CRYPTO_MLDSA65_FIXED).unwrap();
948 let proof = fixed.sign_mldsa(b"call").unwrap();
949 assert!(runtime
950 .encode_signed_extrinsic(
951 &[0, 0],
952 fixed.public_key_bytes(),
953 &proof,
954 crate::keys::CRYPTO_MLDSA65_FIXED,
955 ¶ms()
956 )
957 .is_err());
958 }
959
960 #[test]
961 fn mldsa_support_requires_registration_enum_not_a_call_name_marker() {
962 let mut runtime = runtime_mldsa();
963 let call_id = runtime
964 .pallets
965 .iter()
966 .find(|pallet| pallet.name == "HashedAccounts")
967 .unwrap()
968 .calls_type
969 .unwrap();
970 let TypeDef::Variant(calls) = &mut runtime.types.types[call_id as usize].ty.type_def else {
971 panic!("call variants")
972 };
973 let descriptor_id = calls.variants[0].fields[0].ty.id;
974 calls.variants[0].name = "register_unsupported".into();
975 assert!(!runtime.supports_mldsa_accounts());
976 let TypeDef::Variant(calls) = &mut runtime.types.types[call_id as usize].ty.type_def else {
977 panic!("call variants")
978 };
979 calls.variants[0].name = "register".into();
980 assert!(runtime.supports_mldsa_accounts());
981 let TypeDef::Variant(schemes) =
982 &mut runtime.types.types[descriptor_id as usize].ty.type_def
983 else {
984 panic!("registration variants")
985 };
986 schemes.variants.retain(|variant| variant.name != "MlDsa65");
987 assert!(!runtime.supports_mldsa_accounts());
988 }
989
990 #[test]
991 fn mldsa_v4_proof_preserves_outer_types_and_call_bytes() {
992 for crypto in [
993 crate::keys::CRYPTO_MLDSA65,
994 crate::keys::CRYPTO_MLDSA65_FIXED,
995 crate::keys::CRYPTO_FALCON512,
996 crate::keys::CRYPTO_FALCON512_HASHED,
997 crate::keys::CRYPTO_FALCON1024,
998 crate::keys::CRYPTO_FALCON1024_HASHED,
999 ] {
1000 let legacy_runtime = runtime(false);
1001 let runtime = runtime_falcon();
1002 let call = runtime
1003 .compose_call(
1004 "System",
1005 "remark",
1006 &Value::record(vec![("remark".into(), Value::Bytes(vec![7; 300]))]),
1007 )
1008 .unwrap();
1009 let wallet = Keypair::from_seed(&[7; 32], crypto)
1010 .unwrap()
1011 .at_generation(if crate::keys::is_rotating_crypto(crypto) {
1012 3
1013 } else {
1014 0
1015 })
1016 .unwrap();
1017 let (extra, additional) = runtime.signature_payload_parts(¶ms()).unwrap();
1018 let payload = [call.clone(), extra.clone(), additional].concat();
1019 let proof = wallet
1020 .sign_mldsa_with_randomness(&payload, &[0; 32])
1021 .unwrap();
1022 if crate::keys::is_falcon_crypto(crypto) {
1023 assert!(subtensor_hashed::falcon::SignerAddress::from_bytes(&proof)
1024 .unwrap()
1025 .verify(&payload));
1026 } else {
1027 assert!(subtensor_hashed::mldsa::SignerAddress::from_bytes(&proof)
1028 .unwrap()
1029 .verify(&payload));
1030 }
1031 let (encoded, hash) = runtime
1032 .encode_signed_extrinsic(
1033 &call,
1034 wallet.public_key_bytes(),
1035 &proof,
1036 crypto,
1037 ¶ms(),
1038 )
1039 .unwrap();
1040 assert_eq!(hash, blake2_256(&encoded));
1041 runtime.decode_extrinsic(&encoded, true).unwrap();
1042 legacy_runtime.decode_extrinsic(&encoded, true).unwrap();
1043 let mut body = Cursor::new(&encoded);
1044 let _ = compact_u128(&mut body).unwrap();
1045 assert_eq!(body.byte().unwrap(), 0x84);
1046 assert_eq!(body.byte().unwrap(), 2);
1047 assert_eq!(compact_u128(&mut body).unwrap(), proof.len() as u128);
1048 assert_eq!(body.take(proof.len()).unwrap(), proof);
1049 assert_eq!(body.byte().unwrap(), 0);
1050 assert_eq!(
1051 body.take(64).unwrap(),
1052 subtensor_hashed::mldsa::CARRIER_SIGNATURE
1053 );
1054 assert_eq!(body.take(extra.len()).unwrap(), extra);
1055 assert_eq!(body.take(call.len()).unwrap(), call);
1056 assert!(runtime
1057 .encode_signed_extrinsic(&call, [0; 32], &proof, crypto, ¶ms())
1058 .is_err());
1059 assert!(legacy_runtime
1060 .encode_signed_extrinsic(
1061 &call,
1062 wallet.public_key_bytes(),
1063 &proof,
1064 crypto,
1065 ¶ms()
1066 )
1067 .is_err());
1068 }
1069 }
1070
1071 #[test]
1072 fn historical_general_zero_decodes_without_hashed_authorization() {
1073 let runtime = runtime(false);
1074 let call = runtime
1075 .compose_call(
1076 "System",
1077 "remark",
1078 &Value::record(vec![("remark".into(), Value::Bytes(vec![]))]),
1079 )
1080 .unwrap();
1081 let (extra, _) = runtime.signature_payload_parts(¶ms()).unwrap();
1082 let mut body = vec![0x45, 0];
1083 body.extend_from_slice(&extra);
1084 body.extend_from_slice(&call);
1085 let mut encoded = Vec::new();
1086 compact(body.len() as u128, &mut encoded).unwrap();
1087 encoded.extend_from_slice(&body);
1088 let decoded = runtime.decode_extrinsic(&encoded, true).unwrap();
1089 let Value::Dict(fields) = decoded else {
1090 panic!("record expected")
1091 };
1092 assert!(!fields
1093 .iter()
1094 .any(|(name, _)| name == &Value::str("signature")));
1095 assert!(fields
1096 .iter()
1097 .any(|(name, value)| name == &Value::str("nonce")
1098 && matches!(value, Value::Uint(4) | Value::Int(4))));
1099 }
1100}