1use std::collections::HashMap;
38use std::fmt;
39use std::future::Future;
40use std::str::FromStr;
41use std::sync::{Mutex, OnceLock};
42use std::time::{Duration, Instant};
43
44use serde_json::Value;
45
46use crate::error::{Error, Result};
47use crate::query::RestClient;
48
49pub const HYBRID_SIGN_BYTES_DOMAIN: &str = "qorechain-pqc-hybrid-v2";
51
52pub const MIGRATION_SIGN_BYTES_DOMAIN: &str = "qorechain-key-migration-v2";
54
55pub const BRIDGE_ATTESTATION_SIGN_BYTES_DOMAIN: &str = "qorechain-bridge-attestation-v2";
57
58pub const SIGN_BYTES_V2_UPGRADE: &str = "v3.2.0";
61
62pub const SIGN_BYTES_V2_UPGRADES: &[&str] = &["v3.2.0", "v3.1.98"];
68
69pub const LEGACY_SIGN_BYTES_CHAINS: &[&str] = &["qorechain-vladi", "qorechain-diana"];
72
73pub const DEFAULT_SIGN_BYTES_CACHE_TTL: Duration = Duration::from_secs(60);
75
76pub const PQC_CODESPACE: &str = "pqc";
78
79pub const PQC_HYBRID_VERIFY_FAILED_CODE: u32 = 21;
81
82pub const PQC_HYBRID_VERIFY_FAILED_MESSAGE: &str = "hybrid PQC signature verification failed";
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
87pub enum SignBytesVersion {
88 V1,
90 V2,
92}
93
94impl SignBytesVersion {
95 pub fn number(self) -> u8 {
97 match self {
98 SignBytesVersion::V1 => 1,
99 SignBytesVersion::V2 => 2,
100 }
101 }
102
103 pub fn as_str(self) -> &'static str {
105 match self {
106 SignBytesVersion::V1 => "v1",
107 SignBytesVersion::V2 => "v2",
108 }
109 }
110}
111
112impl fmt::Display for SignBytesVersion {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 f.write_str(self.as_str())
115 }
116}
117
118impl FromStr for SignBytesVersion {
119 type Err = Error;
120
121 fn from_str(s: &str) -> Result<Self> {
122 match s {
123 "v1" => Ok(SignBytesVersion::V1),
124 "v2" => Ok(SignBytesVersion::V2),
125 other => Err(Error::SignBytes(format!(
126 "sign-bytes version must be v1 or v2, got {other:?}"
127 ))),
128 }
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
134pub enum SignBytesMode {
135 #[default]
137 Auto,
138 V1,
140 V2,
142}
143
144impl SignBytesMode {
145 pub fn fixed(self) -> Option<SignBytesVersion> {
147 match self {
148 SignBytesMode::Auto => None,
149 SignBytesMode::V1 => Some(SignBytesVersion::V1),
150 SignBytesMode::V2 => Some(SignBytesVersion::V2),
151 }
152 }
153}
154
155impl From<SignBytesVersion> for SignBytesMode {
156 fn from(v: SignBytesVersion) -> Self {
157 match v {
158 SignBytesVersion::V1 => SignBytesMode::V1,
159 SignBytesVersion::V2 => SignBytesMode::V2,
160 }
161 }
162}
163
164impl fmt::Display for SignBytesMode {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 f.write_str(match self {
167 SignBytesMode::Auto => "auto",
168 SignBytesMode::V1 => "v1",
169 SignBytesMode::V2 => "v2",
170 })
171 }
172}
173
174impl FromStr for SignBytesMode {
175 type Err = Error;
176
177 fn from_str(s: &str) -> Result<Self> {
180 match s {
181 "" | "auto" => Ok(SignBytesMode::Auto),
182 "v1" => Ok(SignBytesMode::V1),
183 "v2" => Ok(SignBytesMode::V2),
184 other => Err(Error::SignBytes(format!(
185 "sign-bytes mode must be auto, v1 or v2, got {other:?}"
186 ))),
187 }
188 }
189}
190
191pub fn is_legacy_sign_bytes_chain(chain_id: &str) -> bool {
197 LEGACY_SIGN_BYTES_CHAINS.contains(&chain_id)
198}
199
200pub fn sign_bytes_version_for(chain_id: &str, v2_applied_height: i64) -> SignBytesVersion {
207 if v2_applied_height > 0 || !is_legacy_sign_bytes_chain(chain_id) {
208 SignBytesVersion::V2
209 } else {
210 SignBytesVersion::V1
211 }
212}
213
214pub fn require_sign_bytes_version(
219 chain_id: &str,
220 version: Option<SignBytesVersion>,
221) -> Result<SignBytesVersion> {
222 match version {
223 Some(v) => Ok(v),
224 None if !is_legacy_sign_bytes_chain(chain_id) => Ok(SignBytesVersion::V2),
225 None => Err(Error::SignBytes(format!(
226 "chain {chain_id:?} verifies hybrid sign-bytes v1 until upgrade {} is applied \
227 and v2 after it, so the version cannot be chosen offline: pass an explicit \
228 sign-bytes version (v1 or v2), or resolve one with SignBytesResolver / an async \
229 sign-and-broadcast path given a REST URL",
230 upgrade_names()
231 ))),
232 }
233}
234
235pub fn hybrid_sign_bytes_v1(body_without_pqc_ext: &[u8], auth_info: &[u8]) -> Vec<u8> {
241 let mut out = Vec::with_capacity(8 + body_without_pqc_ext.len() + auth_info.len());
242 push_be32_prefixed(&mut out, body_without_pqc_ext);
243 push_be32_prefixed(&mut out, auth_info);
244 out
245}
246
247pub fn hybrid_sign_bytes_v2(
250 chain_id: &str,
251 body_without_pqc_ext: &[u8],
252 auth_info: &[u8],
253) -> Vec<u8> {
254 let mut out = Vec::with_capacity(
255 HYBRID_SIGN_BYTES_DOMAIN.len()
256 + 8
257 + chain_id.len()
258 + 8
259 + body_without_pqc_ext.len()
260 + auth_info.len(),
261 );
262 out.extend_from_slice(HYBRID_SIGN_BYTES_DOMAIN.as_bytes());
263 push_be64_prefixed(&mut out, chain_id.as_bytes());
264 push_be32_prefixed(&mut out, body_without_pqc_ext);
265 push_be32_prefixed(&mut out, auth_info);
266 out
267}
268
269pub fn hybrid_sign_bytes(
271 version: SignBytesVersion,
272 chain_id: &str,
273 body_without_pqc_ext: &[u8],
274 auth_info: &[u8],
275) -> Vec<u8> {
276 match version {
277 SignBytesVersion::V1 => hybrid_sign_bytes_v1(body_without_pqc_ext, auth_info),
278 SignBytesVersion::V2 => hybrid_sign_bytes_v2(chain_id, body_without_pqc_ext, auth_info),
279 }
280}
281
282#[derive(Debug, Clone, Copy)]
288pub struct MigrationSignFields<'a> {
289 pub chain_id: &'a str,
291 pub account: &'a str,
293 pub from_algorithm_id: u32,
295 pub to_algorithm_id: u32,
297 pub execution_height: i64,
299 pub old_public_key: &'a [u8],
301 pub new_public_key: &'a [u8],
303}
304
305pub fn migration_sign_bytes_v1(f: &MigrationSignFields<'_>) -> Vec<u8> {
309 format!(
310 "qorechain-key-migration:chain={}:from={}:to={}:account={}:height={}",
311 f.chain_id, f.from_algorithm_id, f.to_algorithm_id, f.account, f.execution_height
312 )
313 .into_bytes()
314}
315
316pub fn migration_sign_bytes_v2(f: &MigrationSignFields<'_>) -> Vec<u8> {
320 let mut out = Vec::with_capacity(
321 MIGRATION_SIGN_BYTES_DOMAIN.len()
322 + 8
323 + f.chain_id.len()
324 + 8
325 + f.account.len()
326 + 16
327 + 8
328 + f.old_public_key.len()
329 + f.new_public_key.len(),
330 );
331 out.extend_from_slice(MIGRATION_SIGN_BYTES_DOMAIN.as_bytes());
332 push_be64_prefixed(&mut out, f.chain_id.as_bytes());
333 push_be64_prefixed(&mut out, f.account.as_bytes());
334 out.extend_from_slice(&f.from_algorithm_id.to_be_bytes());
335 out.extend_from_slice(&f.to_algorithm_id.to_be_bytes());
336 out.extend_from_slice(&(f.execution_height as u64).to_be_bytes());
338 push_be32_prefixed(&mut out, f.old_public_key);
339 push_be32_prefixed(&mut out, f.new_public_key);
340 out
341}
342
343pub fn migration_sign_bytes(version: SignBytesVersion, f: &MigrationSignFields<'_>) -> Vec<u8> {
345 match version {
346 SignBytesVersion::V1 => migration_sign_bytes_v1(f),
347 SignBytesVersion::V2 => migration_sign_bytes_v2(f),
348 }
349}
350
351#[derive(Debug, Clone, Copy)]
357pub struct BridgeAttestationSignFields<'a> {
358 pub chain: &'a str,
360 pub event_type: &'a str,
362 pub operation_id: &'a str,
364 pub tx_hash: &'a str,
366 pub amount: &'a str,
368 pub asset: &'a str,
370}
371
372pub fn bridge_attestation_sign_bytes_v1(f: &BridgeAttestationSignFields<'_>) -> Vec<u8> {
375 format!(
376 "{}|{}|{}|{}|{}|{}",
377 f.chain, f.event_type, f.operation_id, f.tx_hash, f.amount, f.asset
378 )
379 .into_bytes()
380}
381
382pub fn bridge_attestation_sign_bytes_v2(
386 chain_id: &str,
387 f: &BridgeAttestationSignFields<'_>,
388) -> Vec<u8> {
389 let fields = [
390 chain_id,
391 f.chain,
392 f.event_type,
393 f.operation_id,
394 f.tx_hash,
395 f.amount,
396 f.asset,
397 ];
398 let mut out = Vec::with_capacity(
399 BRIDGE_ATTESTATION_SIGN_BYTES_DOMAIN.len()
400 + fields.iter().map(|s| 8 + s.len()).sum::<usize>(),
401 );
402 out.extend_from_slice(BRIDGE_ATTESTATION_SIGN_BYTES_DOMAIN.as_bytes());
403 for field in fields {
404 push_be64_prefixed(&mut out, field.as_bytes());
405 }
406 out
407}
408
409pub fn bridge_attestation_sign_bytes(
411 version: SignBytesVersion,
412 chain_id: &str,
413 f: &BridgeAttestationSignFields<'_>,
414) -> Vec<u8> {
415 match version {
416 SignBytesVersion::V1 => bridge_attestation_sign_bytes_v1(f),
417 SignBytesVersion::V2 => bridge_attestation_sign_bytes_v2(chain_id, f),
418 }
419}
420
421#[derive(Debug)]
436pub struct SignBytesResolver {
437 http: reqwest::Client,
438 ttl: Duration,
439 cache: Mutex<HashMap<(String, String), (SignBytesVersion, Instant)>>,
440}
441
442impl Default for SignBytesResolver {
443 fn default() -> Self {
444 Self::new()
445 }
446}
447
448impl SignBytesResolver {
449 pub fn new() -> Self {
451 Self::with_client(reqwest::Client::new())
452 }
453
454 pub fn with_client(http: reqwest::Client) -> Self {
456 Self {
457 http,
458 ttl: DEFAULT_SIGN_BYTES_CACHE_TTL,
459 cache: Mutex::new(HashMap::new()),
460 }
461 }
462
463 pub fn with_ttl(mut self, ttl: Duration) -> Self {
465 self.ttl = ttl;
466 self
467 }
468
469 pub fn ttl(&self) -> Duration {
471 self.ttl
472 }
473
474 pub async fn resolve(
482 &self,
483 mode: SignBytesMode,
484 chain_id: &str,
485 rest_url: Option<&str>,
486 ) -> Result<SignBytesVersion> {
487 if let Some(v) = mode.fixed() {
488 return Ok(v);
489 }
490 if !is_legacy_sign_bytes_chain(chain_id) {
491 return Ok(SignBytesVersion::V2);
492 }
493 let rest_url = require_rest_url(chain_id, rest_url)?;
494 if let Some(v) = self.cached(rest_url, chain_id) {
495 return Ok(v);
496 }
497 self.fetch_and_store(chain_id, rest_url).await
498 }
499
500 pub async fn force_refresh(
503 &self,
504 chain_id: &str,
505 rest_url: Option<&str>,
506 ) -> Result<SignBytesVersion> {
507 if !is_legacy_sign_bytes_chain(chain_id) {
508 return Ok(SignBytesVersion::V2);
509 }
510 let rest_url = require_rest_url(chain_id, rest_url)?;
511 self.invalidate(rest_url, chain_id);
512 self.fetch_and_store(chain_id, rest_url).await
513 }
514
515 pub fn invalidate(&self, rest_url: &str, chain_id: &str) {
517 self.lock_cache().remove(&cache_key(rest_url, chain_id));
518 }
519
520 pub fn clear_cache(&self) {
522 self.lock_cache().clear();
523 }
524
525 pub async fn fetch_v2_applied_height(
532 &self,
533 rest_url: &str,
534 plan_name: Option<&str>,
535 ) -> Result<i64> {
536 let plan_name = plan_name.unwrap_or(SIGN_BYTES_V2_UPGRADE);
537 let rest = RestClient::with_client(rest_url, self.http.clone());
538 let path = format!("/cosmos/upgrade/v1beta1/applied_plan/{plan_name}");
539 let body = rest.get(&path, &[]).await.map_err(|e| {
540 Error::SignBytes(format!(
541 "cannot ask {rest_url} whether upgrade {plan_name} is applied (the v2 \
542 sign-bytes upgrade ships as {}) ({e}); pass an explicit sign-bytes version \
543 (v1 or v2)",
544 upgrade_names()
545 ))
546 })?;
547 parse_applied_height(&body)
548 }
549
550 pub async fn fetch_v2_applied_height_any(&self, rest_url: &str) -> Result<i64> {
557 for plan_name in SIGN_BYTES_V2_UPGRADES {
558 let height = self
559 .fetch_v2_applied_height(rest_url, Some(plan_name))
560 .await?;
561 if height > 0 {
562 return Ok(height);
563 }
564 }
565 Ok(0)
566 }
567
568 async fn fetch_and_store(&self, chain_id: &str, rest_url: &str) -> Result<SignBytesVersion> {
569 let height = self.fetch_v2_applied_height_any(rest_url).await?;
570 let v = sign_bytes_version_for(chain_id, height);
571 if !self.ttl.is_zero() {
572 self.lock_cache()
573 .insert(cache_key(rest_url, chain_id), (v, Instant::now()));
574 }
575 Ok(v)
576 }
577
578 fn cached(&self, rest_url: &str, chain_id: &str) -> Option<SignBytesVersion> {
579 let key = cache_key(rest_url, chain_id);
580 let mut cache = self.lock_cache();
581 match cache.get(&key) {
582 Some((v, at)) if at.elapsed() < self.ttl => Some(*v),
583 Some(_) => {
584 cache.remove(&key);
585 None
586 }
587 None => None,
588 }
589 }
590
591 fn lock_cache(
592 &self,
593 ) -> std::sync::MutexGuard<'_, HashMap<(String, String), (SignBytesVersion, Instant)>> {
594 self.cache.lock().unwrap_or_else(|p| p.into_inner())
597 }
598}
599
600pub fn default_sign_bytes_resolver() -> &'static SignBytesResolver {
602 static RESOLVER: OnceLock<SignBytesResolver> = OnceLock::new();
603 RESOLVER.get_or_init(SignBytesResolver::new)
604}
605
606pub async fn resolve_sign_bytes_version(
608 mode: SignBytesMode,
609 chain_id: &str,
610 rest_url: Option<&str>,
611) -> Result<SignBytesVersion> {
612 default_sign_bytes_resolver()
613 .resolve(mode, chain_id, rest_url)
614 .await
615}
616
617pub fn clear_sign_bytes_cache() {
619 default_sign_bytes_resolver().clear_cache();
620}
621
622pub fn parse_applied_height(body: &Value) -> Result<i64> {
625 match body.get("height") {
626 None | Some(Value::Null) => Ok(0),
627 Some(Value::String(s)) if s.is_empty() => Ok(0),
628 Some(Value::String(s)) => s
629 .trim()
630 .parse::<i64>()
631 .map_err(|_| Error::SignBytes(format!("applied_plan height is not an integer: {s:?}"))),
632 Some(Value::Number(n)) => n
633 .as_i64()
634 .ok_or_else(|| Error::SignBytes(format!("applied_plan height is not an integer: {n}"))),
635 Some(other) => Err(Error::SignBytes(format!(
636 "applied_plan height has an unexpected type: {other}"
637 ))),
638 }
639}
640
641pub fn is_hybrid_sign_bytes_rejection(codespace: &str, code: u32, log: &str) -> bool {
649 (codespace == PQC_CODESPACE && code == PQC_HYBRID_VERIFY_FAILED_CODE)
650 || log.contains(PQC_HYBRID_VERIFY_FAILED_MESSAGE)
651}
652
653pub fn is_hybrid_sign_bytes_rejection_response(resp: &Value) -> bool {
656 [resp.get("tx_response"), Some(resp)]
657 .into_iter()
658 .flatten()
659 .any(|obj| {
660 let code = obj
661 .get("code")
662 .and_then(Value::as_u64)
663 .and_then(|c| u32::try_from(c).ok())
664 .unwrap_or(0);
665 let codespace = obj.get("codespace").and_then(Value::as_str).unwrap_or("");
666 let log = ["raw_log", "log", "message"]
667 .iter()
668 .filter_map(|k| obj.get(*k).and_then(Value::as_str))
669 .collect::<Vec<_>>()
670 .join("\n");
671 is_hybrid_sign_bytes_rejection(codespace, code, &log)
672 })
673}
674
675pub fn is_hybrid_sign_bytes_rejection_error(err: &Error) -> bool {
677 match err {
678 Error::Tx(e) => is_hybrid_sign_bytes_rejection(
679 &e.codespace,
680 e.code,
681 &format!("{}\n{}", e.raw_log, e.reason),
682 ),
683 Error::Http { body, .. } => match serde_json::from_str::<Value>(body) {
684 Ok(v) => is_hybrid_sign_bytes_rejection_response(&v),
685 Err(_) => body.contains(PQC_HYBRID_VERIFY_FAILED_MESSAGE),
686 },
687 Error::JsonRpc { message, .. } => message.contains(PQC_HYBRID_VERIFY_FAILED_MESSAGE),
688 _ => false,
689 }
690}
691
692#[derive(Debug, Clone)]
694pub struct HybridBroadcast {
695 pub response: Value,
697 pub sign_bytes_version: SignBytesVersion,
699 pub retried: bool,
701}
702
703pub async fn broadcast_with_sign_bytes_retry<B, S, Fut>(
713 resolver: &SignBytesResolver,
714 mode: SignBytesMode,
715 chain_id: &str,
716 rest_url: Option<&str>,
717 mut build: B,
718 mut send: S,
719) -> Result<HybridBroadcast>
720where
721 B: FnMut(SignBytesVersion) -> Result<Vec<u8>>,
722 S: FnMut(Vec<u8>) -> Fut,
723 Fut: Future<Output = Result<Value>>,
724{
725 let version = resolver.resolve(mode, chain_id, rest_url).await?;
726 let first = send(build(version)?).await;
727 let refused = match &first {
728 Ok(resp) => is_hybrid_sign_bytes_rejection_response(resp),
729 Err(e) => is_hybrid_sign_bytes_rejection_error(e),
730 };
731 if mode != SignBytesMode::Auto || !refused {
732 return first.map(|response| HybridBroadcast {
733 response,
734 sign_bytes_version: version,
735 retried: false,
736 });
737 }
738 let version = resolver.force_refresh(chain_id, rest_url).await?;
739 let response = send(build(version)?).await?;
740 Ok(HybridBroadcast {
741 response,
742 sign_bytes_version: version,
743 retried: true,
744 })
745}
746
747fn push_be32_prefixed(out: &mut Vec<u8>, bytes: &[u8]) {
752 out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
753 out.extend_from_slice(bytes);
754}
755
756fn push_be64_prefixed(out: &mut Vec<u8>, bytes: &[u8]) {
757 out.extend_from_slice(&(bytes.len() as u64).to_be_bytes());
758 out.extend_from_slice(bytes);
759}
760
761fn cache_key(rest_url: &str, chain_id: &str) -> (String, String) {
762 (
763 rest_url.trim_end_matches('/').to_string(),
764 chain_id.to_string(),
765 )
766}
767
768fn require_rest_url<'a>(chain_id: &str, rest_url: Option<&'a str>) -> Result<&'a str> {
769 match rest_url {
770 Some(u) if !u.trim().is_empty() => Ok(u),
771 _ => Err(Error::SignBytes(format!(
772 "chain {chain_id:?} verifies hybrid sign-bytes v1 until upgrade {} is applied \
773 and v2 after it; to choose, the SDK must ask a node — pass a REST URL, or an \
774 explicit sign-bytes version (v1 or v2)",
775 upgrade_names()
776 ))),
777 }
778}
779
780fn upgrade_names() -> String {
782 SIGN_BYTES_V2_UPGRADES.join(" or ")
783}
784
785#[cfg(test)]
786mod tests {
787 use super::*;
788 use serde_json::json;
789
790 #[test]
791 fn mode_and_version_parse() {
792 assert_eq!(
793 "auto".parse::<SignBytesMode>().unwrap(),
794 SignBytesMode::Auto
795 );
796 assert_eq!("".parse::<SignBytesMode>().unwrap(), SignBytesMode::Auto);
797 assert_eq!("v1".parse::<SignBytesMode>().unwrap(), SignBytesMode::V1);
798 assert_eq!("v2".parse::<SignBytesMode>().unwrap(), SignBytesMode::V2);
799 assert!("v3".parse::<SignBytesMode>().is_err());
800 assert_eq!(
801 "v2".parse::<SignBytesVersion>().unwrap(),
802 SignBytesVersion::V2
803 );
804 assert!("auto".parse::<SignBytesVersion>().is_err());
805 assert_eq!(SignBytesMode::default(), SignBytesMode::Auto);
806 assert_eq!(SignBytesVersion::V1.number(), 1);
807 assert_eq!(SignBytesVersion::V2.to_string(), "v2");
808 }
809
810 #[test]
811 fn applied_height_parsing() {
812 assert_eq!(
813 parse_applied_height(&json!({"height": "5746000"})).unwrap(),
814 5_746_000
815 );
816 assert_eq!(parse_applied_height(&json!({"height": "0"})).unwrap(), 0);
817 assert_eq!(parse_applied_height(&json!({})).unwrap(), 0);
818 assert_eq!(parse_applied_height(&json!({"height": 12})).unwrap(), 12);
819 assert!(parse_applied_height(&json!({"height": "abc"})).is_err());
820 }
821
822 #[test]
823 fn require_version_fails_loudly_on_legacy_chain() {
824 assert_eq!(
825 require_sign_bytes_version("qorechain-new", None).unwrap(),
826 SignBytesVersion::V2
827 );
828 assert_eq!(
829 require_sign_bytes_version("qorechain-vladi", Some(SignBytesVersion::V1)).unwrap(),
830 SignBytesVersion::V1
831 );
832 let err = require_sign_bytes_version("qorechain-vladi", None).unwrap_err();
833 assert!(matches!(err, Error::SignBytes(_)), "{err:?}");
834 }
835}