host-encoding 0.3.8

Pure codec and hash functions for DOTNS and statement-store — no I/O, WASM-safe
Documentation
//! Pure encoding/decoding functions for DOTNS and sp-statement-store.
//!
//! No I/O. No threads. WASM-safe.

pub mod dotns;
pub mod extrinsic;
pub mod identity;
pub mod statement_store;

// ---------------------------------------------------------------------------
// Substrate MultiSignature SCALE encoding
// ---------------------------------------------------------------------------

/// Crypto scheme selector for MultiSignature SCALE encoding.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CryptoScheme {
    /// Ed25519 — SCALE variant byte 0x00
    Ed25519,
    /// Sr25519 — SCALE variant byte 0x01
    Sr25519,
    /// ECDSA (secp256k1) — SCALE variant byte 0x02
    Ecdsa,
}

/// Wrap a raw signature in a Substrate `MultiSignature` SCALE envelope.
///
/// Substrate MultiSignature enum order:
/// - Ed25519: variant 0x00, 64 bytes
/// - Sr25519: variant 0x01, 64 bytes
/// - Ecdsa:   variant 0x02, 65 bytes
///
/// Returns `Err` if `sig_bytes` has the wrong length for the chosen scheme.
pub fn encode_multi_signature(sig_bytes: &[u8], scheme: CryptoScheme) -> Result<Vec<u8>, String> {
    let (variant_byte, expected_len) = match scheme {
        CryptoScheme::Ed25519 => (0x00u8, 64usize),
        CryptoScheme::Sr25519 => (0x01u8, 64usize),
        CryptoScheme::Ecdsa => (0x02u8, 65usize),
    };

    if sig_bytes.len() != expected_len {
        return Err(format!(
            "expected {} bytes for {:?} signature, got {}",
            expected_len,
            scheme,
            sig_bytes.len(),
        ));
    }

    let mut out = Vec::with_capacity(1 + expected_len);
    out.push(variant_byte);
    out.extend_from_slice(sig_bytes);
    Ok(out)
}

/// Hex-encode bytes as a `0x`-prefixed lowercase string.
pub fn hex_encode(bytes: &[u8]) -> String {
    let mut s = String::with_capacity(2 + bytes.len() * 2);
    s.push_str("0x");
    for b in bytes {
        s.push_str(&format!("{b:02x}"));
    }
    s
}

/// Decode a `0x`-prefixed (or bare) hex string into bytes.
///
/// Returns `None` if the string contains invalid hex characters or has an odd length.
pub fn hex_decode(s: &str) -> Option<Vec<u8>> {
    let s = s.strip_prefix("0x").unwrap_or(s);
    if !s.len().is_multiple_of(2) {
        return None;
    }
    let mut out = Vec::with_capacity(s.len() / 2);
    for i in (0..s.len()).step_by(2) {
        out.push(u8::from_str_radix(&s[i..i + 2], 16).ok()?);
    }
    Some(out)
}

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

    #[test]
    fn test_sr25519_multi_signature_prefix_and_length() {
        let sig = [0xabu8; 64];
        let out = encode_multi_signature(&sig, CryptoScheme::Sr25519).unwrap();
        assert_eq!(out.len(), 65);
        assert_eq!(out[0], 0x01);
        assert_eq!(&out[1..], sig.as_ref());
    }

    #[test]
    fn test_ed25519_multi_signature_prefix_and_length() {
        let sig = [0xabu8; 64];
        let out = encode_multi_signature(&sig, CryptoScheme::Ed25519).unwrap();
        assert_eq!(out.len(), 65);
        assert_eq!(out[0], 0x00);
        assert_eq!(&out[1..], sig.as_ref());
    }

    #[test]
    fn test_ecdsa_multi_signature_prefix_and_length() {
        let sig = [0xabu8; 65];
        let out = encode_multi_signature(&sig, CryptoScheme::Ecdsa).unwrap();
        assert_eq!(out.len(), 66);
        assert_eq!(out[0], 0x02);
        assert_eq!(&out[1..], sig.as_ref());
    }

    #[test]
    fn test_multi_signature_rejects_wrong_sr25519_length() {
        let sig = [0x00u8; 63];
        assert!(encode_multi_signature(&sig, CryptoScheme::Sr25519).is_err());
    }

    #[test]
    fn test_multi_signature_rejects_wrong_ecdsa_length() {
        // ECDSA expects 65 bytes; 64 must be rejected.
        let sig = [0x00u8; 64];
        assert!(encode_multi_signature(&sig, CryptoScheme::Ecdsa).is_err());
    }

    #[test]
    fn test_multi_signature_pinned_zeros() {
        let sig = [0u8; 64];
        let out = encode_multi_signature(&sig, CryptoScheme::Sr25519).unwrap();
        // Expected: [0x01, 0x00 × 64] — 65 bytes total.
        assert_eq!(out.len(), 65);
        assert_eq!(out[0], 0x01);
        assert!(out[1..].iter().all(|&b| b == 0x00));
    }
}