use crate::error::{QuantusError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CompatibleRuntime {
pub spec_version: u32,
pub transaction_version: u32,
pub supports_ml_dsa_65: bool,
}
pub const EXPECTED_RUNTIME_SPEC_NAME: &str = "quantus-runtime";
pub const COMPATIBLE_RUNTIMES: &[CompatibleRuntime] = &[
CompatibleRuntime { spec_version: 134, transaction_version: 2, supports_ml_dsa_65: false },
CompatibleRuntime { spec_version: 135, transaction_version: 2, supports_ml_dsa_65: false },
CompatibleRuntime { spec_version: 135, transaction_version: 3, supports_ml_dsa_65: false },
CompatibleRuntime { spec_version: 136, transaction_version: 3, supports_ml_dsa_65: false },
CompatibleRuntime { spec_version: 142, transaction_version: 3, supports_ml_dsa_65: true },
CompatibleRuntime { spec_version: 143, transaction_version: 3, supports_ml_dsa_65: true },
];
pub fn is_runtime_compatible(spec_version: u32, transaction_version: u32) -> bool {
COMPATIBLE_RUNTIMES.iter().any(|runtime| {
runtime.spec_version == spec_version && runtime.transaction_version == transaction_version
})
}
pub fn runtime_supports_ml_dsa_65(spec_version: u32, transaction_version: u32) -> bool {
COMPATIBLE_RUNTIMES.iter().any(|runtime| {
runtime.spec_version == spec_version &&
runtime.transaction_version == transaction_version &&
runtime.supports_ml_dsa_65
})
}
pub fn validate_runtime_identity(
spec_name: &str,
spec_version: u32,
transaction_version: u32,
) -> Result<()> {
if spec_name != EXPECTED_RUNTIME_SPEC_NAME ||
!is_runtime_compatible(spec_version, transaction_version)
{
return Err(QuantusError::NetworkError(format!(
"Unsupported Quantus runtime: specName={spec_name}, specVersion={spec_version}, transactionVersion={transaction_version}"
)));
}
Ok(())
}
pub fn ensure_ml_dsa_65_supported(spec_version: u32, transaction_version: u32) -> Result<()> {
if runtime_supports_ml_dsa_65(spec_version, transaction_version) {
return Ok(());
}
Err(QuantusError::NetworkError(format!(
"ML-DSA-65 wallets require a runtime that supports Dilithium65 signatures \
(spec 142+); connected node is specVersion={spec_version}, transactionVersion={transaction_version}. \
Use --scheme ml-dsa-87 or upgrade the node."
)))
}
pub fn validate_runtime_version_value(runtime_version: &serde_json::Value) -> Result<()> {
let spec_name = runtime_version["specName"].as_str().ok_or_else(|| {
QuantusError::NetworkError("Failed to parse runtime spec name".to_string())
})?;
let spec_version = runtime_version["specVersion"]
.as_u64()
.ok_or_else(|| QuantusError::NetworkError("Failed to parse spec version".to_string()))?
as u32;
let transaction_version = runtime_version["transactionVersion"].as_u64().ok_or_else(|| {
QuantusError::NetworkError("Failed to parse transaction version".to_string())
})? as u32;
validate_runtime_identity(spec_name, spec_version, transaction_version)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn validate_runtime_identity_accepts_compatible_quantus_runtime() {
validate_runtime_identity(EXPECTED_RUNTIME_SPEC_NAME, 136, 3)
.expect("compatible quantus runtime must be accepted");
validate_runtime_identity(EXPECTED_RUNTIME_SPEC_NAME, 142, 3)
.expect("vesting-enabled runtime must be accepted");
validate_runtime_identity(EXPECTED_RUNTIME_SPEC_NAME, 143, 3)
.expect("the current runtime must be accepted");
}
#[test]
fn validate_runtime_identity_accepts_real_quantus_runtime_spec_name() {
validate_runtime_identity("quantus-runtime", 136, 3)
.expect("the real runtime spec name 'quantus-runtime' must be accepted");
}
#[test]
fn validate_runtime_identity_rejects_wrong_spec_name() {
let err = validate_runtime_identity("quantus-impersonator", 136, 3).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Unsupported Quantus runtime") && msg.contains("quantus-impersonator"),
"expected wrong-spec-name rejection, got: {msg}"
);
}
#[test]
fn validate_runtime_identity_rejects_incompatible_runtime_versions() {
let err =
validate_runtime_identity(EXPECTED_RUNTIME_SPEC_NAME, 999_999, 999_999).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Unsupported Quantus runtime") &&
msg.contains("999999") &&
msg.contains(EXPECTED_RUNTIME_SPEC_NAME),
"expected incompatible-version rejection, got: {msg}"
);
}
#[test]
fn validate_runtime_version_value_rejects_wrong_spec_name() {
let value = json!({
"specName": "polkadot",
"specVersion": 136,
"transactionVersion": 3,
});
let err = validate_runtime_version_value(&value).unwrap_err();
assert!(
err.to_string().contains("polkadot"),
"expected wrong-spec-name rejection via JSON helper"
);
}
#[test]
fn validate_runtime_version_value_rejects_incompatible_runtime() {
let value = json!({
"specName": EXPECTED_RUNTIME_SPEC_NAME,
"specVersion": 1,
"transactionVersion": 1,
});
assert!(validate_runtime_version_value(&value).is_err());
}
#[test]
fn ml_dsa_65_supported_only_on_runtimes_that_declare_it() {
assert!(!runtime_supports_ml_dsa_65(134, 2));
assert!(!runtime_supports_ml_dsa_65(135, 2));
assert!(!runtime_supports_ml_dsa_65(135, 3));
assert!(!runtime_supports_ml_dsa_65(136, 3));
assert!(runtime_supports_ml_dsa_65(142, 3));
assert!(runtime_supports_ml_dsa_65(143, 3));
assert!(!runtime_supports_ml_dsa_65(142, 2), "unknown tx version must not match");
}
#[test]
fn ensure_ml_dsa_65_supported_rejects_legacy_compatible_runtimes() {
let err = ensure_ml_dsa_65_supported(136, 3).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("ML-DSA-65") && msg.contains("ml-dsa-87") && msg.contains("136"),
"expected actionable ML-DSA-65 gate error, got: {msg}"
);
ensure_ml_dsa_65_supported(142, 3).expect("142 must allow ML-DSA-65");
ensure_ml_dsa_65_supported(143, 3).expect("143 must allow ML-DSA-65");
}
#[test]
fn every_compatible_runtime_has_an_explicit_ml_dsa_65_flag() {
assert!(
COMPATIBLE_RUNTIMES.iter().any(|r| r.supports_ml_dsa_65),
"at least one compatible runtime must support ML-DSA-65"
);
assert!(
COMPATIBLE_RUNTIMES.iter().any(|r| !r.supports_ml_dsa_65),
"legacy ML-DSA-87-only runtimes remain listed for 87 wallets"
);
}
}