use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use blake2::Blake2bVar;
use blake2::digest::{Update, VariableOutput};
use kobe_primitives::DeriveError;
pub const ACCOUNT_HASH_PREFIX: &str = "account-hash-";
pub const ED25519_TAG: u8 = 0x01;
pub const SECP256K1_TAG: u8 = 0x02;
const ED25519_NAME: &[u8] = b"ed25519";
const SECP256K1_NAME: &[u8] = b"secp256k1";
#[inline]
#[must_use]
pub fn format_account_hash(digest: &[u8; 32]) -> String {
format!("{ACCOUNT_HASH_PREFIX}{}", hex::encode(digest))
}
#[inline]
#[must_use]
pub fn tagged_public_key_hex(tag: u8, raw_key: &[u8]) -> String {
let mut buf = Vec::with_capacity(1 + raw_key.len());
buf.push(tag);
buf.extend_from_slice(raw_key);
hex::encode(buf)
}
pub fn account_hash_ed25519(pubkey: &[u8; 32]) -> Result<[u8; 32], DeriveError> {
account_hash_from_parts(ED25519_NAME, pubkey)
}
pub fn account_hash_secp256k1(compressed_pubkey: &[u8; 33]) -> Result<[u8; 32], DeriveError> {
account_hash_from_parts(SECP256K1_NAME, compressed_pubkey)
}
fn account_hash_from_parts(algorithm_name: &[u8], raw_key: &[u8]) -> Result<[u8; 32], DeriveError> {
let mut preimage = Vec::with_capacity(algorithm_name.len() + 1 + raw_key.len());
preimage.extend_from_slice(algorithm_name);
preimage.push(0);
preimage.extend_from_slice(raw_key);
blake2b_256(&preimage)
}
fn blake2b_256(data: &[u8]) -> Result<[u8; 32], DeriveError> {
let mut hasher =
Blake2bVar::new(32).map_err(|e| DeriveError::Crypto(format!("blake2b init: {e}")))?;
hasher.update(data);
let mut out = [0u8; 32];
hasher
.finalize_variable(&mut out)
.map_err(|e| DeriveError::Crypto(format!("blake2b finalize: {e}")))?;
Ok(out)
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "unit tests panic on assertion failure"
)]
mod tests {
use super::*;
#[test]
fn kat_account_hash_ed25519_fixed_key() {
let pk = hex::decode("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
.unwrap();
let pk: [u8; 32] = pk.try_into().unwrap();
let digest = account_hash_ed25519(&pk).unwrap();
assert_eq!(
hex::encode(digest),
"5b1c945c6e0923bf4f8da320444804791eb60d70983c7c5756d8ef236c1fdece"
);
assert_eq!(
format_account_hash(&digest),
"account-hash-5b1c945c6e0923bf4f8da320444804791eb60d70983c7c5756d8ef236c1fdece"
);
assert_eq!(
tagged_public_key_hex(ED25519_TAG, &pk),
"010123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
);
}
#[test]
fn kat_account_hash_secp256k1_generator() {
let pk = hex::decode("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")
.unwrap();
let pk: [u8; 33] = pk.try_into().unwrap();
let digest = account_hash_secp256k1(&pk).unwrap();
assert_eq!(
hex::encode(digest),
"86937931937ee0281e50806b94f8d4993e8869b0689dfa0a21d2946ab677183c"
);
assert_eq!(
tagged_public_key_hex(SECP256K1_TAG, &pk),
"020279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
);
}
#[test]
fn preimage_includes_null_separator() {
let pk = [0xab_u8; 32];
let good = account_hash_ed25519(&pk).unwrap();
let mut wrong = Vec::with_capacity(33);
wrong.push(ED25519_TAG);
wrong.extend_from_slice(&pk);
let wrong_hash = blake2b_256(&wrong).unwrap();
assert_ne!(good, wrong_hash);
}
}