1use alloc::format;
9use alloc::string::String;
10use alloc::vec::Vec;
11
12use blake2::Blake2bVar;
13use blake2::digest::{Update, VariableOutput};
14use kobe_primitives::DeriveError;
15
16pub const ACCOUNT_HASH_PREFIX: &str = "account-hash-";
18
19pub const ED25519_TAG: u8 = 0x01;
21
22pub const SECP256K1_TAG: u8 = 0x02;
24
25const ED25519_NAME: &[u8] = b"ed25519";
27
28const SECP256K1_NAME: &[u8] = b"secp256k1";
30
31#[inline]
33#[must_use]
34pub fn format_account_hash(digest: &[u8; 32]) -> String {
35 format!("{ACCOUNT_HASH_PREFIX}{}", hex::encode(digest))
36}
37
38#[inline]
43#[must_use]
44pub fn tagged_public_key_hex(tag: u8, raw_key: &[u8]) -> String {
45 let mut buf = Vec::with_capacity(1 + raw_key.len());
46 buf.push(tag);
47 buf.extend_from_slice(raw_key);
48 hex::encode(buf)
49}
50
51pub fn account_hash_ed25519(pubkey: &[u8; 32]) -> Result<[u8; 32], DeriveError> {
60 account_hash_from_parts(ED25519_NAME, pubkey)
61}
62
63pub fn account_hash_secp256k1(compressed_pubkey: &[u8; 33]) -> Result<[u8; 32], DeriveError> {
72 account_hash_from_parts(SECP256K1_NAME, compressed_pubkey)
73}
74
75fn account_hash_from_parts(algorithm_name: &[u8], raw_key: &[u8]) -> Result<[u8; 32], DeriveError> {
77 let mut preimage = Vec::with_capacity(algorithm_name.len() + 1 + raw_key.len());
78 preimage.extend_from_slice(algorithm_name);
79 preimage.push(0);
80 preimage.extend_from_slice(raw_key);
81 blake2b_256(&preimage)
82}
83
84fn blake2b_256(data: &[u8]) -> Result<[u8; 32], DeriveError> {
86 let mut hasher =
87 Blake2bVar::new(32).map_err(|e| DeriveError::Crypto(format!("blake2b init: {e}")))?;
88 hasher.update(data);
89 let mut out = [0u8; 32];
90 hasher
91 .finalize_variable(&mut out)
92 .map_err(|e| DeriveError::Crypto(format!("blake2b finalize: {e}")))?;
93 Ok(out)
94}
95
96#[cfg(test)]
97#[allow(
98 clippy::unwrap_used,
99 clippy::expect_used,
100 reason = "unit tests panic on assertion failure"
101)]
102mod tests {
103 use super::*;
104
105 #[test]
108 fn kat_account_hash_ed25519_fixed_key() {
109 let pk = hex::decode("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
110 .unwrap();
111 let pk: [u8; 32] = pk.try_into().unwrap();
112 let digest = account_hash_ed25519(&pk).unwrap();
113 assert_eq!(
114 hex::encode(digest),
115 "5b1c945c6e0923bf4f8da320444804791eb60d70983c7c5756d8ef236c1fdece"
116 );
117 assert_eq!(
118 format_account_hash(&digest),
119 "account-hash-5b1c945c6e0923bf4f8da320444804791eb60d70983c7c5756d8ef236c1fdece"
120 );
121 assert_eq!(
122 tagged_public_key_hex(ED25519_TAG, &pk),
123 "010123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
124 );
125 }
126
127 #[test]
129 fn kat_account_hash_secp256k1_generator() {
130 let pk = hex::decode("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798")
131 .unwrap();
132 let pk: [u8; 33] = pk.try_into().unwrap();
133 let digest = account_hash_secp256k1(&pk).unwrap();
134 assert_eq!(
135 hex::encode(digest),
136 "86937931937ee0281e50806b94f8d4993e8869b0689dfa0a21d2946ab677183c"
137 );
138 assert_eq!(
139 tagged_public_key_hex(SECP256K1_TAG, &pk),
140 "020279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
141 );
142 }
143
144 #[test]
146 fn preimage_includes_null_separator() {
147 let pk = [0xab_u8; 32];
148 let good = account_hash_ed25519(&pk).unwrap();
149 let mut wrong = Vec::with_capacity(33);
151 wrong.push(ED25519_TAG);
152 wrong.extend_from_slice(&pk);
153 let wrong_hash = blake2b_256(&wrong).unwrap();
154 assert_ne!(good, wrong_hash);
155 }
156}