code/primitives/hashed/src/safe.rs

safe.rs

107 lines · 4,007 bytes · d967d13588RawGitHub
//! Scheme-agnostic composite addresses with no raw Ed25519/Sr25519 signing key.
//!
//! These accounts have ordinary delegation policy. Their address does not prove
//! that the authorities use a particular signature scheme or immutable policy.

use codec::Encode;

pub const MAX_DERIVATION_ATTEMPTS: u8 = 64;
/// Subtensor's signer-count limit, shared with offline SDK address derivation.
pub const MAX_MULTISIG_SIGNATORIES: u32 = 100;
pub const MULTISIG_DOMAIN: &[u8] = b"subtensor/safe-multisig/v1";
pub const DERIVATIVE_DOMAIN: &[u8] = b"subtensor/safe-derivative/v1";
pub const PURE_PROXY_DOMAIN: &[u8] = b"subtensor/safe-pure/v1";

/// Return the first full-width hash of SCALE `(domain, manifest, attempt)`
/// rejected by both legacy raw-key decoders. Attempts are `0..64` in order.
///
/// Callers must bound and validate the manifest before invoking this helper.
/// Exhaustion is an error: never substitute the last candidate or a legacy
/// derivation. Runtime and SDK callers use the same canonical encoding.
pub fn safe_derived_account(domain: &[u8], manifest: &impl Encode) -> Option<[u8; 32]> {
    derive_with(domain, manifest, sp_core::hashing::blake2_256)
}

fn derive_with(
    domain: &[u8],
    manifest: &impl Encode,
    mut hash: impl FnMut(&[u8]) -> [u8; 32],
) -> Option<[u8; 32]> {
    let mut encoded = (domain, manifest).encode();
    encoded.push(0);
    for attempt in 0..MAX_DERIVATION_ATTEMPTS {
        *encoded.last_mut()? = attempt;
        let account = hash(&encoded);
        if crate::mldsa::rejects_legacy_public_keys(&account) {
            return Some(account);
        }
    }
    None
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn uses_first_full_hash_and_binds_every_manifest_field() {
        let manifest = (alloc::vec![[1u8; 32], [2u8; 32]], 2u16);
        let result = safe_derived_account(MULTISIG_DOMAIN, &manifest);
        assert_eq!(
            result,
            Some([
                42, 34, 149, 134, 241, 8, 13, 143, 224, 200, 176, 5, 78, 113, 69, 82, 238, 255, 93,
                28, 133, 45, 147, 28, 7, 7, 92, 60, 90, 205, 95, 109
            ])
        );
        let expected = (0..MAX_DERIVATION_ATTEMPTS)
            .map(|attempt| {
                sp_core::hashing::blake2_256(&(MULTISIG_DOMAIN, &manifest, attempt).encode())
            })
            .find(crate::mldsa::rejects_legacy_public_keys);
        assert!(result.is_some());
        assert_eq!(result, expected);
        assert_ne!(result, safe_derived_account(DERIVATIVE_DOMAIN, &manifest));
        assert_ne!(
            result,
            safe_derived_account(MULTISIG_DOMAIN, &(manifest.0.clone(), 1u16))
        );
        assert_ne!(
            result,
            safe_derived_account(MULTISIG_DOMAIN, &(alloc::vec![[1u8; 32], [3u8; 32]], 2u16))
        );
    }

    #[test]
    fn exhaustion_checks_exact_bound_and_never_returns_an_unsafe_candidate() {
        let mut attempts = alloc::vec![];
        let result = derive_with(DERIVATIVE_DOMAIN, &([1u8; 32], 7u16), |encoded| {
            attempts.push(encoded.last().copied());
            // The all-zero encoding decompresses as an Edwards point.
            [0u8; 32]
        });
        assert_eq!(result, None);
        assert_eq!(
            attempts,
            (0..MAX_DERIVATION_ATTEMPTS)
                .map(Some)
                .collect::<alloc::vec::Vec<_>>()
        );
    }

    #[test]
    fn tuple_encoding_is_unambiguous_and_deterministic() {
        assert_ne!(
            safe_derived_account(b"a", &b"bc"),
            safe_derived_account(b"ab", &b"c"),
        );
        for index in 0..128u16 {
            let manifest = ([42u8; 32], index);
            let result = safe_derived_account(DERIVATIVE_DOMAIN, &manifest);
            assert!(
                result.is_some_and(|account| crate::mldsa::rejects_legacy_public_keys(&account))
            );
            assert_eq!(result, safe_derived_account(DERIVATIVE_DOMAIN, &manifest));
        }
    }
}