code/runtime/src/hashed_envelope_tests.rs
//! Standard-v4 client compatibility and transaction-boundary authorization.
use super::*;
use frame_support::assert_ok;
use sp_runtime::traits::ExtrinsicCall;
#[test]
fn direct_call_authenticates_rotates_and_rejects_replay() {
ext().execute_with(|| {
setup();
let tx = signed(call(), 0, &pair(10));
assert_eq!(ExtrinsicCall::call(&tx), &call());
let bytes = tx.encode();
let decoded = UncheckedExtrinsic::decode(&mut &bytes[..]).unwrap();
let legacy = LegacyUnchecked::decode(&mut &bytes[..]).unwrap();
assert!(matches!(
legacy.0.preamble,
sp_runtime::generic::Preamble::Signed(Address::Raw(_), Signature::Sr25519(_), _)
));
assert_eq!(legacy.encode(), bytes);
let opaque: sp_runtime::OpaqueExtrinsic = decoded.clone().into();
assert_eq!(opaque.encode(), bytes);
assert_ok!(Executive::apply_extrinsic(decoded).unwrap());
assert_eq!(HashedAccounts::accounts(account()).unwrap().generation, 1);
assert!(
Executive::apply_extrinsic(UncheckedExtrinsic::decode(&mut &bytes[..]).unwrap())
.is_err()
);
});
}
#[test]
fn bare_general_and_ordinary_signature_cannot_impersonate_hashed_account() {
ext().execute_with(|| {
setup();
let before = signing_state();
assert!(Executive::apply_extrinsic(UncheckedExtrinsic::new_bare(call())).is_err());
let general = UncheckedExtrinsic::Legacy(
sp_runtime::generic::UncheckedExtrinsic {
preamble: sp_runtime::generic::Preamble::General(0, extra(0).into()),
function: call(),
}
.into(),
);
assert!(Executive::apply_extrinsic(general).is_err());
let ordinary = SignedPayload::new(call(), extra(0))
.unwrap()
.using_encoded(|p| pair(10).sign(p));
let mut tx = signed(call(), 0, &pair(10));
let UncheckedExtrinsic::Legacy(xt) = &mut tx;
let sp_runtime::generic::Preamble::Signed(_, sig, _) = &mut xt.0.preamble else {
panic!("signed")
};
*sig = Signature::Sr25519(ordinary);
assert!(Executive::apply_extrinsic(tx).is_err());
assert_eq!(signing_state(), before);
});
}
#[test]
fn raw_signer_rejects_unknown_versions_trailing_bytes_and_oversized_lengths() {
ext().execute_with(|| {
setup();
let tx = signed(call(), 0, &pair(10));
for bad in [
alloc::vec![],
alloc::vec![0;105],
alloc::vec![0;107],
alloc::vec![0;106],
] {
let mut changed = tx.clone();
let UncheckedExtrinsic::Legacy(xt) = &mut changed;
let sp_runtime::generic::Preamble::Signed(address, _, _) = &mut xt.0.preamble else {
panic!("signed")
};
*address = Address::Raw(bad);
assert!(UncheckedExtrinsic::decode(&mut &changed.encode()[..]).is_err());
assert!(Executive::apply_extrinsic(changed).is_err());
}
let mut body = alloc::vec![0x84, 2];
codec::Compact(u32::MAX).encode_to(&mut body);
let mut encoded = codec::Compact(body.len() as u32).encode();
encoded.extend_from_slice(&body);
assert!(UncheckedExtrinsic::decode(&mut &encoded[..]).is_err());
});
}
#[test]
fn export_client_envelope_fixtures() {
ext().execute_with(|| {
setup();
let ed = sp_core::ed25519::Pair::from_seed(&[23; 32]);
let ed_extra = extra(0);
let signature = SignedPayload::new(call(), ed_extra.clone()).unwrap().using_encoded(|p| ed.sign(p));
let address = subtensor_hashed::SignerAddress {
version: 1, account: [45;32], scheme: Scheme::Ed25519, generation: 7,
next_commitment: [46;32], public_key: ed.public().0,
};
let hashed_signature = ed.sign(&hashed_extrinsic::signing_hash(&address, &call(), &ed_extra).unwrap());
let ecdsa = sp_core::ecdsa::Pair::from_seed(&[24; 32]);
let ecdsa_signature = SignedPayload::new(call(), ed_extra.clone()).unwrap().using_encoded(|p| ecdsa.sign(p));
let ecdsa_account = sp_runtime::traits::IdentifyAccount::into_account(sp_runtime::MultiSigner::from(ecdsa.public()));
let transactions = [
legacy(&pair(1), call()),
{ mldsa::register(42); mldsa::sign(42, call()) },
UncheckedExtrinsic::new_signed(call(), AccountId::from(ed.public()).into(), Signature::Ed25519(signature), ed_extra.clone()),
signed(call(), 0, &pair(10)),
UncheckedExtrinsic::new_signed(call(), Address::Raw(address.encode()), Signature::Ed25519(hashed_signature), ed_extra),
UncheckedExtrinsic::new_signed(call(), ecdsa_account.into(), Signature::Ecdsa(ecdsa_signature), extra(0)),
UncheckedExtrinsic::new_bare(call()),
];
// Export a proof checked by the runtime, not just synthetic opaque bytes.
assert!(transactions[1].clone().check(&ChainContext::default()).is_ok());
let bytes = transactions.iter().map(|tx| {
let bytes = tx.encode();
assert_eq!(UncheckedExtrinsic::decode(&mut &bytes[..]).unwrap().encode(), bytes);
format!("0x{}", hex::encode(bytes))
}).collect::<Vec<_>>();
if let Some(path) = std::env::var_os("ACCOUNT_ENVELOPE_FIXTURES") {
let metadata = [14,15,16].map(|version| (version.to_string(), format!("0x{}", hex::encode(&*hashed_extrinsic::metadata_at_version(version).unwrap()))));
let value = serde_json::json!({"metadata": metadata.into_iter().collect::<std::collections::BTreeMap<_,_>>(), "extrinsics": bytes,
"specVersion": VERSION.spec_version, "transactionVersion": VERSION.transaction_version});
std::fs::write(path, serde_json::to_vec(&value).unwrap()).unwrap();
}
});
}
/// Legacy Polkadot.js rejects fixed arrays above 2048 while constructing a
/// call decoder, even if that transaction uses another enum variant. Large
/// proofs must use bounded vectors and validate their exact length separately.
#[test]
fn all_metadata_versions_respect_legacy_client_fixed_array_limit() {
for version in [14, 15, 16] {
let encoded = hashed_extrinsic::metadata_at_version(version).unwrap();
let metadata = frame_metadata::RuntimeMetadataPrefixed::decode(&mut &encoded[..]).unwrap();
macro_rules! profiles {
($metadata:expr, $name:literal) => {
&$metadata
.pallets
.iter()
.find(|p| p.name == "HashedAccounts")
.unwrap()
.constants
.iter()
.find(|c| c.name == $name)
.unwrap()
.value
};
}
let advertised = match &metadata.1 {
frame_metadata::RuntimeMetadata::V14(data) => profiles!(data, "MldsaAccountProfiles"),
frame_metadata::RuntimeMetadata::V15(data) => profiles!(data, "MldsaAccountProfiles"),
frame_metadata::RuntimeMetadata::V16(data) => profiles!(data, "MldsaAccountProfiles"),
_ => panic!("unexpected metadata version"),
};
assert_eq!(advertised.as_slice(), &[4, 5]);
let falcon_profiles = match &metadata.1 {
frame_metadata::RuntimeMetadata::V14(data) => profiles!(data, "FalconAccountProfiles"),
frame_metadata::RuntimeMetadata::V15(data) => profiles!(data, "FalconAccountProfiles"),
frame_metadata::RuntimeMetadata::V16(data) => profiles!(data, "FalconAccountProfiles"),
_ => panic!("unexpected metadata version"),
};
assert_eq!(falcon_profiles.as_slice(), &[6, 7, 8, 9]);
let registry = match metadata.1 {
frame_metadata::RuntimeMetadata::V14(metadata) => metadata.types,
frame_metadata::RuntimeMetadata::V15(metadata) => metadata.types,
frame_metadata::RuntimeMetadata::V16(metadata) => metadata.types,
_ => panic!("unexpected metadata version"),
};
for portable in registry.types {
if let scale_info::TypeDef::Array(array) = &portable.ty.type_def {
assert!(
array.len <= 2048,
"metadata v{version} type {} exposes an unsupported fixed array: {:?}",
portable.id,
portable.ty
);
}
}
}
}
#[test]
fn account_metadata_uses_scheme_names_and_contiguous_call_indices() {
for version in [14, 15, 16] {
let encoded = hashed_extrinsic::metadata_at_version(version).unwrap();
let metadata = frame_metadata::RuntimeMetadataPrefixed::decode(&mut &encoded[..]).unwrap();
macro_rules! check {
($data:expr) => {{
let data = $data;
let pallet = data
.pallets
.iter()
.find(|p| p.name == "HashedAccounts")
.unwrap();
let storage = &pallet.storage.as_ref().unwrap().entries;
assert!(storage.iter().any(|entry| entry.name == "MlDsaAccounts"));
assert!(
storage
.iter()
.all(|entry| !entry.name.contains("Capabilit"))
);
assert!(
pallet
.constants
.iter()
.all(|item| !item.name.contains("Capabilit"))
);
assert!(storage.iter().all(|entry| !matches!(
entry.name.as_str(),
"MlDsaRegistrationEnabled" | "MlDsaAuthorizationEnabled"
)));
let admin = data
.pallets
.iter()
.find(|p| p.name == "AdminUtils")
.unwrap();
assert!(
admin
.storage
.as_ref()
.unwrap()
.entries
.iter()
.any(|entry| entry.name == "HashedAccountsEnabled")
);
assert!(storage.iter().all(|entry| !entry.name.contains("V2")));
assert!(
pallet
.constants
.iter()
.any(|c| c.name == "MlDsaRegistrationDeposit")
);
assert!(pallet.constants.iter().all(|c| !c.name.contains("V2")));
let scale_info::TypeDef::Variant(events) = &data
.types
.resolve(pallet.event.as_ref().unwrap().ty.id)
.unwrap()
.type_def
else {
panic!("event enum")
};
assert_eq!(
events
.variants
.iter()
.map(|v| (v.index, v.name.as_str()))
.collect::<Vec<_>>(),
vec![
(0, "Registered"),
(2, "RegisteredMlDsa"),
(4, "DerivedProtected"),
]
);
let scale_info::TypeDef::Variant(calls) = &data
.types
.resolve(pallet.calls.as_ref().unwrap().ty.id)
.unwrap()
.type_def
else {
panic!("call enum")
};
assert!(calls.variants.iter().all(|v| !v.name.contains("v2")));
let mut call_indices = calls
.variants
.iter()
.map(|v| (v.index, v.name.as_str()))
.collect::<Vec<_>>();
call_indices.sort_unstable();
assert_eq!(
call_indices,
vec![
(0, "register"),
(1, "check_registered"),
(2, "register_derived"),
(3, "register_derived_sponsored"),
(4, "check_derived"),
(5, "check_registration_deposit"),
]
);
let orders = data
.pallets
.iter()
.find(|p| p.name == "LimitOrders")
.unwrap();
let scale_info::TypeDef::Variant(order_calls) = &data
.types
.resolve(orders.calls.as_ref().unwrap().ty.id)
.unwrap()
.type_def
else {
panic!("order call enum")
};
let mut indices = order_calls
.variants
.iter()
.map(|v| v.index)
.collect::<Vec<_>>();
indices.sort_unstable();
assert_eq!(
indices,
(0..order_calls.variants.len() as u8).collect::<Vec<_>>()
);
let storage = &orders.storage.as_ref().unwrap().entries;
assert!(storage.iter().any(|entry| entry.name == "ApprovedOrders"));
assert!(!storage.iter().any(|entry| entry.name == "OrderNonces"));
}};
}
match metadata.1 {
frame_metadata::RuntimeMetadata::V14(data) => check!(data),
frame_metadata::RuntimeMetadata::V15(data) => check!(data),
frame_metadata::RuntimeMetadata::V16(data) => check!(data),
_ => panic!("unexpected metadata version"),
}
}
}
#[test]
fn detached_order_proof_variants_are_rejected() {
use pallet_limit_orders::{OrderAuthorization, OrderSignature};
// Include enough trailing bytes for the former proofs, so these fail on
// their removed variant rather than merely on a truncated payload.
for index in [4, 5] {
let mut encoded = vec![0; 6000];
encoded[0] = index;
assert!(OrderSignature::decode(&mut encoded.as_slice()).is_err());
}
for index in [1, 2] {
let mut encoded = vec![0; 6000];
encoded[0] = index;
assert!(OrderAuthorization::decode(&mut encoded.as_slice()).is_err());
}
assert_eq!(OrderSignature::AccountApproved.encode(), vec![3]);
assert_eq!(OrderAuthorization::AccountApproved.encode(), vec![0]);
}
#[test]
fn python_sdk_v4_vectors_execute_rotate_and_preserve_account_identity() {
let fixture: serde_json::Value =
serde_json::from_str(include_str!("../tests/fixtures/hashed-python-v4.json")).unwrap();
assert_eq!(fixture["spec_version"], VERSION.spec_version);
assert_eq!(fixture["transaction_version"], VERSION.transaction_version);
for wallet in fixture["wallets"].as_array().unwrap() {
ext().execute_with(|| {
let descriptor = Descriptor::decode(
&mut &hex::decode(wallet["descriptor"].as_str().unwrap()).unwrap()[..],
)
.unwrap();
let account = super::composable_accounts::register(descriptor);
assert_eq!(
hex::encode(AsRef::<[u8]>::as_ref(&account)),
wallet["account"].as_str().unwrap()
);
let recipient = AccountId::new([99; 32]);
for (index, wire) in wallet["extrinsics"].as_array().unwrap().iter().enumerate() {
let bytes = hex::decode(wire.as_str().unwrap()).unwrap();
let xt = UncheckedExtrinsic::decode(&mut &bytes[..]).unwrap();
assert_eq!(xt.encode(), bytes);
let replay = xt.clone();
let result = Executive::apply_extrinsic(xt).unwrap();
if index < 2 {
assert_ok!(result);
} else {
assert!(result.is_err());
}
assert_eq!(
HashedAccounts::accounts(&account).unwrap().generation,
(index + 1) as u64
);
assert_eq!(System::account_nonce(&account), (index + 2) as u32);
assert!(Executive::apply_extrinsic(replay).is_err());
}
assert_eq!(
Balances::free_balance(&recipient),
TaoBalance::new(1_000_000_000)
);
assert!(Balances::free_balance(&account) < TaoBalance::new(9_000_000_000));
});
}
}
#[test]
fn ordinary_v4_fee_weight_accounts_for_all_protected_namespaces() {
ext().execute_with(|| {
let call = super::call();
let tx = legacy(&pair(1), call.clone());
let expected = frame_support::dispatch::DispatchInfo {
extension_weight: <Runtime as frame_system::Config>::DbWeight::get().reads(3),
..call.get_dispatch_info()
};
assert_eq!(tx.get_dispatch_info(), expected);
assert_eq!(
tx.check(&ChainContext::default())
.unwrap()
.get_dispatch_info(),
expected
);
});
}