1#![allow(clippy::arithmetic_side_effects, clippy::indexing_slicing)]
11
12use std::collections::{BTreeMap, HashMap};
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::{Arc, RwLock};
15use std::thread;
16use std::time::{Duration, Instant};
17
18use codec::{Decode, Encode, MaxEncodedLen};
19use reqwest::blocking::Client as HttpClient;
20use serde_json::{json, Value as JsonValue};
21
22use crate::codec::extrinsic::{era_birth, TxParams};
23use crate::codec::value::Value;
24use crate::error::CoreError;
25use crate::keys::{is_hashed_crypto, is_pq_crypto, is_rotating_crypto, Keypair};
26use crate::mlkem;
27use crate::runtime::type_string::TypeSpec;
28use crate::runtime::{Runtime, RuntimeApiMethodInfo, StorageInfo};
29
30const DEFAULT_RPC_TIMEOUT: Duration = Duration::from_secs(30);
31const DEFAULT_RECEIPT_TIMEOUT: Duration = Duration::from_secs(120);
32const DEFAULT_ERA_PERIOD: u64 = 64;
33const SHIELD_ERA_PERIOD: u64 = 8;
35const STORAGE_PAGE_SIZE: u64 = 1_000;
36const RAO_PER_TAO: u128 = 1_000_000_000;
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct BlockHeader {
41 pub hash: String,
42 pub parent_hash: String,
43 pub number: u64,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct SubnetInfo {
49 pub netuid: u16,
50 pub tempo: u16,
51 pub burn_rao: u128,
52 pub neuron_count: u16,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct SwapQuote {
58 pub tao_amount: u128,
59 pub alpha_amount: u128,
60 pub tao_fee: u128,
61 pub alpha_fee: u128,
62 pub tao_slippage: u128,
63 pub alpha_slippage: u128,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct DispatchError {
70 pub pallet: Option<String>,
71 pub name: String,
72 pub docs: Vec<String>,
73 pub semantic_code: String,
74}
75
76impl std::fmt::Display for DispatchError {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 match &self.pallet {
79 Some(pallet) => write!(f, "{pallet}.{}", self.name),
80 None => write!(f, "{}", self.name),
81 }
82 }
83}
84
85#[derive(Debug, Clone)]
87pub struct TxOutcome {
88 pub success: bool,
89 pub extrinsic_hash: String,
90 pub block_hash: Option<String>,
91 pub block_number: Option<u64>,
92 pub extrinsic_index: Option<u32>,
93 pub fee_rao: Option<u128>,
94 pub events: Vec<Value>,
95 pub error: Option<DispatchError>,
96 pub message: String,
97 pub data: BTreeMap<String, Value>,
98}
99
100impl TxOutcome {
101 fn pool_rejection(hash: String, message: String) -> Self {
102 Self {
103 success: false,
104 extrinsic_hash: hash,
105 block_hash: None,
106 block_number: None,
107 extrinsic_index: None,
108 fee_rao: None,
109 events: Vec::new(),
110 error: None,
111 message,
112 data: BTreeMap::new(),
113 }
114 }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120enum InclusionFinalization {
121 Finalized,
122 Reorged,
123}
124
125fn classify_inclusion_finalization(
126 inclusion_hash: &str,
127 canonical_hash: Option<&str>,
128 included_at: u64,
129 finalized_at: u64,
130) -> Option<InclusionFinalization> {
131 match canonical_hash {
132 Some(canonical) if canonical.eq_ignore_ascii_case(inclusion_hash) => {
133 (finalized_at >= included_at).then_some(InclusionFinalization::Finalized)
134 }
135 _ => Some(InclusionFinalization::Reorged),
136 }
137}
138
139pub struct Client {
141 endpoint: String,
142 http: HttpClient,
143 next_id: AtomicU64,
144 runtime: RwLock<Arc<Runtime>>,
145 genesis_hash: [u8; 32],
146 ss58_format: u16,
147}
148
149struct SigningContext {
153 runtime: Arc<Runtime>,
154 params: TxParams,
155 registered: Option<RegisteredSigningAuthority>,
156}
157
158struct RegisteredSigningAuthority {
159 key: Keypair,
160 generation: u64,
161}
162
163impl Client {
164 pub fn connect(endpoint: impl Into<String>) -> Result<Self, CoreError> {
167 let endpoint = http_endpoint(&endpoint.into());
168 let http = HttpClient::builder()
169 .timeout(DEFAULT_RPC_TIMEOUT)
170 .build()
171 .map_err(|error| CoreError::Rpc(format!("cannot build HTTP client: {error}")))?;
172 let bootstrap = RpcBootstrap {
173 endpoint: endpoint.clone(),
174 http: http.clone(),
175 next_id: AtomicU64::new(1),
176 };
177 let ss58_format = bootstrap.ss58_format()?;
178 let version = bootstrap.runtime_version()?;
179 let metadata = bootstrap.metadata()?;
180 let genesis_hash = parse_h256(&bootstrap.block_hash(Some(0))?)?;
181 let runtime = Runtime::parse(
182 &metadata,
183 version.spec_version,
184 version.transaction_version,
185 ss58_format,
186 )?;
187 Ok(Self {
188 endpoint,
189 http,
190 next_id: AtomicU64::new(100),
191 runtime: RwLock::new(Arc::new(runtime)),
192 genesis_hash,
193 ss58_format,
194 })
195 }
196
197 pub fn endpoint(&self) -> &str {
198 &self.endpoint
199 }
200
201 pub fn read_catalog(&self) -> &'static [&'static str] {
206 &[
207 "balance",
208 "existential_deposit",
209 "subnets",
210 "metagraph",
211 "neurons",
212 "subnet_hyperparameters",
213 "stake",
214 "quote_stake",
215 "block_number",
216 "block_time",
217 "lease",
218 "leases",
219 "mev_shield_next_key",
220 "proxies",
221 "multisig",
222 "identity",
223 "subnet_identity",
224 "children",
225 "root_claim_type",
226 "auto_stake",
227 ]
228 }
229
230 pub fn ss58_format(&self) -> u16 {
231 self.ss58_format
232 }
233
234 pub fn genesis_hash(&self) -> [u8; 32] {
235 self.genesis_hash
236 }
237
238 pub fn runtime(&self) -> Result<Arc<Runtime>, CoreError> {
239 self.runtime
240 .read()
241 .map_err(|_| CoreError::Rpc("runtime metadata lock is poisoned".into()))
242 .map(|runtime| Arc::clone(&runtime))
243 }
244
245 pub fn refresh_runtime(&self) -> Result<bool, CoreError> {
248 let version = self.runtime_version()?;
249 let current = self.runtime()?;
250 if current.spec_version == version.spec_version
251 && current.transaction_version == version.transaction_version
252 {
253 return Ok(false);
254 }
255 let metadata = self.metadata()?;
256 let next = Runtime::parse(
257 &metadata,
258 version.spec_version,
259 version.transaction_version,
260 self.ss58_format,
261 )?;
262 let mut guard = self
263 .runtime
264 .write()
265 .map_err(|_| CoreError::Rpc("runtime metadata lock is poisoned".into()))?;
266 *guard = Arc::new(next);
267 Ok(true)
268 }
269
270 pub fn rpc_value(&self, method: &str, params: JsonValue) -> Result<JsonValue, CoreError> {
272 rpc_request(
273 &self.http,
274 &self.endpoint,
275 self.next_id.fetch_add(1, Ordering::Relaxed),
276 method,
277 params,
278 )
279 }
280
281 fn runtime_version(&self) -> Result<RuntimeVersion, CoreError> {
282 parse_runtime_version(self.rpc_value("state_getRuntimeVersion", json!([]))?)
283 }
284
285 fn metadata(&self) -> Result<Vec<u8>, CoreError> {
286 fetch_metadata(|method, params| self.rpc_value(method, params))
287 }
288
289 pub fn block_hash(&self, block: Option<u64>) -> Result<String, CoreError> {
290 let params = block.map_or_else(|| json!([]), |number| json!([number]));
291 let value = self.rpc_value("chain_getBlockHash", params)?;
292 Ok(json_string(&value, "chain_getBlockHash result")?.to_string())
293 }
294
295 fn canonical_block_hash(&self, block: u64) -> Result<Option<String>, CoreError> {
296 let value = self.rpc_value("chain_getBlockHash", json!([block]))?;
297 match value {
298 JsonValue::Null => Ok(None),
299 JsonValue::String(hash) => Ok(Some(hash)),
300 other => Err(CoreError::Rpc(format!(
301 "chain_getBlockHash returned {other}, expected a hash or null"
302 ))),
303 }
304 }
305
306 pub fn finalized_head(&self) -> Result<String, CoreError> {
307 let value = self.rpc_value("chain_getFinalizedHead", json!([]))?;
308 Ok(json_string(&value, "chain_getFinalizedHead result")?.to_string())
309 }
310
311 pub fn header(&self, block_hash: Option<&str>) -> Result<BlockHeader, CoreError> {
312 let params = block_hash.map_or_else(|| json!([]), |hash| json!([hash]));
313 let value = self.rpc_value("chain_getHeader", params)?;
314 let (number, parent_hash) = parse_header_fields(&value)?;
315 let hash = match block_hash {
316 Some(hash) => hash.to_string(),
317 None => self.block_hash(Some(number))?,
318 };
319 Ok(BlockHeader {
320 hash,
321 parent_hash,
322 number,
323 })
324 }
325
326 fn best_header_past(&self, last: Option<u64>) -> Result<Option<BlockHeader>, CoreError> {
329 let value = self.rpc_value("chain_getHeader", json!([]))?;
330 let (number, parent_hash) = parse_header_fields(&value)?;
331 if last.is_some_and(|last| number <= last) {
332 return Ok(None);
333 }
334 let hash = self.block_hash(Some(number))?;
335 Ok(Some(BlockHeader {
336 hash,
337 parent_hash,
338 number,
339 }))
340 }
341
342 pub fn block_number(&self) -> Result<u64, CoreError> {
343 self.header(None).map(|header| header.number)
344 }
345
346 pub fn block_time(&self) -> Result<Duration, CoreError> {
347 let millis = as_u128(&self.constant("Aura", "SlotDuration")?)
348 .ok_or_else(|| CoreError::Codec("Aura.SlotDuration is not an integer".into()))?;
349 let millis = u64::try_from(millis)
350 .map_err(|_| CoreError::Codec("Aura.SlotDuration does not fit u64".into()))?;
351 Ok(Duration::from_millis(millis.max(1)))
352 }
353
354 pub fn blocks(&self, finalized: bool) -> BlockStream<'_> {
362 let poll_interval = (self.block_time().unwrap_or_else(|_| Duration::from_secs(1)) / 4)
363 .clamp(Duration::from_millis(50), Duration::from_secs(3));
364 BlockStream {
365 client: self,
366 finalized,
367 last_number: None,
368 last_finalized_hash: None,
369 poll_interval,
370 }
371 }
372
373 pub fn at(&self, block: Option<u64>) -> Result<Snapshot<'_>, CoreError> {
374 let hash = self.block_hash(block)?;
375 let number = self.header(Some(&hash))?.number;
376 Ok(Snapshot {
377 client: self,
378 block_hash: hash,
379 block_number: number,
380 })
381 }
382
383 pub fn compose_call(
384 &self,
385 pallet: &str,
386 function: &str,
387 params: &Value,
388 ) -> Result<Vec<u8>, CoreError> {
389 self.runtime()?.compose_call(pallet, function, params)
390 }
391
392 pub fn decode_call(&self, data: &[u8]) -> Result<Value, CoreError> {
393 self.runtime()?.decode_spec(&TypeSpec::Call, data, true)
394 }
395
396 pub fn decode_scale(&self, type_name: &str, data: &[u8]) -> Result<Value, CoreError> {
397 let runtime = self.runtime()?;
398 if type_name == "Call" {
399 return runtime.decode_spec(&TypeSpec::Call, data, true);
400 }
401 let type_id = runtime
402 .type_id_of(type_name)
403 .ok_or_else(|| CoreError::NotInRuntime(format!("type {type_name}")))?;
404 runtime.decode_spec(&TypeSpec::Id(type_id), data, true)
405 }
406
407 pub fn constant(&self, pallet: &str, name: &str) -> Result<Value, CoreError> {
408 let runtime = self.runtime()?;
409 let info = runtime
410 .constant(pallet, name)
411 .cloned()
412 .ok_or_else(|| CoreError::NotInRuntime(format!("constant {pallet}.{name}")))?;
413 runtime.decode_spec(&TypeSpec::Id(info.ty), &info.value, true)
414 }
415
416 pub fn query(
417 &self,
418 pallet: &str,
419 storage: &str,
420 params: &[Value],
421 block_hash: Option<&str>,
422 ) -> Result<Value, CoreError> {
423 self.query_with_runtime(
424 self.runtime()?.as_ref(),
425 pallet,
426 storage,
427 params,
428 block_hash,
429 )
430 }
431
432 fn query_with_runtime(
433 &self,
434 runtime: &Runtime,
435 pallet: &str,
436 storage: &str,
437 params: &[Value],
438 block_hash: Option<&str>,
439 ) -> Result<Value, CoreError> {
440 let info = storage_info(runtime, pallet, storage)?;
441 let key = runtime.storage_key(&info, params)?;
442 let raw = self.storage_raw(&key, block_hash)?;
443 decode_storage(runtime, &info, raw.as_deref())
444 }
445
446 pub fn query_batch(
447 &self,
448 pallet: &str,
449 storage: &str,
450 param_sets: &[Vec<Value>],
451 block_hash: Option<&str>,
452 ) -> Result<Vec<Value>, CoreError> {
453 if param_sets.is_empty() {
454 return Ok(Vec::new());
455 }
456 let runtime = self.runtime()?;
457 let info = storage_info(&runtime, pallet, storage)?;
458 let mut keys = Vec::with_capacity(param_sets.len());
459 for params in param_sets {
460 keys.push(runtime.storage_key(&info, params)?);
461 }
462 let values = self.storage_batch_raw(&keys, block_hash)?;
463 values
464 .iter()
465 .map(|raw| decode_storage(&runtime, &info, raw.as_deref()))
466 .collect()
467 }
468
469 pub fn query_map(
470 &self,
471 pallet: &str,
472 storage: &str,
473 fixed_params: &[Value],
474 block_hash: Option<&str>,
475 ) -> Result<Vec<(Value, Value)>, CoreError> {
476 let runtime = self.runtime()?;
477 let info = storage_info(&runtime, pallet, storage)?;
478 if fixed_params.len() > info.key_types.len() {
479 return Err(CoreError::Codec(format!(
480 "{pallet}.{storage} has {} key parts, got {} fixed params",
481 info.key_types.len(),
482 fixed_params.len()
483 )));
484 }
485 let prefix = runtime.storage_key(&info, fixed_params)?;
486 let keys = self.storage_keys_paged(&prefix, block_hash)?;
487 let raws = self.storage_batch_raw(&keys, block_hash)?;
488 let mut rows = Vec::with_capacity(keys.len());
489 for (key, raw) in keys.iter().zip(raws.iter()) {
490 let decoded_key = runtime.decode_storage_key_params(&info, key, fixed_params.len())?;
491 let key_value = match decoded_key.as_slice() {
492 [] => Value::Tuple(Vec::new()),
493 [one] => one.clone(),
494 many => Value::Tuple(many.to_vec()),
495 };
496 rows.push((key_value, decode_storage(&runtime, &info, raw.as_deref())?));
497 }
498 Ok(rows)
499 }
500
501 pub fn runtime_call(
502 &self,
503 api: &str,
504 method: &str,
505 params: &[Value],
506 block_hash: Option<&str>,
507 ) -> Result<Value, CoreError> {
508 let runtime = self.runtime()?;
509 let method_info = runtime_api_method(&runtime, api, method)?.clone();
510 if method_info.inputs.len() != params.len() {
511 return Err(CoreError::Codec(format!(
512 "{api}.{method} expects {} params, got {}",
513 method_info.inputs.len(),
514 params.len()
515 )));
516 }
517 let mut encoded = Vec::new();
518 for (input, value) in method_info.inputs.iter().zip(params.iter()) {
519 runtime.encode_id(input.ty, value, &mut encoded)?;
520 }
521 let mut rpc_params = vec![
522 JsonValue::String(format!("{api}_{method}")),
523 JsonValue::String(hex_prefixed(&encoded)),
524 ];
525 if let Some(hash) = block_hash {
526 rpc_params.push(JsonValue::String(hash.to_string()));
527 }
528 let result = self.rpc_value("state_call", JsonValue::Array(rpc_params))?;
529 let bytes = decode_hex(json_string(&result, "state_call result")?)?;
530 runtime.decode_spec(&TypeSpec::Id(method_info.output), &bytes, true)
531 }
532
533 pub fn account_next_index(&self, address: &str) -> Result<u64, CoreError> {
534 let value = self.rpc_value("system_accountNextIndex", json!([address]))?;
535 json_u64(&value)
536 }
537
538 pub fn sign_extrinsic(
542 &self,
543 call_data: &[u8],
544 signer: &Keypair,
545 nonce: u64,
546 period: Option<u64>,
547 ) -> Result<(Vec<u8>, String), CoreError> {
548 let context = self.prepare_signing(signer, Some(nonce), period, false)?;
549 self.sign_prepared(call_data, signer, &context, 0)
550 }
551
552 fn prepare_signing(
553 &self,
554 signer: &Keypair,
555 requested_nonce: Option<u64>,
556 period: Option<u64>,
557 shielded: bool,
558 ) -> Result<SigningContext, CoreError> {
559 let runtime = self.runtime()?;
560 let mut registered = None;
561 let mut authority_block = None;
562 let stateless = crate::keys::is_fixed_pq_crypto(signer.crypto_type());
563 let nonce = if stateless {
564 if !runtime.supports_mldsa_profile(signer.mldsa_profile()?) {
565 return Err(CoreError::Policy(
566 "runtime does not support registration-free post-quantum accounts".into(),
567 ));
568 }
569 let state_hash = self.finalized_head()?;
570 if self.query_with_runtime(
571 &runtime,
572 "AdminUtils",
573 "HashedAccountsEnabled",
574 &[],
575 Some(&state_hash),
576 )? != Value::Bool(true)
577 {
578 return Err(CoreError::Policy(
579 "post-quantum authorization is not active".into(),
580 ));
581 }
582 authority_block = Some(state_hash);
583 match requested_nonce {
584 Some(nonce) => nonce,
585 None => self.account_next_index(&signer.ss58_address())?,
586 }
587 } else if is_rotating_crypto(signer.crypto_type()) {
588 let mldsa = is_pq_crypto(signer.crypto_type());
589 if mldsa && !runtime.supports_mldsa_profile(signer.mldsa_descriptor()?[1]) {
590 return Err(CoreError::Policy(
591 "runtime does not support post-quantum accounts".into(),
592 ));
593 }
594 let state_hash = if shielded || mldsa {
595 self.finalized_head()?
596 } else {
597 self.block_hash(None)?
598 };
599 let query = |pallet, storage, params: &[Value]| {
600 self.query_with_runtime(&runtime, pallet, storage, params, Some(&state_hash))
601 };
602 if query("AdminUtils", "HashedAccountsEnabled", &[])? != Value::Bool(true) {
603 return Err(CoreError::Policy(
604 "hashed account authorization is not active".into(),
605 ));
606 }
607 let address = [Value::str(signer.ss58_address())];
608 let record = query(
609 "HashedAccounts",
610 if mldsa { "MlDsaAccounts" } else { "Accounts" },
611 &address,
612 )?;
613 if mldsa {
614 let descriptor = field(&record, "descriptor")
615 .ok_or_else(|| CoreError::Codec("post-quantum descriptor missing".into()))?;
616 let expected = signer.mldsa_descriptor()?;
617 if field(descriptor, "version").and_then(as_u128) != Some(u128::from(expected[0]))
618 || field(descriptor, "algorithm").and_then(as_u128)
619 != Some(u128::from(expected[1]))
620 || field(descriptor, "initial_commitment")
621 .and_then(value_bytes)
622 .as_deref()
623 != Some(&expected[2..])
624 {
625 return Err(CoreError::Policy(
626 "post-quantum descriptor does not match this wallet".into(),
627 ));
628 }
629 }
630 let generation = field(&record, "generation")
631 .and_then(as_u128)
632 .and_then(|value| u64::try_from(value).ok())
633 .ok_or_else(|| {
634 CoreError::Policy("hashed account must be registered before use".into())
635 })?;
636 let active = signer.at_generation(generation)?;
637 let commitment = field(&record, "commitment")
638 .and_then(value_bytes)
639 .ok_or_else(|| CoreError::Codec("hashed account commitment missing".into()))?;
640 if commitment != active.authorization_current_commitment()? {
641 return Err(CoreError::Policy(
642 "hashed account commitment does not match this mnemonic".into(),
643 ));
644 }
645 let account = query("System", "Account", &address)?;
648 let confirmed_nonce = field(&account, "nonce")
649 .and_then(as_u128)
650 .and_then(|value| u64::try_from(value).ok())
651 .ok_or_else(|| CoreError::Codec("System.Account.nonce is missing".into()))?;
652 self.ensure_registered_nonce(signer, confirmed_nonce)?;
653 if requested_nonce.is_some_and(|nonce| nonce != confirmed_nonce) {
654 return Err(CoreError::Policy(
655 "hashed accounts must use the current on-chain nonce; refresh and sign again"
656 .into(),
657 ));
658 }
659 registered = Some(RegisteredSigningAuthority {
660 key: active,
661 generation,
662 });
663 authority_block = Some(state_hash);
664 confirmed_nonce
665 } else {
666 match requested_nonce {
667 Some(nonce) => nonce,
668 None => self.account_next_index(&signer.ss58_address())?,
669 }
670 };
671 let params = match authority_block {
672 Some(hash) => {
673 let current = self.header(Some(&hash))?.number;
674 self.transaction_params_at(nonce, period, current)?
675 }
676 None => self.transaction_params(nonce, period)?,
677 };
678 Ok(SigningContext {
679 runtime,
680 params,
681 registered,
682 })
683 }
684
685 fn ensure_registered_nonce(
686 &self,
687 signer: &Keypair,
688 confirmed_nonce: u64,
689 ) -> Result<(), CoreError> {
690 if self.account_next_index(&signer.ss58_address())? != confirmed_nonce {
691 return Err(CoreError::Policy(
692 "hashed account has a pending transaction; wait for finalization before signing again"
693 .into(),
694 ));
695 }
696 Ok(())
697 }
698
699 fn sign_prepared(
702 &self,
703 call_data: &[u8],
704 signer: &Keypair,
705 context: &SigningContext,
706 generation_offset: u64,
707 ) -> Result<(Vec<u8>, String), CoreError> {
708 if generation_offset > 1 {
709 return Err(CoreError::Policy("invalid Shield generation offset".into()));
710 }
711 let params = TxParams {
712 era: context.params.era.clone(),
713 nonce: context
714 .params
715 .nonce
716 .checked_add(generation_offset)
717 .ok_or_else(|| CoreError::Policy("account nonce exhausted".into()))?,
718 tip: context.params.tip,
719 tip_asset_id: context.params.tip_asset_id,
720 genesis_hash: context.params.genesis_hash,
721 era_block_hash: context.params.era_block_hash,
722 metadata_hash: context.params.metadata_hash,
723 };
724 if crate::keys::is_fixed_pq_crypto(signer.crypto_type())
725 && params.nonce >= u64::from(u32::MAX)
726 {
727 return Err(CoreError::Policy("account nonce exhausted".into()));
728 }
729 let runtime = &context.runtime;
730 let signature = if let Some(authority) = &context.registered {
731 self.ensure_registered_nonce(signer, context.params.nonce)?;
735 let generation = authority
736 .generation
737 .checked_add(generation_offset)
738 .ok_or_else(|| CoreError::Policy("hashed signing generation exhausted".into()))?;
739 let selected = authority.key.at_generation(generation)?;
740 let (extra, additional) = runtime.signature_payload_parts(¶ms)?;
741 let mut payload = call_data.to_vec();
742 payload.extend_from_slice(&extra);
743 payload.extend_from_slice(&additional);
744 selected.sign_authorization(&payload)?
745 } else if crate::keys::is_fixed_pq_crypto(signer.crypto_type()) {
746 let (extra, additional) = runtime.signature_payload_parts(¶ms)?;
750 let mut payload = call_data.to_vec();
751 payload.extend_from_slice(&extra);
752 payload.extend_from_slice(&additional);
753 signer.sign_authorization(&payload)?
754 } else {
755 let payload = runtime.signature_payload(call_data, ¶ms)?;
756 signer.sign(&payload)?
757 };
758 let (extrinsic, hash) = runtime.encode_signed_extrinsic(
759 call_data,
760 signer.public_key_bytes(),
761 &signature,
762 signer.crypto_type(),
763 ¶ms,
764 )?;
765 Ok((extrinsic, hex_prefixed(&hash)))
766 }
767
768 fn transaction_params(&self, nonce: u64, period: Option<u64>) -> Result<TxParams, CoreError> {
769 let current = self.block_number()?;
770 self.transaction_params_at(nonce, period, current)
771 }
772
773 fn transaction_params_at(
774 &self,
775 nonce: u64,
776 period: Option<u64>,
777 current: u64,
778 ) -> Result<TxParams, CoreError> {
779 let (era, era_block_hash) = match period {
780 Some(period) if period > 0 => {
781 let birth = era_birth(period, current);
782 let hash = parse_h256(&self.block_hash(Some(birth))?)?;
783 (
784 Value::record(vec![
785 ("period".into(), Value::Uint(u128::from(period))),
786 ("current".into(), Value::Uint(u128::from(current))),
787 ]),
788 hash,
789 )
790 }
791 _ => (Value::str("00"), self.genesis_hash),
792 };
793 Ok(TxParams {
794 era,
795 nonce,
796 tip: 0,
797 tip_asset_id: None,
798 genesis_hash: self.genesis_hash,
799 era_block_hash,
800 metadata_hash: None,
801 })
802 }
803
804 fn payment_info(&self, call_data: &[u8], signer: &Keypair) -> Result<JsonValue, CoreError> {
807 let runtime = self.runtime()?;
808 let nonce = self.account_next_index(&signer.ss58_address())?;
809 let params = self.transaction_params(nonce, Some(DEFAULT_ERA_PERIOD))?;
810 let proof_len = if is_hashed_crypto(signer.crypto_type()) {
811 subtensor_hashed::Proof::max_encoded_len()
812 } else {
813 64
814 };
815 let proof = if crate::keys::is_falcon_crypto(signer.crypto_type()) {
816 let profile = crate::keys::pq_profile(signer.crypto_type())
817 .ok_or_else(|| CoreError::Codec("invalid Falcon crypto type".into()))?;
818 subtensor_hashed::falcon::SignerAddress {
819 account: signer.public_key_bytes(),
820 version: subtensor_hashed::falcon::VERSION,
821 algorithm: profile,
822 generation: 0,
823 public_key: vec![
824 0;
825 subtensor_hashed::falcon::public_key_len(profile).ok_or_else(
826 || CoreError::Codec("invalid Falcon profile".into())
827 )?
828 ],
829 next_commitment: [1; 64],
830 signature: vec![
831 0;
832 subtensor_hashed::falcon::signature_len(profile).ok_or_else(
833 || CoreError::Codec("invalid Falcon profile".into())
834 )?
835 ],
836 }
837 .encode()
838 } else if is_pq_crypto(signer.crypto_type()) {
839 subtensor_hashed::mldsa::SignerAddress {
840 account: signer.public_key_bytes(),
841 version: subtensor_hashed::mldsa::VERSION,
842 algorithm: if signer.crypto_type() == crate::keys::CRYPTO_MLDSA65 {
843 subtensor_hashed::mldsa::ALGORITHM
844 } else {
845 subtensor_hashed::mldsa::FIXED_ALGORITHM
846 },
847 generation: 0,
848 public_key: [0; subtensor_hashed::mldsa::PUBLIC_KEY_LEN],
849 next_commitment: [1; 64],
850 signature: [0; subtensor_hashed::mldsa::SIGNATURE_LEN],
851 }
852 .encode()
853 } else {
854 vec![0; proof_len]
855 };
856 let (extrinsic, _) = runtime.encode_signed_extrinsic(
857 call_data,
858 signer.public_key_bytes(),
859 &proof,
860 signer.crypto_type(),
861 ¶ms,
862 )?;
863 self.rpc_value("payment_queryInfo", json!([hex_prefixed(&extrinsic)]))
864 }
865
866 pub fn estimate_fee(&self, call_data: &[u8], signer: &Keypair) -> Result<u128, CoreError> {
867 let value = self.payment_info(call_data, signer)?;
868 let object = value
869 .as_object()
870 .ok_or_else(|| CoreError::Rpc("payment_queryInfo returned a non-object".into()))?;
871 let fee = object
872 .get("partialFee")
873 .or_else(|| object.get("partial_fee"))
874 .ok_or_else(|| CoreError::Rpc("payment_queryInfo omitted partialFee".into()))?;
875 json_u128(fee)
876 }
877
878 pub fn estimate_weight(&self, call_data: &[u8], signer: &Keypair) -> Result<Value, CoreError> {
879 let value = self.payment_info(call_data, signer)?;
880 let weight = value
881 .as_object()
882 .and_then(|object| object.get("weight"))
883 .ok_or_else(|| CoreError::Rpc("payment_queryInfo omitted weight".into()))?;
884 json_to_value(weight)
885 }
886
887 pub fn submit(
888 &self,
889 call_data: &[u8],
890 signer: &Keypair,
891 nonce: Option<u64>,
892 period: Option<u64>,
893 wait_for_finalization: bool,
894 ) -> Result<TxOutcome, CoreError> {
895 let context = self.prepare_signing(signer, nonce, period, false)?;
896 let (extrinsic, hash) = self.sign_prepared(call_data, signer, &context, 0)?;
897 self.submit_encoded(&extrinsic, hash, wait_for_finalization)
898 }
899
900 pub fn submit_encoded(
901 &self,
902 extrinsic: &[u8],
903 expected_hash: String,
904 wait_for_finalization: bool,
905 ) -> Result<TxOutcome, CoreError> {
906 let start_block = self.block_number()?;
907 let xt_hex = hex_prefixed(extrinsic);
908 let submitted = match self.rpc_value("author_submitExtrinsic", json!([xt_hex])) {
909 Ok(value) => value,
910 Err(error) => {
911 return Ok(TxOutcome::pool_rejection(expected_hash, error.to_string()));
912 }
913 };
914 let submitted_hash = submitted
915 .as_str()
916 .map_or(expected_hash, ToString::to_string);
917 self.wait_for_inclusion(
918 extrinsic,
919 submitted_hash,
920 start_block,
921 wait_for_finalization,
922 DEFAULT_RECEIPT_TIMEOUT,
923 )
924 }
925
926 pub fn submit_shielded(
931 &self,
932 call_data: &[u8],
933 signer: &Keypair,
934 wait_for_finalization: bool,
935 ) -> Result<TxOutcome, CoreError> {
936 let next_key = self.query("MevShield", "NextKey", &[], None)?;
937 let public_key = value_bytes(&next_key)
938 .ok_or_else(|| CoreError::Rpc("MevShield.NextKey is unavailable".into()))?;
939 let context = self.prepare_signing(signer, None, Some(SHIELD_ERA_PERIOD), true)?;
940 let (inner, inner_hash) = self.sign_prepared(call_data, signer, &context, 1)?;
941 let ciphertext = mlkem::seal(&public_key, &inner, true)?;
942 let outer = context.runtime.compose_call(
943 "MevShield",
944 "submit_encrypted",
945 &Value::record(vec![("ciphertext".into(), Value::Bytes(ciphertext))]),
946 )?;
947 let (extrinsic, hash) = self.sign_prepared(&outer, signer, &context, 0)?;
948 let carrier = self.submit_encoded(&extrinsic, hash, wait_for_finalization)?;
949 self.resolve_shielded_inner(
950 &inner,
951 inner_hash,
952 carrier,
953 wait_for_finalization,
954 DEFAULT_RECEIPT_TIMEOUT,
955 )
956 }
957
958 fn resolve_shielded_inner(
959 &self,
960 inner: &[u8],
961 inner_hash: String,
962 carrier: TxOutcome,
963 wait_for_finalization: bool,
964 timeout: Duration,
965 ) -> Result<TxOutcome, CoreError> {
966 if !carrier.success {
967 return Ok(carrier);
968 }
969 let start_block = carrier
970 .block_number
971 .ok_or_else(|| CoreError::Rpc("successful Shield carrier omitted its block".into()))?;
972 let mut result = self.wait_for_inclusion(
975 inner,
976 inner_hash.clone(),
977 start_block,
978 wait_for_finalization,
979 timeout,
980 )?;
981 result.data.insert("shielded".into(), Value::Bool(true));
982 result
983 .data
984 .insert("inner_extrinsic_hash".into(), Value::str(inner_hash));
985 result.data.insert(
986 "carrier_extrinsic_hash".into(),
987 Value::str(carrier.extrinsic_hash),
988 );
989 if let Some(fee) = result.fee_rao {
990 result.data.insert("inner_fee_rao".into(), Value::Uint(fee));
991 }
992 if let Some(fee) = carrier.fee_rao {
993 result
994 .data
995 .insert("carrier_fee_rao".into(), Value::Uint(fee));
996 result.fee_rao = Some(
997 fee.checked_add(result.fee_rao.unwrap_or_default())
998 .ok_or_else(|| CoreError::Codec("Shield total fee exceeds u128".into()))?,
999 );
1000 }
1001 Ok(result)
1002 }
1003
1004 pub fn balance_rao(&self, address: &str) -> Result<u128, CoreError> {
1005 let account = self.query("System", "Account", &[Value::str(address)], None)?;
1006 field(&account, "data")
1007 .and_then(|data| field(data, "free"))
1008 .and_then(as_u128)
1009 .ok_or_else(|| CoreError::Codec("System.Account.data.free is missing".into()))
1010 }
1011
1012 pub fn existential_deposit_rao(&self) -> Result<u128, CoreError> {
1013 as_u128(&self.constant("Balances", "ExistentialDeposit")?)
1014 .ok_or_else(|| CoreError::Codec("ExistentialDeposit is not an integer".into()))
1015 }
1016
1017 pub fn subnets(&self, block_hash: Option<&str>) -> Result<Vec<SubnetInfo>, CoreError> {
1018 let added = self.query_map("SubtensorModule", "NetworksAdded", &[], block_hash)?;
1019 let tempos = value_map_u16(self.query_map("SubtensorModule", "Tempo", &[], block_hash)?);
1020 let burns = value_map_u128(self.query_map("SubtensorModule", "Burn", &[], block_hash)?);
1021 let counts =
1022 value_map_u16(self.query_map("SubtensorModule", "SubnetworkN", &[], block_hash)?);
1023 let mut out = Vec::new();
1024 for (key, value) in added {
1025 if !matches!(value, Value::Bool(true)) {
1026 continue;
1027 }
1028 let Some(netuid) = as_u128(&key).and_then(|n| u16::try_from(n).ok()) else {
1029 continue;
1030 };
1031 out.push(SubnetInfo {
1032 netuid,
1033 tempo: tempos.get(&netuid).copied().unwrap_or(0),
1034 burn_rao: burns.get(&netuid).copied().unwrap_or(0),
1035 neuron_count: counts.get(&netuid).copied().unwrap_or(0),
1036 });
1037 }
1038 out.sort_by_key(|subnet| subnet.netuid);
1039 Ok(out)
1040 }
1041
1042 pub fn metagraph(&self, netuid: u16, block_hash: Option<&str>) -> Result<Value, CoreError> {
1043 self.runtime_call(
1044 "SubnetInfoRuntimeApi",
1045 "get_metagraph",
1046 &[Value::Uint(u128::from(netuid))],
1047 block_hash,
1048 )
1049 }
1050
1051 pub fn neurons(&self, netuid: u16, block_hash: Option<&str>) -> Result<Vec<Value>, CoreError> {
1053 match self.runtime_call(
1054 "NeuronInfoRuntimeApi",
1055 "get_neurons_lite",
1056 &[Value::Uint(u128::from(netuid))],
1057 block_hash,
1058 )? {
1059 Value::List(neurons) => Ok(neurons),
1060 other => Err(CoreError::Codec(format!(
1061 "NeuronInfoRuntimeApi.get_neurons_lite returned {other:?}, expected a list"
1062 ))),
1063 }
1064 }
1065
1066 pub fn subnet_hyperparameters(
1067 &self,
1068 netuid: u16,
1069 block_hash: Option<&str>,
1070 ) -> Result<Value, CoreError> {
1071 let entries = self.runtime_call(
1072 "SubnetInfoRuntimeApi",
1073 "get_subnet_hyperparams_v3",
1074 &[Value::Uint(u128::from(netuid))],
1075 block_hash,
1076 )?;
1077 flatten_subnet_hyperparams_v3(entries)
1078 }
1079
1080 pub fn stake_rao(
1081 &self,
1082 coldkey: &str,
1083 hotkey: &str,
1084 netuid: u16,
1085 block_hash: Option<&str>,
1086 ) -> Result<u128, CoreError> {
1087 let info = self.runtime_call(
1088 "StakeInfoRuntimeApi",
1089 "get_stake_info_for_hotkey_coldkey_netuid",
1090 &[
1091 Value::str(hotkey),
1092 Value::str(coldkey),
1093 Value::Uint(u128::from(netuid)),
1094 ],
1095 block_hash,
1096 )?;
1097 if matches!(info, Value::Null) {
1098 return Ok(0);
1099 }
1100 field(&info, "stake")
1101 .and_then(as_u128)
1102 .ok_or_else(|| CoreError::Codec("stake runtime API omitted stake".into()))
1103 }
1104
1105 pub fn quote_stake(
1106 &self,
1107 netuid: u16,
1108 amount_rao: u128,
1109 block_hash: Option<&str>,
1110 ) -> Result<SwapQuote, CoreError> {
1111 let quote = self.runtime_call(
1112 "SwapRuntimeApi",
1113 "sim_swap_tao_for_alpha",
1114 &[Value::Uint(u128::from(netuid)), Value::Uint(amount_rao)],
1115 block_hash,
1116 )?;
1117 Ok(SwapQuote {
1118 tao_amount: required_u128("e, "tao_amount")?,
1119 alpha_amount: required_u128("e, "alpha_amount")?,
1120 tao_fee: required_u128("e, "tao_fee")?,
1121 alpha_fee: required_u128("e, "alpha_fee")?,
1122 tao_slippage: required_u128("e, "tao_slippage")?,
1123 alpha_slippage: required_u128("e, "alpha_slippage")?,
1124 })
1125 }
1126
1127 pub fn tao(value: u128) -> Result<u128, CoreError> {
1128 value
1129 .checked_mul(RAO_PER_TAO)
1130 .ok_or_else(|| CoreError::Codec("TAO amount overflows u128".into()))
1131 }
1132
1133 fn storage_raw(
1134 &self,
1135 key: &[u8],
1136 block_hash: Option<&str>,
1137 ) -> Result<Option<String>, CoreError> {
1138 let mut params = vec![JsonValue::String(hex_prefixed(key))];
1139 if let Some(hash) = block_hash {
1140 params.push(JsonValue::String(hash.to_string()));
1141 }
1142 let value = self.rpc_value("state_getStorage", JsonValue::Array(params))?;
1143 match value {
1144 JsonValue::Null => Ok(None),
1145 JsonValue::String(raw) => Ok(Some(raw)),
1146 _ => Err(CoreError::Rpc(
1147 "state_getStorage returned neither hex nor null".into(),
1148 )),
1149 }
1150 }
1151
1152 fn storage_keys_paged(
1153 &self,
1154 prefix: &[u8],
1155 block_hash: Option<&str>,
1156 ) -> Result<Vec<Vec<u8>>, CoreError> {
1157 let prefix_hex = hex_prefixed(prefix);
1158 let mut all = Vec::new();
1159 let mut start: Option<String> = None;
1160 loop {
1161 let mut params = vec![
1162 JsonValue::String(prefix_hex.clone()),
1163 JsonValue::from(STORAGE_PAGE_SIZE),
1164 start
1165 .as_ref()
1166 .map_or(JsonValue::Null, |key| JsonValue::String(key.clone())),
1167 ];
1168 if let Some(hash) = block_hash {
1169 params.push(JsonValue::String(hash.to_string()));
1170 }
1171 let value = self.rpc_value("state_getKeysPaged", JsonValue::Array(params))?;
1172 let page = value
1173 .as_array()
1174 .ok_or_else(|| CoreError::Rpc("state_getKeysPaged returned a non-array".into()))?;
1175 if page.is_empty() {
1176 break;
1177 }
1178 let mut last = None;
1179 for item in page {
1180 let key = json_string(item, "storage key")?.to_string();
1181 all.push(decode_hex(&key)?);
1182 last = Some(key);
1183 }
1184 if page.len() < usize::try_from(STORAGE_PAGE_SIZE).unwrap_or(usize::MAX) {
1185 break;
1186 }
1187 if last == start {
1188 return Err(CoreError::Rpc(
1189 "state_getKeysPaged repeated its cursor".into(),
1190 ));
1191 }
1192 start = last;
1193 }
1194 Ok(all)
1195 }
1196
1197 fn storage_batch_raw(
1198 &self,
1199 keys: &[Vec<u8>],
1200 block_hash: Option<&str>,
1201 ) -> Result<Vec<Option<String>>, CoreError> {
1202 if keys.is_empty() {
1203 return Ok(Vec::new());
1204 }
1205 let key_hex: Vec<String> = keys.iter().map(|key| hex_prefixed(key)).collect();
1206 let mut params = vec![json!(key_hex)];
1207 if let Some(hash) = block_hash {
1208 params.push(JsonValue::String(hash.to_string()));
1209 }
1210 let value = self.rpc_value("state_queryStorageAt", JsonValue::Array(params))?;
1211 let sets = value
1212 .as_array()
1213 .ok_or_else(|| CoreError::Rpc("state_queryStorageAt returned a non-array".into()))?;
1214 let changes = sets
1215 .first()
1216 .and_then(JsonValue::as_object)
1217 .and_then(|set| set.get("changes"))
1218 .and_then(JsonValue::as_array)
1219 .ok_or_else(|| CoreError::Rpc("state_queryStorageAt omitted changes".into()))?;
1220 let mut by_key: HashMap<String, Option<String>> = HashMap::new();
1221 for change in changes {
1222 let pair = change
1223 .as_array()
1224 .ok_or_else(|| CoreError::Rpc("storage change is not a pair".into()))?;
1225 let key = pair
1226 .first()
1227 .and_then(JsonValue::as_str)
1228 .ok_or_else(|| CoreError::Rpc("storage change has no key".into()))?;
1229 let raw = pair
1230 .get(1)
1231 .and_then(JsonValue::as_str)
1232 .map(ToString::to_string);
1233 by_key.insert(key.to_ascii_lowercase(), raw);
1234 }
1235 Ok(keys
1236 .iter()
1237 .map(|key| {
1238 by_key
1239 .remove(&hex_prefixed(key).to_ascii_lowercase())
1240 .flatten()
1241 })
1242 .collect())
1243 }
1244
1245 fn wait_for_inclusion(
1246 &self,
1247 extrinsic: &[u8],
1248 hash: String,
1249 start_block: u64,
1250 wait_for_finalization: bool,
1251 timeout: Duration,
1252 ) -> Result<TxOutcome, CoreError> {
1253 let deadline = Instant::now() + timeout;
1254 let xt_hex = hex_prefixed(extrinsic).to_ascii_lowercase();
1255 let mut next_block = start_block;
1256 let poll = self
1257 .block_time()
1258 .unwrap_or_else(|_| Duration::from_millis(250))
1259 .min(Duration::from_secs(1));
1260 'track_inclusion: while Instant::now() < deadline {
1261 let head = self.block_number()?;
1262 while next_block <= head {
1263 let included_at = next_block;
1264 let block_hash = self.block_hash(Some(included_at))?;
1265 if let Some(index) = self.find_extrinsic(&block_hash, &xt_hex)? {
1266 if wait_for_finalization {
1267 match self.wait_until_finalized(&block_hash, included_at, deadline, poll)? {
1268 InclusionFinalization::Finalized => {}
1269 InclusionFinalization::Reorged => {
1270 next_block = included_at;
1274 continue 'track_inclusion;
1275 }
1276 }
1277 }
1278 return self.decode_outcome(hash, block_hash, included_at, index);
1279 }
1280 next_block = next_block.saturating_add(1);
1281 }
1282 thread::sleep(poll);
1283 }
1284 Ok(TxOutcome::pool_rejection(
1285 hash,
1286 format!("extrinsic was not included within {}s", timeout.as_secs()),
1287 ))
1288 }
1289
1290 fn find_extrinsic(&self, block_hash: &str, wanted: &str) -> Result<Option<u32>, CoreError> {
1291 let block = self.rpc_value("chain_getBlock", json!([block_hash]))?;
1292 let extrinsics = block
1293 .as_object()
1294 .and_then(|object| object.get("block"))
1295 .and_then(JsonValue::as_object)
1296 .and_then(|inner| inner.get("extrinsics"))
1297 .and_then(JsonValue::as_array)
1298 .ok_or_else(|| CoreError::Rpc("chain_getBlock omitted extrinsics".into()))?;
1299 for (index, extrinsic) in extrinsics.iter().enumerate() {
1300 if extrinsic
1301 .as_str()
1302 .is_some_and(|raw| raw.eq_ignore_ascii_case(wanted))
1303 {
1304 return u32::try_from(index)
1305 .map(Some)
1306 .map_err(|_| CoreError::Rpc("extrinsic index exceeds u32".into()));
1307 }
1308 }
1309 Ok(None)
1310 }
1311
1312 fn wait_until_finalized(
1313 &self,
1314 inclusion_hash: &str,
1315 included_at: u64,
1316 deadline: Instant,
1317 poll: Duration,
1318 ) -> Result<InclusionFinalization, CoreError> {
1319 while Instant::now() < deadline {
1320 let finalized = self.finalized_head()?;
1324 let finalized_at = self.header(Some(&finalized))?.number;
1325 let canonical_hash = self.canonical_block_hash(included_at)?;
1326 if let Some(status) = classify_inclusion_finalization(
1327 inclusion_hash,
1328 canonical_hash.as_deref(),
1329 included_at,
1330 finalized_at,
1331 ) {
1332 return Ok(status);
1333 }
1334 thread::sleep(poll);
1335 }
1336 Err(CoreError::Rpc(format!(
1337 "block {included_at} ({inclusion_hash}) was included but not finalized before the receipt timeout"
1338 )))
1339 }
1340
1341 fn decode_outcome(
1342 &self,
1343 extrinsic_hash: String,
1344 block_hash: String,
1345 block_number: u64,
1346 extrinsic_index: u32,
1347 ) -> Result<TxOutcome, CoreError> {
1348 let records = self.query("System", "Events", &[], Some(&block_hash))?;
1349 let mut events = Vec::new();
1350 if let Value::List(items) = records {
1351 for item in items {
1352 if field(&item, "extrinsic_idx")
1353 .and_then(as_u128)
1354 .is_some_and(|index| index == u128::from(extrinsic_index))
1355 {
1356 events.push(item);
1357 }
1358 }
1359 }
1360 let runtime = self.runtime()?;
1361 let mut success = events
1362 .iter()
1363 .any(|event| event_is(event, "System", "ExtrinsicSuccess"));
1364 let mut error = events
1365 .iter()
1366 .find(|event| event_is(event, "System", "ExtrinsicFailed"))
1367 .and_then(event_attributes)
1368 .and_then(|attributes| field(attributes, "dispatch_error").or(Some(attributes)))
1369 .map(|value| dispatch_error(&runtime, value))
1370 .transpose()?;
1371
1372 if error.is_none() {
1373 for (module, event_name) in [
1374 ("Proxy", "ProxyExecuted"),
1375 ("Sudo", "Sudid"),
1376 ("Multisig", "MultisigExecuted"),
1377 ] {
1378 let Some(attributes) = events
1379 .iter()
1380 .find(|event| event_is(event, module, event_name))
1381 .and_then(event_attributes)
1382 else {
1383 continue;
1384 };
1385 let result = field(attributes, "result").unwrap_or(attributes);
1386 if let Some(inner) = variant(result, "Err") {
1387 error = Some(dispatch_error(&runtime, inner)?);
1388 success = false;
1389 break;
1390 }
1391 }
1392 }
1393
1394 let fee_rao = events
1395 .iter()
1396 .find(|event| event_is(event, "TransactionPayment", "TransactionFeePaid"))
1397 .and_then(event_attributes)
1398 .and_then(|attributes| field(attributes, "actual_fee"))
1399 .and_then(as_u128);
1400 let message = match &error {
1401 Some(error) => error.to_string(),
1402 None if success => "extrinsic succeeded".into(),
1403 None => "extrinsic did not emit System.ExtrinsicSuccess".into(),
1404 };
1405 Ok(TxOutcome {
1406 success,
1407 extrinsic_hash,
1408 block_hash: Some(block_hash),
1409 block_number: Some(block_number),
1410 extrinsic_index: Some(extrinsic_index),
1411 fee_rao,
1412 events,
1413 error,
1414 message,
1415 data: BTreeMap::new(),
1416 })
1417 }
1418}
1419
1420pub struct Snapshot<'a> {
1422 client: &'a Client,
1423 pub block_hash: String,
1424 pub block_number: u64,
1425}
1426
1427impl Snapshot<'_> {
1428 pub fn query(&self, pallet: &str, storage: &str, params: &[Value]) -> Result<Value, CoreError> {
1429 self.client
1430 .query(pallet, storage, params, Some(&self.block_hash))
1431 }
1432
1433 pub fn query_map(
1434 &self,
1435 pallet: &str,
1436 storage: &str,
1437 fixed_params: &[Value],
1438 ) -> Result<Vec<(Value, Value)>, CoreError> {
1439 self.client
1440 .query_map(pallet, storage, fixed_params, Some(&self.block_hash))
1441 }
1442
1443 pub fn runtime_call(
1444 &self,
1445 api: &str,
1446 method: &str,
1447 params: &[Value],
1448 ) -> Result<Value, CoreError> {
1449 self.client
1450 .runtime_call(api, method, params, Some(&self.block_hash))
1451 }
1452
1453 pub fn balance_rao(&self, address: &str) -> Result<u128, CoreError> {
1454 let account = self.query("System", "Account", &[Value::str(address)])?;
1455 field(&account, "data")
1456 .and_then(|data| field(data, "free"))
1457 .and_then(as_u128)
1458 .ok_or_else(|| CoreError::Codec("System.Account.data.free is missing".into()))
1459 }
1460}
1461
1462pub struct BlockStream<'a> {
1464 client: &'a Client,
1465 finalized: bool,
1466 last_number: Option<u64>,
1467 last_finalized_hash: Option<String>,
1468 poll_interval: Duration,
1469}
1470
1471impl BlockStream<'_> {
1472 fn poll(&mut self) -> Result<Option<BlockHeader>, CoreError> {
1476 let header = if self.finalized {
1477 let hash = self.client.finalized_head()?;
1478 if self.last_finalized_hash.as_deref() == Some(hash.as_str()) {
1479 return Ok(None);
1480 }
1481 self.last_finalized_hash = Some(hash.clone());
1482 let header = self.client.header(Some(&hash))?;
1483 (self.last_number.is_none_or(|last| header.number > last)).then_some(header)
1484 } else {
1485 self.client.best_header_past(self.last_number)?
1486 };
1487 if let Some(header) = &header {
1488 self.last_number = Some(header.number);
1489 }
1490 Ok(header)
1491 }
1492}
1493
1494impl Iterator for BlockStream<'_> {
1495 type Item = Result<BlockHeader, CoreError>;
1496
1497 fn next(&mut self) -> Option<Self::Item> {
1498 loop {
1499 match self.poll() {
1500 Ok(Some(header)) => return Some(Ok(header)),
1501 Ok(None) => thread::sleep(self.poll_interval),
1502 Err(error) => return Some(Err(error)),
1503 }
1504 }
1505 }
1506}
1507
1508struct RpcBootstrap {
1509 endpoint: String,
1510 http: HttpClient,
1511 next_id: AtomicU64,
1512}
1513
1514impl RpcBootstrap {
1515 fn rpc(&self, method: &str, params: JsonValue) -> Result<JsonValue, CoreError> {
1516 rpc_request(
1517 &self.http,
1518 &self.endpoint,
1519 self.next_id.fetch_add(1, Ordering::Relaxed),
1520 method,
1521 params,
1522 )
1523 }
1524
1525 fn runtime_version(&self) -> Result<RuntimeVersion, CoreError> {
1526 parse_runtime_version(self.rpc("state_getRuntimeVersion", json!([]))?)
1527 }
1528
1529 fn metadata(&self) -> Result<Vec<u8>, CoreError> {
1530 fetch_metadata(|method, params| self.rpc(method, params))
1531 }
1532
1533 fn block_hash(&self, block: Option<u64>) -> Result<String, CoreError> {
1534 let params = block.map_or_else(|| json!([]), |number| json!([number]));
1535 let value = self.rpc("chain_getBlockHash", params)?;
1536 Ok(json_string(&value, "chain_getBlockHash result")?.to_string())
1537 }
1538
1539 fn ss58_format(&self) -> Result<u16, CoreError> {
1540 let properties = self.rpc("system_properties", json!([]))?;
1541 let value = properties
1542 .as_object()
1543 .and_then(|object| object.get("ss58Format"))
1544 .map(json_u64)
1545 .transpose()?
1546 .unwrap_or(42);
1547 u16::try_from(value)
1548 .map_err(|_| CoreError::Rpc(format!("ss58Format {value} does not fit u16")))
1549 }
1550}
1551
1552#[derive(Debug, Clone, Copy)]
1553struct RuntimeVersion {
1554 spec_version: u32,
1555 transaction_version: u32,
1556}
1557
1558fn parse_runtime_version(value: JsonValue) -> Result<RuntimeVersion, CoreError> {
1559 let object = value
1560 .as_object()
1561 .ok_or_else(|| CoreError::Rpc("state_getRuntimeVersion returned a non-object".into()))?;
1562 let spec = object
1563 .get("specVersion")
1564 .ok_or_else(|| CoreError::Rpc("runtime version has no specVersion".into()))?;
1565 let transaction = object
1566 .get("transactionVersion")
1567 .ok_or_else(|| CoreError::Rpc("runtime version has no transactionVersion".into()))?;
1568 Ok(RuntimeVersion {
1569 spec_version: u32::try_from(json_u64(spec)?)
1570 .map_err(|_| CoreError::Rpc("specVersion does not fit u32".into()))?,
1571 transaction_version: u32::try_from(json_u64(transaction)?)
1572 .map_err(|_| CoreError::Rpc("transactionVersion does not fit u32".into()))?,
1573 })
1574}
1575
1576fn fetch_metadata<F>(mut rpc: F) -> Result<Vec<u8>, CoreError>
1583where
1584 F: FnMut(&str, JsonValue) -> Result<JsonValue, CoreError>,
1585{
1586 let requested_version = hex_prefixed(&15u32.to_le_bytes());
1587 match rpc(
1588 "state_call",
1589 json!(["Metadata_metadata_at_version", requested_version]),
1590 ) {
1591 Ok(value) => {
1592 let encoded = decode_hex(json_string(&value, "Metadata_metadata_at_version result")?)?;
1593 let mut input = encoded.as_slice();
1594 let metadata = Option::<Vec<u8>>::decode(&mut input).map_err(|error| {
1595 CoreError::Codec(format!(
1596 "cannot decode Metadata_metadata_at_version result: {error}"
1597 ))
1598 })?;
1599 if !input.is_empty() {
1600 return Err(CoreError::Codec(format!(
1601 "{} trailing bytes in Metadata_metadata_at_version result",
1602 input.len()
1603 )));
1604 }
1605 if let Some(metadata) = metadata {
1606 return Ok(metadata);
1607 }
1608 }
1609 Err(CoreError::Rpc(_)) => {}
1610 Err(error) => return Err(error),
1611 }
1612
1613 let value = rpc("state_getMetadata", json!([]))?;
1614 decode_hex(json_string(&value, "state_getMetadata result")?)
1615}
1616
1617fn rpc_request(
1618 client: &HttpClient,
1619 endpoint: &str,
1620 id: u64,
1621 method: &str,
1622 params: JsonValue,
1623) -> Result<JsonValue, CoreError> {
1624 let response = client
1625 .post(endpoint)
1626 .json(&json!({
1627 "jsonrpc": "2.0",
1628 "id": id,
1629 "method": method,
1630 "params": params,
1631 }))
1632 .send()
1633 .map_err(|error| CoreError::Rpc(format!("{method} request failed: {error}")))?
1634 .error_for_status()
1635 .map_err(|error| CoreError::Rpc(format!("{method} HTTP error: {error}")))?;
1636 let body: JsonValue = response
1637 .json()
1638 .map_err(|error| CoreError::Rpc(format!("{method} returned invalid JSON: {error}")))?;
1639 if let Some(error) = body.as_object().and_then(|object| object.get("error")) {
1640 return Err(CoreError::Rpc(format!("{method}: {error}")));
1641 }
1642 body.as_object()
1643 .and_then(|object| object.get("result"))
1644 .cloned()
1645 .ok_or_else(|| CoreError::Rpc(format!("{method} response omitted result")))
1646}
1647
1648fn http_endpoint(endpoint: &str) -> String {
1649 if let Some(rest) = endpoint.strip_prefix("ws://") {
1650 return format!("http://{rest}");
1651 }
1652 if let Some(rest) = endpoint.strip_prefix("wss://") {
1653 return format!("https://{rest}");
1654 }
1655 endpoint.to_string()
1656}
1657
1658fn parse_header_fields(value: &JsonValue) -> Result<(u64, String), CoreError> {
1659 let object = value
1660 .as_object()
1661 .ok_or_else(|| CoreError::Rpc("chain_getHeader returned a non-object".into()))?;
1662 let number = json_u64(
1663 object
1664 .get("number")
1665 .ok_or_else(|| CoreError::Rpc("header has no number".into()))?,
1666 )?;
1667 let parent_hash = object
1668 .get("parentHash")
1669 .and_then(JsonValue::as_str)
1670 .unwrap_or_default()
1671 .to_string();
1672 Ok((number, parent_hash))
1673}
1674
1675fn storage_info(runtime: &Runtime, pallet: &str, storage: &str) -> Result<StorageInfo, CoreError> {
1676 runtime
1677 .storage_entry(pallet, storage)
1678 .cloned()
1679 .ok_or_else(|| CoreError::NotInRuntime(format!("storage {pallet}.{storage}")))
1680}
1681
1682fn runtime_api_method<'a>(
1683 runtime: &'a Runtime,
1684 api: &str,
1685 method: &str,
1686) -> Result<&'a RuntimeApiMethodInfo, CoreError> {
1687 runtime
1688 .apis
1689 .iter()
1690 .find(|candidate| candidate.name == api)
1691 .and_then(|candidate| {
1692 candidate
1693 .methods
1694 .iter()
1695 .find(|candidate| candidate.name == method)
1696 })
1697 .ok_or_else(|| CoreError::NotInRuntime(format!("runtime API {api}.{method}")))
1698}
1699
1700fn decode_storage(
1701 runtime: &Runtime,
1702 info: &StorageInfo,
1703 raw: Option<&str>,
1704) -> Result<Value, CoreError> {
1705 let bytes = match raw {
1706 Some(raw) => decode_hex(raw)?,
1707 None if info.modifier == "Optional" => return Ok(Value::Null),
1708 None => info.default_bytes.clone(),
1709 };
1710 runtime.decode_spec(&TypeSpec::Id(info.value_type), &bytes, true)
1711}
1712
1713fn dispatch_error(runtime: &Runtime, value: &Value) -> Result<DispatchError, CoreError> {
1714 if let Some(module) = variant(value, "Module") {
1715 let index = field(module, "index")
1716 .and_then(as_u128)
1717 .or_else(|| tuple_item(module, 0).and_then(as_u128))
1718 .and_then(|value| u8::try_from(value).ok())
1719 .ok_or_else(|| CoreError::Codec("module error omitted pallet index".into()))?;
1720 let error_index = field(module, "error")
1721 .or_else(|| tuple_item(module, 1))
1722 .and_then(first_byte)
1723 .ok_or_else(|| CoreError::Codec("module error omitted error bytes".into()))?;
1724 let pallet = runtime.pallet_at(index).map(|pallet| pallet.name.clone());
1725 let (name, docs) = runtime
1726 .module_error(index, error_index)
1727 .unwrap_or_else(|_| (format!("Error{error_index}"), Vec::new()));
1728 return Ok(DispatchError {
1729 pallet,
1730 semantic_code: semantic_error_code(&name),
1731 name,
1732 docs,
1733 });
1734 }
1735 let name = variant_name(value).unwrap_or_else(|| "DispatchError".into());
1736 Ok(DispatchError {
1737 pallet: None,
1738 semantic_code: semantic_error_code(&name),
1739 name,
1740 docs: Vec::new(),
1741 })
1742}
1743
1744fn semantic_error_code(name: &str) -> String {
1745 let canonical = match name {
1746 "SubNetworkDoesNotExist"
1747 | "SubnetDoesNotExist"
1748 | "SubnetNotExists"
1749 | "SubNetDoesNotExist" => "subnet_not_exists",
1750 "NotProxy" => "not_proxy",
1751 "NoPermission" | "NotAllowed" | "Unproxyable" => "not_allowed",
1752 "HotKeyNotRegisteredInNetwork" | "HotkeyNotRegisteredInSubNet" | "NotRegistered" => {
1753 "not_registered"
1754 }
1755 "TxRateLimitExceeded" | "SettingWeightsTooFast" => "rate_limited",
1756 _ => return snake_case(name),
1757 };
1758 canonical.into()
1759}
1760
1761fn snake_case(name: &str) -> String {
1762 let mut out = String::new();
1763 for (index, ch) in name.chars().enumerate() {
1764 if ch.is_uppercase() {
1765 if index > 0 {
1766 out.push('_');
1767 }
1768 for lower in ch.to_lowercase() {
1769 out.push(lower);
1770 }
1771 } else {
1772 out.push(ch);
1773 }
1774 }
1775 out
1776}
1777
1778fn event_is(record: &Value, module: &str, name: &str) -> bool {
1779 field(record, "module_id")
1780 .or_else(|| field(record, "event").and_then(|event| field(event, "module_id")))
1781 .and_then(as_str)
1782 == Some(module)
1783 && field(record, "event_id")
1784 .or_else(|| field(record, "event").and_then(|event| field(event, "event_id")))
1785 .and_then(as_str)
1786 == Some(name)
1787}
1788
1789fn event_attributes(record: &Value) -> Option<&Value> {
1790 field(record, "attributes")
1791 .or_else(|| field(record, "event").and_then(|event| field(event, "attributes")))
1792}
1793
1794fn flatten_subnet_hyperparams_v3(raw: Value) -> Result<Value, CoreError> {
1797 if matches!(raw, Value::Null) {
1798 return Ok(Value::Null);
1799 }
1800 let Value::List(entries) = raw else {
1801 return Err(CoreError::Codec(
1802 "SubnetInfoRuntimeApi.get_subnet_hyperparams_v3 returned a non-list".into(),
1803 ));
1804 };
1805 let mut out = Vec::with_capacity(entries.len());
1806 for entry in entries {
1807 let name = field(&entry, "name")
1808 .and_then(as_str)
1809 .ok_or_else(|| {
1810 CoreError::Codec(
1811 "SubnetInfoRuntimeApi.get_subnet_hyperparams_v3 entry omitted name".into(),
1812 )
1813 })?
1814 .to_owned();
1815 let tagged = field(&entry, "value").ok_or_else(|| {
1816 CoreError::Codec(
1817 "SubnetInfoRuntimeApi.get_subnet_hyperparams_v3 entry omitted value".into(),
1818 )
1819 })?;
1820 out.push((Value::Str(name), flatten_hyperparam_value(tagged.clone())));
1821 }
1822 Ok(Value::Dict(out))
1823}
1824
1825fn flatten_hyperparam_value(tagged: Value) -> Value {
1832 const I32F32_ONE: i128 = 1 << 32;
1833 let Value::Dict(entries) = &tagged else {
1834 return tagged;
1835 };
1836 let [(tag, payload)] = entries.as_slice() else {
1837 return tagged;
1838 };
1839 let Some(tag) = as_str(tag) else {
1840 return tagged;
1841 };
1842 if let Value::Dict(inner) = payload {
1843 if let [(Value::Str(key), bits)] = inner.as_slice() {
1844 if key == "bits" {
1845 if tag == "I32F32" {
1846 let bits = match bits {
1847 Value::Int(v) => *v,
1848 Value::Uint(v) => match i128::try_from(*v) {
1849 Ok(v) => v,
1850 Err(_) => return tagged,
1851 },
1852 _ => return tagged,
1853 };
1854 return Value::Int(bits.div_euclid(I32F32_ONE));
1855 }
1856 return bits.clone();
1857 }
1858 }
1859 }
1860 payload.clone()
1861}
1862
1863pub fn field<'a>(value: &'a Value, name: &str) -> Option<&'a Value> {
1864 let Value::Dict(entries) = value else {
1865 return None;
1866 };
1867 entries.iter().find_map(|(key, value)| match key {
1868 Value::Str(key) if key == name => Some(value),
1869 _ => None,
1870 })
1871}
1872
1873pub fn variant<'a>(value: &'a Value, name: &str) -> Option<&'a Value> {
1874 field(value, name)
1875}
1876
1877pub fn variant_name(value: &Value) -> Option<String> {
1878 match value {
1879 Value::Str(name) => Some(name.clone()),
1880 Value::Dict(entries) if entries.len() == 1 => entries.first().and_then(|(key, _)| {
1881 if let Value::Str(name) = key {
1882 Some(name.clone())
1883 } else {
1884 None
1885 }
1886 }),
1887 _ => None,
1888 }
1889}
1890
1891pub fn as_str(value: &Value) -> Option<&str> {
1892 match value {
1893 Value::Str(value) => Some(value),
1894 _ => None,
1895 }
1896}
1897
1898pub fn as_u128(value: &Value) -> Option<u128> {
1899 match value {
1900 Value::Int(value) => u128::try_from(*value).ok(),
1901 Value::Uint(value) => Some(*value),
1902 _ => None,
1903 }
1904}
1905
1906pub fn as_bool(value: &Value) -> Option<bool> {
1907 match value {
1908 Value::Bool(value) => Some(*value),
1909 _ => None,
1910 }
1911}
1912
1913pub fn value_bytes(value: &Value) -> Option<Vec<u8>> {
1914 match value {
1915 Value::Bytes(bytes) => Some(bytes.clone()),
1916 Value::Str(value) => {
1917 if value.starts_with("0x") {
1918 decode_hex(value).ok()
1919 } else {
1920 Some(value.as_bytes().to_vec())
1921 }
1922 }
1923 Value::List(items) | Value::Tuple(items) => {
1924 let direct = items
1925 .iter()
1926 .map(as_u128)
1927 .map(|value| value.and_then(|value| u8::try_from(value).ok()))
1928 .collect::<Option<Vec<_>>>();
1929 direct.or_else(|| match items.as_slice() {
1930 [inner] => value_bytes(inner),
1931 _ => None,
1932 })
1933 }
1934 Value::Dict(entries) => match entries.as_slice() {
1935 [(_, inner)] => value_bytes(inner),
1936 _ => None,
1937 },
1938 _ => None,
1939 }
1940}
1941
1942fn tuple_item(value: &Value, index: usize) -> Option<&Value> {
1943 match value {
1944 Value::Tuple(items) | Value::List(items) => items.get(index),
1945 _ => None,
1946 }
1947}
1948
1949fn first_byte(value: &Value) -> Option<u8> {
1950 value_bytes(value).and_then(|bytes| bytes.first().copied())
1951}
1952
1953fn required_u128(value: &Value, name: &str) -> Result<u128, CoreError> {
1954 field(value, name)
1955 .and_then(as_u128)
1956 .ok_or_else(|| CoreError::Codec(format!("runtime result omitted integer field {name}")))
1957}
1958
1959fn value_map_u16(rows: Vec<(Value, Value)>) -> BTreeMap<u16, u16> {
1960 rows.into_iter()
1961 .filter_map(|(key, value)| {
1962 let key = as_u128(&key).and_then(|value| u16::try_from(value).ok())?;
1963 let value = as_u128(&value).and_then(|value| u16::try_from(value).ok())?;
1964 Some((key, value))
1965 })
1966 .collect()
1967}
1968
1969fn value_map_u128(rows: Vec<(Value, Value)>) -> BTreeMap<u16, u128> {
1970 rows.into_iter()
1971 .filter_map(|(key, value)| {
1972 let key = as_u128(&key).and_then(|value| u16::try_from(value).ok())?;
1973 Some((key, as_u128(&value)?))
1974 })
1975 .collect()
1976}
1977
1978fn json_string<'a>(value: &'a JsonValue, context: &str) -> Result<&'a str, CoreError> {
1979 match value {
1980 JsonValue::String(text) => Ok(text),
1981 _ => Err(CoreError::Rpc(format!("{context} is not a string"))),
1982 }
1983}
1984
1985fn json_number_u64(value: &JsonValue) -> Option<u64> {
1986 match value {
1987 JsonValue::Number(number) => number.to_string().parse().ok(),
1988 _ => None,
1989 }
1990}
1991
1992fn json_number_u128(value: &JsonValue) -> Option<u128> {
1993 match value {
1994 JsonValue::Number(number) => number.to_string().parse().ok(),
1995 _ => None,
1996 }
1997}
1998
1999fn json_number_i128(value: &JsonValue) -> Option<i128> {
2000 match value {
2001 JsonValue::Number(number) => number.to_string().parse().ok(),
2002 _ => None,
2003 }
2004}
2005
2006fn json_u64(value: &JsonValue) -> Result<u64, CoreError> {
2007 if let Some(value) = json_number_u64(value) {
2008 return Ok(value);
2009 }
2010 let text = value
2011 .as_str()
2012 .ok_or_else(|| CoreError::Rpc(format!("expected integer, got {value}")))?;
2013 if let Some(hex) = text.strip_prefix("0x") {
2014 return u64::from_str_radix(hex, 16)
2015 .map_err(|error| CoreError::Rpc(format!("invalid hex integer {text}: {error}")));
2016 }
2017 text.parse::<u64>()
2018 .map_err(|error| CoreError::Rpc(format!("invalid integer {text}: {error}")))
2019}
2020
2021fn json_u128(value: &JsonValue) -> Result<u128, CoreError> {
2022 if let Some(value) = json_number_u128(value) {
2023 return Ok(value);
2024 }
2025 let text = value
2026 .as_str()
2027 .ok_or_else(|| CoreError::Rpc(format!("expected integer, got {value}")))?;
2028 if let Some(hex) = text.strip_prefix("0x") {
2029 return u128::from_str_radix(hex, 16)
2030 .map_err(|error| CoreError::Rpc(format!("invalid hex integer {text}: {error}")));
2031 }
2032 text.parse::<u128>()
2033 .map_err(|error| CoreError::Rpc(format!("invalid integer {text}: {error}")))
2034}
2035
2036fn json_to_value(value: &JsonValue) -> Result<Value, CoreError> {
2037 Ok(match value {
2038 JsonValue::Null => Value::Null,
2039 JsonValue::Bool(value) => Value::Bool(*value),
2040 JsonValue::Number(value) => {
2041 let number = JsonValue::Number(value.clone());
2042 if let Some(signed) = json_number_i128(&number) {
2043 Value::Int(i128::from(signed))
2044 } else if let Some(unsigned) = json_number_u128(&number) {
2045 Value::Uint(unsigned)
2046 } else {
2047 return Err(CoreError::Codec(format!(
2048 "non-integral JSON number cannot become SCALE Value: {value}"
2049 )));
2050 }
2051 }
2052 JsonValue::String(value) => Value::str(value),
2053 JsonValue::Array(items) => Value::List(
2054 items
2055 .iter()
2056 .map(json_to_value)
2057 .collect::<Result<Vec<_>, _>>()?,
2058 ),
2059 JsonValue::Object(entries) => Value::record(
2060 entries
2061 .iter()
2062 .map(|(key, value)| Ok((key.clone(), json_to_value(value)?)))
2063 .collect::<Result<Vec<_>, CoreError>>()?,
2064 ),
2065 })
2066}
2067
2068fn parse_h256(value: &str) -> Result<[u8; 32], CoreError> {
2069 let bytes = decode_hex(value)?;
2070 <[u8; 32]>::try_from(bytes.as_slice())
2071 .map_err(|_| CoreError::Rpc(format!("expected H256, got {} bytes", bytes.len())))
2072}
2073
2074fn decode_hex(value: &str) -> Result<Vec<u8>, CoreError> {
2075 hex::decode(value.trim_start_matches("0x"))
2076 .map_err(|error| CoreError::Codec(format!("invalid hex {value}: {error}")))
2077}
2078
2079fn hex_prefixed(bytes: &[u8]) -> String {
2080 format!("0x{}", hex::encode(bytes))
2081}
2082
2083#[cfg(test)]
2084mod reorg_finalization_tests {
2085 use super::{classify_inclusion_finalization, InclusionFinalization};
2086
2087 #[test]
2088 fn canonical_inclusion_remains_pending_below_finalized_height() {
2089 assert_eq!(
2090 classify_inclusion_finalization("0xabc", Some("0xABC"), 10, 9),
2091 None
2092 );
2093 }
2094
2095 #[test]
2096 fn canonical_inclusion_finalizes_at_or_above_its_height() {
2097 assert_eq!(
2098 classify_inclusion_finalization("0xabc", Some("0xABC"), 10, 10),
2099 Some(InclusionFinalization::Finalized)
2100 );
2101 assert_eq!(
2102 classify_inclusion_finalization("0xabc", Some("0xabc"), 10, 12),
2103 Some(InclusionFinalization::Finalized)
2104 );
2105 }
2106
2107 #[test]
2108 fn replaced_or_missing_inclusion_hash_is_a_reorg() {
2109 assert_eq!(
2110 classify_inclusion_finalization("0xabc", Some("0xdef"), 10, 12),
2111 Some(InclusionFinalization::Reorged)
2112 );
2113 assert_eq!(
2114 classify_inclusion_finalization("0xabc", None, 10, 9),
2115 Some(InclusionFinalization::Reorged)
2116 );
2117 }
2118}
2119
2120#[cfg(test)]
2121mod fee_quote_tests {
2122 #![allow(clippy::unwrap_used, clippy::expect_used)]
2123 use super::*;
2124 use std::io::{BufRead, BufReader, Read, Write};
2125 use std::net::TcpListener;
2126
2127 fn runtime() -> Runtime {
2128 crate::codec::extrinsic::hashed_tests::runtime_falcon()
2129 }
2130
2131 #[test]
2132 fn fee_and_weight_rpc_use_sized_dummy_proofs_for_every_wallet_mode() {
2133 for crypto in [
2134 0,
2135 1,
2136 4,
2137 6,
2138 crate::keys::CRYPTO_MLDSA65,
2139 crate::keys::CRYPTO_MLDSA65_FIXED,
2140 crate::keys::CRYPTO_FALCON512,
2141 crate::keys::CRYPTO_FALCON512_HASHED,
2142 crate::keys::CRYPTO_FALCON1024,
2143 crate::keys::CRYPTO_FALCON1024_HASHED,
2144 ] {
2145 let runtime = runtime();
2146 let full = Keypair::from_seed(&[17; 32], crypto).unwrap();
2147 let public = Keypair::new(Some(&full.ss58_address()), None, crypto, 42).unwrap();
2149 let call = runtime
2150 .compose_call(
2151 "System",
2152 "remark",
2153 &Value::record(vec![("remark".into(), Value::Bytes(vec![7; 300]))]),
2154 )
2155 .unwrap();
2156 let params = TxParams {
2157 era: Value::record(vec![
2158 ("period".into(), Value::Uint(64)),
2159 ("current".into(), Value::Uint(10)),
2160 ]),
2161 nonce: 64,
2162 tip: 0,
2163 tip_asset_id: None,
2164 genesis_hash: [8; 32],
2165 era_block_hash: [8; 32],
2166 metadata_hash: None,
2167 };
2168 let proof = if is_hashed_crypto(crypto) || is_pq_crypto(crypto) {
2169 full.sign_authorization(b"length control").unwrap()
2170 } else {
2171 full.sign(b"length control").unwrap()
2172 };
2173 let (control, _) = runtime
2174 .encode_signed_extrinsic(&call, full.public_key_bytes(), &proof, crypto, ¶ms)
2175 .unwrap();
2176 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
2177 let endpoint = format!("http://{}", listener.local_addr().unwrap());
2178 let server = thread::spawn(move || {
2179 let mut quotes = Vec::new();
2180 while quotes.len() < 2 {
2181 let (mut stream, _) = listener.accept().unwrap();
2182 stream
2183 .set_read_timeout(Some(Duration::from_secs(5)))
2184 .unwrap();
2185 let mut reader = BufReader::new(&mut stream);
2186 let mut length = 0;
2187 let mut headers = String::new();
2188 loop {
2189 let mut line = String::new();
2190 assert_ne!(reader.read_line(&mut line).unwrap(), 0);
2191 headers.push_str(&line);
2192 if line == "\r\n" {
2193 break;
2194 }
2195 if let Some(value) = line.to_lowercase().strip_prefix("content-length:") {
2196 length = value.trim().parse::<usize>().unwrap();
2197 }
2198 }
2199 match headers.lines().next().unwrap() {
2200 "HEAD / HTTP/1.1" => {
2201 assert_eq!(length, 0, "HEAD probe must not carry a body");
2203 write!(
2204 stream,
2205 "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
2206 )
2207 .unwrap();
2208 continue;
2209 }
2210 "POST / HTTP/1.1" => {}
2211 other => panic!("unexpected mock HTTP request: {other}"),
2212 }
2213 assert!(length > 0, "mock request has no body length: {headers:?}");
2214 let mut body = vec![0; length];
2215 reader.read_exact(&mut body).unwrap();
2216 let request: JsonValue = serde_json::from_slice(&body).unwrap();
2217 let result = match request["method"].as_str().unwrap() {
2218 "system_accountNextIndex" => json!(64),
2219 "chain_getHeader" => {
2220 json!({"number":"0xa", "parentHash":hex_prefixed(&[8;32])})
2221 }
2222 "chain_getBlockHash" => json!(hex_prefixed(&[8; 32])),
2223 "payment_queryInfo" => {
2224 quotes
2225 .push(decode_hex(request["params"][0].as_str().unwrap()).unwrap());
2226 json!({"partialFee":"1234", "weight":{"ref_time":456, "proof_size":78}})
2227 }
2228 other => {
2229 panic!("quote must not read authorization state or submit: {other}")
2230 }
2231 };
2232 let response =
2233 json!({"jsonrpc":"2.0", "id":request["id"], "result":result}).to_string();
2234 write!(stream, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", response.len(), response).unwrap();
2235 }
2236 quotes
2237 });
2238 let client = Client {
2239 endpoint,
2240 http: HttpClient::builder()
2241 .no_proxy()
2242 .timeout(Duration::from_secs(5))
2243 .build()
2244 .unwrap(),
2245 next_id: AtomicU64::new(1),
2246 runtime: RwLock::new(Arc::new(runtime)),
2247 genesis_hash: [8; 32],
2248 ss58_format: 42,
2249 };
2250 let probe = || {
2251 let response = client.http.head(&client.endpoint).send().unwrap();
2252 assert_eq!(response.status().as_u16(), 200);
2253 assert!(response.bytes().unwrap().is_empty());
2254 };
2255 probe();
2258 assert_eq!(client.estimate_fee(&call, &public).unwrap(), 1234);
2259 probe();
2260 assert_eq!(
2261 client.estimate_weight(&call, &public).unwrap(),
2262 json_to_value(&json!({"ref_time":456,"proof_size":78})).unwrap()
2263 );
2264 let quotes = server.join().unwrap();
2265 assert_eq!(quotes[0], quotes[1]);
2266 assert_eq!(quotes[0].len(), control.len());
2267 let mut body = quotes[0].as_slice();
2268 let _ = codec::Compact::<u32>::decode(&mut body).unwrap();
2269 let mut control_body = control.as_slice();
2270 let _ = codec::Compact::<u32>::decode(&mut control_body).unwrap();
2271 if crate::keys::is_falcon_crypto(crypto) {
2272 assert_eq!(&body[..2], &[0x84, 2]);
2273 let mut raw = &body[2..];
2274 let profile = full.mldsa_profile().unwrap();
2275 assert_eq!(
2276 codec::Compact::<u32>::decode(&mut raw).unwrap().0 as usize,
2277 subtensor_hashed::falcon::proof_len(profile).unwrap()
2278 );
2279 let address = subtensor_hashed::falcon::SignerAddress::decode(&mut raw).unwrap();
2280 assert_eq!(address.account, full.public_key_bytes());
2281 assert!(address.public_key.iter().all(|byte| *byte == 0));
2282 assert!(address.signature.iter().all(|byte| *byte == 0));
2283 assert_eq!(&raw[..65], &[vec![0], vec![0x4d; 64]].concat());
2284 let active = full.pq_public_key().unwrap();
2285 assert!(!quotes[0].windows(active.len()).any(|w| w == active));
2286 } else if is_pq_crypto(crypto) {
2287 assert_eq!(&body[..2], &[0x84, 2]);
2288 let mut raw = &body[2..];
2289 assert_eq!(codec::Compact::<u32>::decode(&mut raw).unwrap().0, 5367);
2290 let address = subtensor_hashed::mldsa::SignerAddress::decode(&mut raw).unwrap();
2291 assert_eq!(address.account, full.public_key_bytes());
2292 assert_eq!(address.public_key, [0; 1952]);
2293 assert_eq!(address.signature, [0; 3309]);
2294 assert_eq!(&raw[..65], &[vec![0], vec![0x4d; 64]].concat());
2295 let active = full.mldsa_public_key().unwrap();
2296 assert!(!quotes[0].windows(active.len()).any(|w| w == active));
2297 } else if is_hashed_crypto(crypto) {
2298 assert_eq!(&body[..2], &[0x84, 2]);
2299 let mut raw = &body[2..];
2300 assert_eq!(codec::Compact::<u32>::decode(&mut raw).unwrap().0, 106);
2301 let address = subtensor_hashed::SignerAddress::decode(&mut raw).unwrap();
2302 assert_eq!(address.account, full.public_key_bytes());
2303 assert_eq!(address.public_key, [0; 32]);
2304 assert_eq!(address.next_commitment, [0; 32]);
2305 assert_eq!(&raw[1..65], &[0; 64]);
2306 let active = full.hashed_public_key().unwrap();
2307 assert!(!quotes[0].windows(active.len()).any(|w| w == active));
2308 } else {
2309 assert_eq!(&body[35..99], &[0; 64]);
2310 assert_eq!(&body[..35], &control_body[..35]);
2311 }
2312 assert!(client
2313 .runtime()
2314 .unwrap()
2315 .decode_extrinsic("es[0], true)
2316 .is_ok());
2317 }
2318 }
2319}
2320
2321#[cfg(test)]
2322mod registered_signing_tests {
2323 #![allow(clippy::unwrap_used, clippy::expect_used)]
2324
2325 use super::*;
2326 use chacha20poly1305::{aead::Aead, KeyInit, XChaCha20Poly1305, XNonce};
2327 use ml_kem::{kem::Decapsulate, EncodedSizeUser, KemCore, MlKem768};
2328 use scale_info::{Field, Path, PortableType, Type, TypeDef, TypeDefArray, TypeDefComposite};
2329 use std::io::{BufRead, BufReader, Read, Write};
2330 use std::net::TcpListener;
2331 use std::sync::atomic::AtomicBool;
2332 use std::sync::Mutex;
2333
2334 const GENERATION: u64 = 12;
2335 const NONCE: u64 = 63;
2336 const PINNED_HASH: [u8; 32] = [8; 32];
2337 const ADVANCED_HASH: [u8; 32] = [9; 32];
2338
2339 fn add_type(runtime: &mut Runtime, type_def: TypeDef<scale_info::form::PortableForm>) -> u32 {
2340 let id = runtime.types.types.len() as u32;
2341 runtime.types.types.push(PortableType {
2342 id,
2343 ty: Type {
2344 path: Path::default(),
2345 type_params: vec![],
2346 type_def,
2347 docs: vec![],
2348 },
2349 });
2350 id
2351 }
2352
2353 fn record_type(runtime: &mut Runtime, fields: &[(&str, u32)]) -> u32 {
2354 add_type(
2355 runtime,
2356 TypeDef::Composite(TypeDefComposite {
2357 fields: fields
2358 .iter()
2359 .map(|(name, ty)| Field {
2360 name: Some((*name).into()),
2361 ty: (*ty).into(),
2362 type_name: None,
2363 docs: vec![],
2364 })
2365 .collect(),
2366 }),
2367 )
2368 }
2369
2370 fn runtime(crypto: u8) -> Runtime {
2373 let mut runtime = crate::codec::extrinsic::hashed_tests::runtime_falcon();
2374 let u8_ty = add_type(
2375 &mut runtime,
2376 TypeDef::Primitive(scale_info::TypeDefPrimitive::U8),
2377 );
2378 let u64_ty = add_type(
2379 &mut runtime,
2380 TypeDef::Primitive(scale_info::TypeDefPrimitive::U64),
2381 );
2382 let bool_ty = add_type(
2383 &mut runtime,
2384 TypeDef::Primitive(scale_info::TypeDefPrimitive::Bool),
2385 );
2386 let commitment_len = if is_pq_crypto(crypto) { 64 } else { 32 };
2387 let commitment_ty = add_type(
2388 &mut runtime,
2389 TypeDef::Array(TypeDefArray {
2390 len: commitment_len,
2391 type_param: u8_ty.into(),
2392 }),
2393 );
2394 let descriptor_ty = record_type(
2395 &mut runtime,
2396 &[
2397 ("version", u8_ty),
2398 ("algorithm", u8_ty),
2399 ("initial_commitment", commitment_ty),
2400 ],
2401 );
2402 let authority_ty = record_type(
2403 &mut runtime,
2404 &[
2405 ("descriptor", descriptor_ty),
2406 ("generation", u64_ty),
2407 ("commitment", commitment_ty),
2408 ],
2409 );
2410 let account_ty = record_type(&mut runtime, &[("nonce", u64_ty)]);
2411 let system = runtime
2412 .pallets
2413 .iter_mut()
2414 .find(|p| p.name == "System")
2415 .unwrap();
2416 let account = system
2417 .storage
2418 .iter_mut()
2419 .find(|s| s.name == "Account")
2420 .unwrap();
2421 account.value_type = account_ty;
2422 let mut authority = account.clone();
2423 authority.prefix = "HashedAccounts".into();
2424 authority.name = if is_pq_crypto(crypto) {
2425 "MlDsaAccounts"
2426 } else {
2427 "Accounts"
2428 }
2429 .into();
2430 authority.value_type = authority_ty;
2431 runtime
2432 .pallets
2433 .iter_mut()
2434 .find(|p| p.name == "HashedAccounts")
2435 .unwrap()
2436 .storage = vec![authority];
2437 runtime
2438 .pallets
2439 .iter_mut()
2440 .find(|p| p.name == "AdminUtils")
2441 .unwrap()
2442 .storage
2443 .push(StorageInfo {
2444 name: "HashedAccountsEnabled".into(),
2445 prefix: "AdminUtils".into(),
2446 modifier: "Default".into(),
2447 hashers: vec![],
2448 key_types: vec![],
2449 value_type: bool_ty,
2450 default_bytes: vec![0],
2451 });
2452 runtime
2453 }
2454
2455 #[derive(Default)]
2456 struct NodeState {
2457 nonce_override: Option<u64>,
2458 advanced: bool,
2459 pending: bool,
2460 advance_after_inner: bool,
2461 consume_nonce_on_advance: bool,
2462 nonce_reads: usize,
2463 finalized_reads: usize,
2464 authority_reads: Vec<String>,
2465 account_reads: Vec<String>,
2466 submitted: Vec<Vec<u8>>,
2467 included: Vec<Vec<u8>>,
2468 events: Vec<u8>,
2469 block_reads: usize,
2470 }
2471
2472 struct MockNode {
2473 client: Client,
2474 state: Arc<Mutex<NodeState>>,
2475 stop: Arc<AtomicBool>,
2476 server: Option<thread::JoinHandle<()>>,
2477 decrypt_key: <MlKem768 as KemCore>::DecapsulationKey,
2478 }
2479
2480 impl Drop for MockNode {
2481 fn drop(&mut self) {
2482 self.stop.store(true, Ordering::Relaxed);
2483 if let Some(server) = self.server.take() {
2484 server.join().unwrap();
2485 }
2486 }
2487 }
2488
2489 impl MockNode {
2490 fn new(signer: &Keypair) -> Self {
2491 let crypto = signer.crypto_type();
2492 let runtime = runtime(crypto);
2493 let address = [Value::str(signer.ss58_address())];
2494 let storage_key = |pallet, storage, params: &[Value]| {
2495 hex_prefixed(
2496 &runtime
2497 .storage_key(&storage_info(&runtime, pallet, storage).unwrap(), params)
2498 .unwrap(),
2499 )
2500 };
2501 let enabled_key = storage_key("AdminUtils", "HashedAccountsEnabled", &[]);
2502 let authority_key = storage_key(
2503 "HashedAccounts",
2504 if is_pq_crypto(crypto) {
2505 "MlDsaAccounts"
2506 } else {
2507 "Accounts"
2508 },
2509 &address,
2510 );
2511 let account_key = storage_key("System", "Account", &address);
2512 let next_key = storage_key("MevShield", "NextKey", &[]);
2513 let events_key = storage_key("System", "Events", &[]);
2514 let descriptor = if is_pq_crypto(crypto) {
2515 signer.mldsa_descriptor().unwrap()
2516 } else {
2517 signer.hashed_descriptor().unwrap()
2518 };
2519 let authority = |generation: u64| {
2520 let mut encoded = descriptor.clone();
2521 encoded.extend(generation.encode());
2522 encoded.extend(
2523 signer
2524 .at_generation(generation)
2525 .unwrap()
2526 .authorization_current_commitment()
2527 .unwrap(),
2528 );
2529 encoded
2530 };
2531 let (old_authority, new_authority) = if crate::keys::is_fixed_pq_crypto(crypto) {
2532 (Vec::new(), Vec::new())
2534 } else {
2535 (authority(GENERATION), authority(GENERATION + 1))
2536 };
2537 let (decrypt_key, encrypt_key) = MlKem768::generate(&mut rand_core::OsRng);
2538 let key_info = storage_info(&runtime, "MevShield", "NextKey").unwrap();
2539 let mut encoded_key = Vec::new();
2540 runtime
2541 .encode_id(
2542 key_info.value_type,
2543 &Value::Bytes(encrypt_key.as_bytes().to_vec()),
2544 &mut encoded_key,
2545 )
2546 .unwrap();
2547 let state = Arc::new(Mutex::new(NodeState::default()));
2548 let server_state = Arc::clone(&state);
2549 let stop = Arc::new(AtomicBool::new(false));
2550 let server_stop = Arc::clone(&stop);
2551 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
2552 listener.set_nonblocking(true).unwrap();
2553 let endpoint = format!("http://{}", listener.local_addr().unwrap());
2554 let server = thread::spawn(move || {
2555 while !server_stop.load(Ordering::Relaxed) {
2556 let (mut stream, _) = match listener.accept() {
2557 Ok(value) => value,
2558 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2559 thread::sleep(Duration::from_millis(1));
2560 continue;
2561 }
2562 Err(error) => panic!("mock accept failed: {error}"),
2563 };
2564 stream.set_nonblocking(false).unwrap();
2567 stream
2568 .set_read_timeout(Some(Duration::from_secs(5)))
2569 .unwrap();
2570 let mut reader = BufReader::new(&mut stream);
2571 let mut length = 0;
2572 loop {
2573 let mut line = String::new();
2574 assert_ne!(reader.read_line(&mut line).unwrap(), 0);
2575 if line == "\r\n" {
2576 break;
2577 }
2578 if let Some(value) = line.to_lowercase().strip_prefix("content-length:") {
2579 length = value.trim().parse::<usize>().unwrap();
2580 }
2581 }
2582 let mut body = vec![0; length];
2583 reader.read_exact(&mut body).unwrap();
2584 let request: JsonValue = serde_json::from_slice(&body).unwrap();
2585 let mut state = server_state.lock().unwrap();
2586 let hash = if state.advanced {
2587 ADVANCED_HASH
2588 } else {
2589 PINNED_HASH
2590 };
2591 let result = match request["method"].as_str().unwrap() {
2592 "chain_getFinalizedHead" => {
2593 state.finalized_reads += 1;
2594 json!(hex_prefixed(&hash))
2595 }
2596 "chain_getBlockHash" => json!(hex_prefixed(&hash)),
2597 "chain_getHeader" => {
2598 json!({"number":"0xa", "parentHash":hex_prefixed(&PINNED_HASH)})
2599 }
2600 "chain_getBlock" => {
2601 state.block_reads += 1;
2602 json!({"block":{"extrinsics":state.included.iter().map(|xt| hex_prefixed(xt)).collect::<Vec<_>>()}})
2603 }
2604 "system_accountNextIndex" => {
2605 state.nonce_reads += 1;
2606 if state.advance_after_inner && state.nonce_reads == 3 {
2610 state.advanced = true;
2611 }
2612 let nonce = NONCE
2613 + u64::from(
2614 state.pending
2615 || (state.advanced && state.consume_nonce_on_advance),
2616 );
2617 json!(state.nonce_override.unwrap_or(nonce))
2618 }
2619 "state_getStorage" => {
2620 let key = request["params"][0].as_str().unwrap();
2621 let block = request["params"].get(1).and_then(JsonValue::as_str);
2622 if key == next_key {
2623 json!(hex_prefixed(&encoded_key))
2624 } else if key == events_key {
2625 json!(hex_prefixed(&state.events))
2626 } else {
2627 let block = block.expect("authority reads must specify a block");
2628 let new = block == hex_prefixed(&ADVANCED_HASH);
2629 assert!(new || block == hex_prefixed(&PINNED_HASH));
2630 if key == enabled_key {
2631 json!("0x01")
2632 } else if key == authority_key {
2633 state.authority_reads.push(block.into());
2634 json!(hex_prefixed(if new && state.consume_nonce_on_advance {
2635 &new_authority
2636 } else {
2637 &old_authority
2638 }))
2639 } else if key == account_key {
2640 state.account_reads.push(block.into());
2641 json!(hex_prefixed(
2642 &(NONCE + u64::from(new && state.consume_nonce_on_advance))
2643 .encode()
2644 ))
2645 } else {
2646 panic!("unexpected storage key {key}")
2647 }
2648 }
2649 }
2650 "author_submitExtrinsic" => {
2651 state
2652 .submitted
2653 .push(decode_hex(request["params"][0].as_str().unwrap()).unwrap());
2654 JsonValue::Null
2655 }
2656 method => panic!("unexpected RPC {method}"),
2657 };
2658 let response = if request["method"] == "author_submitExtrinsic" {
2659 json!({"jsonrpc":"2.0", "id":request["id"], "error":{"code":1010,"message":"captured by signing test"}})
2661 } else { json!({"jsonrpc":"2.0", "id":request["id"], "result":result}) }.to_string();
2662 write!(stream, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", response.len(), response).unwrap();
2663 }
2664 });
2665 Self {
2666 client: Client {
2667 endpoint,
2668 http: HttpClient::builder()
2669 .no_proxy()
2670 .timeout(Duration::from_secs(5))
2671 .build()
2672 .unwrap(),
2673 next_id: AtomicU64::new(1),
2674 runtime: RwLock::new(Arc::new(runtime)),
2675 genesis_hash: PINNED_HASH,
2676 ss58_format: 42,
2677 },
2678 state,
2679 stop,
2680 server: Some(server),
2681 decrypt_key,
2682 }
2683 }
2684
2685 fn call(&self) -> Vec<u8> {
2686 self.client
2687 .compose_call(
2688 "System",
2689 "remark",
2690 &Value::record(vec![("remark".into(), Value::Bytes(vec![42]))]),
2691 )
2692 .unwrap()
2693 }
2694
2695 fn decrypt(&self, ciphertext: &[u8]) -> Vec<u8> {
2696 let blob = &ciphertext[16..];
2697 let kem_len = usize::from(u16::from_le_bytes([blob[0], blob[1]]));
2698 let kem = ml_kem::Ciphertext::<MlKem768>::try_from(&blob[2..2 + kem_len]).unwrap();
2699 let shared = self.decrypt_key.decapsulate(&kem).unwrap();
2700 let nonce = &blob[2 + kem_len..2 + kem_len + 24];
2701 XChaCha20Poly1305::new_from_slice(shared.as_ref())
2702 .unwrap()
2703 .decrypt(XNonce::from_slice(nonce), &blob[2 + kem_len + 24..])
2704 .unwrap()
2705 }
2706 }
2707
2708 fn assert_proof(
2709 node: &MockNode,
2710 signer: &Keypair,
2711 extrinsic: &[u8],
2712 call: &[u8],
2713 generation: u64,
2714 nonce: u64,
2715 ) {
2716 use sp_core::Pair;
2717 let runtime = node.client.runtime().unwrap();
2718 let decoded = runtime.decode_extrinsic(extrinsic, true).unwrap();
2719 assert_eq!(
2720 field(&decoded, "nonce").and_then(as_u128),
2721 Some(u128::from(nonce))
2722 );
2723 let era = match field(&decoded, "era").unwrap() {
2724 Value::List(parts) | Value::Tuple(parts) => Value::record(vec![
2725 ("period".into(), parts[0].clone()),
2726 ("phase".into(), parts[1].clone()),
2727 ]),
2728 value => value.clone(),
2729 };
2730 let params = TxParams {
2731 era,
2732 nonce,
2733 tip: 0,
2734 tip_asset_id: None,
2735 genesis_hash: PINNED_HASH,
2736 era_block_hash: PINNED_HASH,
2737 metadata_hash: None,
2738 };
2739 let (extra, additional) = runtime.signature_payload_parts(¶ms).unwrap();
2740 let mut payload = call.to_vec();
2741 payload.extend(extra);
2742 payload.extend(additional);
2743 let mut bytes = extrinsic;
2744 let _ = codec::Compact::<u32>::decode(&mut bytes).unwrap();
2745 assert_eq!(u8::decode(&mut bytes).unwrap(), 0x84);
2746 assert_eq!(u8::decode(&mut bytes).unwrap(), 2);
2747 let address = Vec::<u8>::decode(&mut bytes).unwrap();
2748 if crate::keys::is_falcon_crypto(signer.crypto_type()) {
2749 let proof = subtensor_hashed::falcon::SignerAddress::from_bytes(&address).unwrap();
2750 assert_eq!(proof.generation, generation);
2751 assert_eq!(proof.account, signer.public_key_bytes());
2752 assert!(proof.verify(&payload));
2753 } else if is_pq_crypto(signer.crypto_type()) {
2754 let proof =
2755 subtensor_hashed::mldsa::SignerAddress::decode(&mut address.as_slice()).unwrap();
2756 assert_eq!(proof.generation, generation);
2757 assert_eq!(proof.account, signer.public_key_bytes());
2758 assert!(proof.verify(&payload));
2759 } else {
2760 let proof = subtensor_hashed::SignerAddress::decode(&mut address.as_slice()).unwrap();
2761 assert_eq!(proof.generation, generation);
2762 assert_eq!(proof.account, signer.public_key_bytes());
2763 let scheme = u8::decode(&mut bytes).unwrap();
2764 let signature = <[u8; 64]>::decode(&mut bytes).unwrap();
2765 let payload = proof.signing_hash(&payload);
2766 let valid = if scheme == 1 {
2767 sp_core::sr25519::Pair::verify(
2768 &sp_core::sr25519::Signature::from_raw(signature),
2769 payload,
2770 &sp_core::sr25519::Public::from_raw(proof.public_key),
2771 )
2772 } else {
2773 assert_eq!(scheme, 0);
2774 sp_core::ed25519::Pair::verify(
2775 &sp_core::ed25519::Signature::from_raw(signature),
2776 payload,
2777 &sp_core::ed25519::Public::from_raw(proof.public_key),
2778 )
2779 };
2780 assert!(valid);
2781 }
2782 }
2783
2784 fn wallet_modes() -> [u8; 5] {
2785 [
2786 crate::keys::CRYPTO_HASHED,
2787 crate::keys::CRYPTO_HASHED_ED25519,
2788 crate::keys::CRYPTO_MLDSA65,
2789 crate::keys::CRYPTO_FALCON512_HASHED,
2790 crate::keys::CRYPTO_FALCON1024_HASHED,
2791 ]
2792 }
2793
2794 fn shield_events(node: &MockNode, inner_success: bool) -> Vec<u8> {
2795 let dispatch_info = Value::record(vec![
2796 (
2797 "weight".into(),
2798 Value::record(vec![
2799 ("ref_time".into(), Value::Uint(1)),
2800 ("proof_size".into(), Value::Uint(0)),
2801 ]),
2802 ),
2803 ("class".into(), Value::str("Normal")),
2804 ("pays_fee".into(), Value::str("Yes")),
2805 ]);
2806 let event = |index, pallet: &str, name: &str, attributes| {
2807 Value::record(vec![
2808 (
2809 "phase".into(),
2810 Value::record(vec![("ApplyExtrinsic".into(), Value::Uint(index))]),
2811 ),
2812 (
2813 "event".into(),
2814 Value::record(vec![(
2815 pallet.into(),
2816 Value::record(vec![(name.into(), attributes)]),
2817 )]),
2818 ),
2819 ("topics".into(), Value::List(vec![])),
2820 ])
2821 };
2822 let mut records = vec![event(
2823 0,
2824 "System",
2825 "ExtrinsicSuccess",
2826 Value::record(vec![("dispatch_info".into(), dispatch_info.clone())]),
2827 )];
2828 records.push(event(
2829 1,
2830 "System",
2831 if inner_success {
2832 "ExtrinsicSuccess"
2833 } else {
2834 "ExtrinsicFailed"
2835 },
2836 Value::record(if inner_success {
2837 vec![("dispatch_info".into(), dispatch_info)]
2838 } else {
2839 vec![
2840 ("dispatch_error".into(), Value::str("BadOrigin")),
2841 ("dispatch_info".into(), dispatch_info),
2842 ]
2843 }),
2844 ));
2845 records.push(event(
2846 1,
2847 "TransactionPayment",
2848 "TransactionFeePaid",
2849 Value::record(vec![
2850 ("who".into(), Value::Bytes(vec![42; 32])),
2851 ("actual_fee".into(), Value::Uint(3)),
2852 ("tip".into(), Value::Uint(0)),
2853 ]),
2854 ));
2855 let runtime = node.client.runtime().unwrap();
2856 let mut encoded = Vec::new();
2857 runtime
2858 .encode_id(
2859 storage_info(&runtime, "System", "Events")
2860 .unwrap()
2861 .value_type,
2862 &Value::List(records),
2863 &mut encoded,
2864 )
2865 .unwrap();
2866 encoded
2867 }
2868
2869 fn successful_carrier() -> TxOutcome {
2870 let mut carrier = TxOutcome::pool_rejection("carrier".into(), "success".into());
2871 carrier.success = true;
2872 carrier.block_number = Some(10);
2873 carrier.fee_rao = Some(7);
2874 carrier
2875 }
2876
2877 #[test]
2878 fn shield_reports_inner_dispatch_outcome_and_both_fees() {
2879 for success in [false, true] {
2880 let signer = Keypair::from_seed(&[34; 32], crate::keys::CRYPTO_MLDSA65_FIXED).unwrap();
2881 let node = MockNode::new(&signer);
2882 let inner = vec![42];
2883 let events = shield_events(&node, success);
2884 {
2885 let mut state = node.state.lock().unwrap();
2886 state.included = vec![vec![41], inner.clone()];
2887 state.events = events;
2888 }
2889 let result = node
2890 .client
2891 .resolve_shielded_inner(
2892 &inner,
2893 "inner".into(),
2894 successful_carrier(),
2895 true,
2896 Duration::from_secs(5),
2897 )
2898 .unwrap();
2899 assert_eq!(result.success, success);
2900 assert_eq!(
2901 result.error.as_ref().map(|error| error.name.as_str()),
2902 if success { None } else { Some("BadOrigin") }
2903 );
2904 assert_eq!(result.extrinsic_hash, "inner");
2905 assert_eq!(result.extrinsic_index, Some(1));
2906 assert_eq!(result.fee_rao, Some(10));
2907 assert_eq!(result.data["carrier_extrinsic_hash"], Value::str("carrier"));
2908 assert_eq!(result.data["carrier_fee_rao"], Value::Uint(7));
2909 assert_eq!(result.data["inner_fee_rao"], Value::Uint(3));
2910 }
2911 }
2912
2913 #[test]
2914 fn shield_carrier_success_does_not_hide_missing_inner_inclusion() {
2915 let signer = Keypair::from_seed(&[35; 32], crate::keys::CRYPTO_MLDSA65_FIXED).unwrap();
2916 let node = MockNode::new(&signer);
2917 node.state.lock().unwrap().included = vec![vec![41]];
2918 let result = node
2919 .client
2920 .resolve_shielded_inner(
2921 &[42],
2922 "inner".into(),
2923 successful_carrier(),
2924 false,
2925 Duration::from_millis(100),
2926 )
2927 .unwrap();
2928 assert!(!result.success);
2929 assert!(result.message.contains("not included"));
2930 assert_eq!(result.extrinsic_hash, "inner");
2931 assert_eq!(result.fee_rao, Some(7));
2932 assert_eq!(result.data["carrier_extrinsic_hash"], Value::str("carrier"));
2933 assert!(node.state.lock().unwrap().block_reads > 0);
2934 }
2935
2936 #[test]
2937 fn fixed_ms_signs_queued_nonces_without_authority_records() {
2938 for crypto in [
2939 crate::keys::CRYPTO_MLDSA65_FIXED,
2940 crate::keys::CRYPTO_FALCON512,
2941 crate::keys::CRYPTO_FALCON1024,
2942 ] {
2943 let signer = Keypair::from_seed(&[31; 32], crypto).unwrap();
2944 let node = MockNode::new(&signer);
2945 let call = node.call();
2946 node.state.lock().unwrap().pending = true;
2947 for nonce in [NONCE, NONCE + 1, NONCE + 2] {
2948 let (encoded, _) = node
2949 .client
2950 .sign_extrinsic(&call, &signer, nonce, None)
2951 .unwrap();
2952 assert_proof(&node, &signer, &encoded, &call, 0, nonce);
2953 }
2954 let state = node.state.lock().unwrap();
2955 assert!(state.authority_reads.is_empty());
2956 assert!(state.account_reads.is_empty());
2957 }
2958 }
2959
2960 #[test]
2961 fn fixed_ms_shield_signs_both_nonces_with_the_same_key() {
2962 for crypto in [
2963 crate::keys::CRYPTO_MLDSA65_FIXED,
2964 crate::keys::CRYPTO_FALCON512,
2965 crate::keys::CRYPTO_FALCON1024,
2966 ] {
2967 let signer = Keypair::from_seed(&[32; 32], crypto).unwrap();
2968 let node = MockNode::new(&signer);
2969 let call = node.call();
2970 let outcome = node.client.submit_shielded(&call, &signer, false).unwrap();
2971 assert!(!outcome.success); let state = node.state.lock().unwrap();
2973 assert!(state.authority_reads.is_empty());
2974 assert!(state.account_reads.is_empty());
2975 assert_eq!(state.finalized_reads, 1);
2976 let outer = &state.submitted[0];
2977 let runtime = node.client.runtime().unwrap();
2978 let decoded = runtime.decode_extrinsic(outer, true).unwrap();
2979 let Value::List(args) = field(field(&decoded, "call").unwrap(), "call_args").unwrap()
2980 else {
2981 panic!("decoded call arguments")
2982 };
2983 let ciphertext = args
2984 .iter()
2985 .find_map(|arg| {
2986 (field(arg, "name") == Some(&Value::str("ciphertext")))
2987 .then(|| value_bytes(field(arg, "value").unwrap()).unwrap())
2988 })
2989 .unwrap();
2990 let inner = node.decrypt(&ciphertext);
2991 let outer_call = runtime
2992 .compose_call(
2993 "MevShield",
2994 "submit_encrypted",
2995 &Value::record(vec![("ciphertext".into(), Value::Bytes(ciphertext))]),
2996 )
2997 .unwrap();
2998 assert_proof(&node, &signer, outer, &outer_call, 0, NONCE);
2999 assert_proof(&node, &signer, &inner, &call, 0, NONCE + 1);
3000 }
3001 }
3002
3003 #[test]
3004 fn shield_uses_short_mortal_eras_for_every_wallet_mode() {
3005 for crypto in [0, 1, 4, 6, 7, 8] {
3006 let signer = Keypair::from_seed(&[36; 32], crypto).unwrap();
3007 let fixture = Keypair::from_seed(&[36; 32], crate::keys::CRYPTO_HASHED).unwrap();
3008 let node = MockNode::new(if crypto <= 1 { &fixture } else { &signer });
3009 let call = node.call();
3010 let outcome = node.client.submit_shielded(&call, &signer, false).unwrap();
3011 assert!(!outcome.success); let outer = node.state.lock().unwrap().submitted[0].clone();
3013 let runtime = node.client.runtime().unwrap();
3014 let decoded = runtime.decode_extrinsic(&outer, true).unwrap();
3015 let Value::List(args) = field(field(&decoded, "call").unwrap(), "call_args").unwrap()
3016 else {
3017 panic!("decoded call arguments")
3018 };
3019 let ciphertext = args
3020 .iter()
3021 .find_map(|arg| {
3022 (field(arg, "name") == Some(&Value::str("ciphertext")))
3023 .then(|| value_bytes(field(arg, "value").unwrap()).unwrap())
3024 })
3025 .unwrap();
3026 let inner = node.decrypt(&ciphertext);
3027 for extrinsic in [&outer, &inner] {
3028 let decoded = runtime.decode_extrinsic(extrinsic, true).unwrap();
3029 let era = field(&decoded, "era").unwrap();
3030 assert_eq!(tuple_item(era, 0).and_then(as_u128), Some(8));
3031 }
3032 let (ordinary, _) = node
3033 .client
3034 .sign_extrinsic(&call, &signer, NONCE, Some(DEFAULT_ERA_PERIOD))
3035 .unwrap();
3036 let decoded = runtime.decode_extrinsic(&ordinary, true).unwrap();
3037 assert_eq!(
3038 tuple_item(field(&decoded, "era").unwrap(), 0).and_then(as_u128),
3039 Some(64)
3040 );
3041 }
3042 }
3043
3044 #[test]
3045 fn fixed_ms_shield_rejects_exhausted_inner_nonce_before_submitting() {
3046 for crypto in [
3047 crate::keys::CRYPTO_MLDSA65_FIXED,
3048 crate::keys::CRYPTO_FALCON512,
3049 crate::keys::CRYPTO_FALCON1024,
3050 ] {
3051 let signer = Keypair::from_seed(&[33; 32], crypto).unwrap();
3052 let node = MockNode::new(&signer);
3053 node.state.lock().unwrap().nonce_override = Some(u64::from(u32::MAX) - 1);
3054 let error = node
3055 .client
3056 .submit_shielded(&node.call(), &signer, false)
3057 .unwrap_err();
3058 assert!(error.to_string().contains("nonce exhausted"));
3059 assert!(node.state.lock().unwrap().submitted.is_empty());
3060 }
3061 }
3062
3063 #[test]
3064 fn standard_keys_keep_explicit_nonce_signing_without_registered_authority() {
3065 for crypto in [crate::keys::CRYPTO_ED25519, crate::keys::CRYPTO_SR25519] {
3066 let signer = Keypair::from_seed(&[26; 32], crypto).unwrap();
3067 let fixture = Keypair::from_seed(&[26; 32], crate::keys::CRYPTO_HASHED).unwrap();
3068 let node = MockNode::new(&fixture);
3069 let (encoded, _) = node
3070 .client
3071 .sign_extrinsic(&node.call(), &signer, NONCE + 7, None)
3072 .unwrap();
3073 let decoded = node
3074 .client
3075 .runtime()
3076 .unwrap()
3077 .decode_extrinsic(&encoded, true)
3078 .unwrap();
3079 assert_eq!(
3080 field(&decoded, "nonce").and_then(as_u128),
3081 Some(u128::from(NONCE + 7))
3082 );
3083 let state = node.state.lock().unwrap();
3084 assert_eq!(state.finalized_reads, 0);
3085 assert_eq!(state.nonce_reads, 0);
3086 assert!(state.authority_reads.is_empty());
3087 assert!(state.account_reads.is_empty());
3088 }
3089 }
3090
3091 #[test]
3092 fn shield_pair_pins_authority_and_nonce_when_finality_advances() {
3093 for crypto in wallet_modes() {
3094 let signer = Keypair::from_seed(&[27; 32], crypto).unwrap();
3095 let node = MockNode::new(&signer);
3096 node.state.lock().unwrap().advance_after_inner = true;
3097 let call = node.call();
3098 let outcome = node.client.submit_shielded(&call, &signer, false).unwrap();
3099 assert!(!outcome.success); let state = node.state.lock().unwrap();
3101 assert!(state.advanced);
3102 assert_eq!(state.finalized_reads, 1);
3103 assert_eq!(state.authority_reads, [hex_prefixed(&PINNED_HASH)]);
3104 assert_eq!(state.account_reads, [hex_prefixed(&PINNED_HASH)]);
3105 assert_eq!(state.submitted.len(), 1);
3106 let outer = &state.submitted[0];
3107 let decoded = node
3108 .client
3109 .runtime()
3110 .unwrap()
3111 .decode_extrinsic(outer, true)
3112 .unwrap();
3113 let outer_call = field(&decoded, "call").unwrap();
3114 let args = field(outer_call, "call_args").unwrap();
3115 let Value::List(args) = args else {
3116 panic!("decoded call arguments")
3117 };
3118 let ciphertext = args
3119 .iter()
3120 .find_map(|arg| {
3121 (field(arg, "name") == Some(&Value::str("ciphertext")))
3122 .then(|| value_bytes(field(arg, "value").unwrap()).unwrap())
3123 })
3124 .unwrap();
3125 let inner = node.decrypt(&ciphertext);
3126 let encoded_outer = node
3127 .client
3128 .compose_call(
3129 "MevShield",
3130 "submit_encrypted",
3131 &Value::record(vec![("ciphertext".into(), Value::Bytes(ciphertext))]),
3132 )
3133 .unwrap();
3134 assert_proof(&node, &signer, outer, &encoded_outer, GENERATION, NONCE);
3135 assert_proof(&node, &signer, &inner, &call, GENERATION + 1, NONCE + 1);
3136 }
3137 }
3138
3139 #[test]
3140 fn shield_rejects_nonce_consumed_between_inner_and_carrier() {
3141 for crypto in wallet_modes() {
3142 let signer = Keypair::from_seed(&[28; 32], crypto).unwrap();
3143 let node = MockNode::new(&signer);
3144 {
3145 let mut state = node.state.lock().unwrap();
3146 state.advance_after_inner = true;
3147 state.consume_nonce_on_advance = true;
3148 }
3149 let error = node
3150 .client
3151 .submit_shielded(&node.call(), &signer, false)
3152 .unwrap_err();
3153 assert!(error.to_string().contains("pending transaction"));
3154 let state = node.state.lock().unwrap();
3155 assert!(state.advanced);
3156 assert_eq!(state.authority_reads, [hex_prefixed(&PINNED_HASH)]);
3157 assert!(state.submitted.is_empty());
3158 }
3159 }
3160
3161 #[test]
3162 fn registered_submissions_reject_pending_and_explicit_future_nonces_then_refresh() {
3163 for crypto in wallet_modes() {
3164 let signer = Keypair::from_seed(&[29; 32], crypto).unwrap();
3165 let node = MockNode::new(&signer);
3166 let call = node.call();
3167 node.state.lock().unwrap().pending = true;
3168 let error = node
3169 .client
3170 .submit(&call, &signer, None, Some(DEFAULT_ERA_PERIOD), false)
3171 .unwrap_err();
3172 assert!(error.to_string().contains("pending transaction"));
3173 node.state.lock().unwrap().pending = false;
3174 let error = node
3175 .client
3176 .sign_extrinsic(&call, &signer, NONCE + 1, Some(DEFAULT_ERA_PERIOD))
3177 .unwrap_err();
3178 assert!(error.to_string().contains("current on-chain nonce"));
3179 let (first, _) = node
3180 .client
3181 .sign_extrinsic(&call, &signer, NONCE, Some(DEFAULT_ERA_PERIOD))
3182 .unwrap();
3183 assert_proof(&node, &signer, &first, &call, GENERATION, NONCE);
3184 {
3185 let mut state = node.state.lock().unwrap();
3186 state.advanced = true;
3187 state.consume_nonce_on_advance = true;
3188 }
3189 let (next, _) = node
3190 .client
3191 .sign_extrinsic(&call, &signer, NONCE + 1, None)
3192 .unwrap();
3193 assert_proof(&node, &signer, &next, &call, GENERATION + 1, NONCE + 1);
3194 assert!(node.state.lock().unwrap().submitted.is_empty());
3195 }
3196 }
3197}