Skip to main content

kobe_casper/
address.rs

1//! Casper `PublicKey` tagging and `AccountHash` encoding.
2//!
3//! Encoding matches `casper-types` (`casper-node` `types` crate):
4//!
5//! - **Tagged public key** (serialization / display hex): tag byte + raw key.
6//! - **`AccountHash`** preimage: `algorithm_name || 0x00 || raw_key` (no tag).
7
8use alloc::format;
9use alloc::string::String;
10use alloc::vec::Vec;
11
12use blake2::Blake2bVar;
13use blake2::digest::{Update, VariableOutput};
14use kobe_primitives::DeriveError;
15
16/// Prefix applied to the hex-encoded `AccountHash` for display.
17pub const ACCOUNT_HASH_PREFIX: &str = "account-hash-";
18
19/// Casper serialization tag for Ed25519 public keys (`PublicKey::Ed25519`).
20pub const ED25519_TAG: u8 = 0x01;
21
22/// Casper serialization tag for secp256k1 public keys (`PublicKey::Secp256k1`).
23pub const SECP256K1_TAG: u8 = 0x02;
24
25/// Lowercase algorithm name used in the `AccountHash` preimage (Ed25519).
26const ED25519_NAME: &[u8] = b"ed25519";
27
28/// Lowercase algorithm name used in the `AccountHash` preimage (secp256k1).
29const SECP256K1_NAME: &[u8] = b"secp256k1";
30
31/// Format a 32-byte `AccountHash` digest as `account-hash-` + lowercase hex.
32#[inline]
33#[must_use]
34pub fn format_account_hash(digest: &[u8; 32]) -> String {
35    format!("{ACCOUNT_HASH_PREFIX}{}", hex::encode(digest))
36}
37
38/// Tagged public-key hex (no `0x` prefix): `01 ‖ ed25519` or `02 ‖ secp`.
39///
40/// `raw_key` must be the 32-byte Ed25519 key or 33-byte compressed secp256k1
41/// key (without the Casper algorithm tag).
42#[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
51/// Compute the Casper `AccountHash` for an Ed25519 public key.
52///
53/// Preimage: `b"ed25519" || 0x00 || pubkey` (32-byte raw key).
54///
55/// # Errors
56///
57/// Returns [`DeriveError::Crypto`] if `BLAKE2b` initialization or finalization
58/// fails (should not occur for a fixed 32-byte output size).
59pub fn account_hash_ed25519(pubkey: &[u8; 32]) -> Result<[u8; 32], DeriveError> {
60    account_hash_from_parts(ED25519_NAME, pubkey)
61}
62
63/// Compute the Casper `AccountHash` for a compressed secp256k1 public key.
64///
65/// Preimage: `b"secp256k1" || 0x00 || compressed_pubkey` (33-byte `SEC1`).
66///
67/// # Errors
68///
69/// Returns [`DeriveError::Crypto`] if `BLAKE2b` initialization or finalization
70/// fails.
71pub fn account_hash_secp256k1(compressed_pubkey: &[u8; 33]) -> Result<[u8; 32], DeriveError> {
72    account_hash_from_parts(SECP256K1_NAME, compressed_pubkey)
73}
74
75/// Shared `AccountHash` construction: `name || 0x00 || raw_key` → `BLAKE2b`-256.
76fn 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
84/// `BLAKE2b`-256 (empty key), matching Casper's `crypto::blake2b`.
85fn 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    /// Independent stdlib-equivalent vectors: preimage is `name||0x00||key`,
106    /// digest is `BLAKE2b`-256. Cross-checked with Python `hashlib.blake2b`.
107    #[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    /// secp256k1 generator point (compressed) — independent `BLAKE2b` KAT.
128    #[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    /// Preimage must include the null separator; wrong layout must not match.
145    #[test]
146    fn preimage_includes_null_separator() {
147        let pk = [0xab_u8; 32];
148        let good = account_hash_ed25519(&pk).unwrap();
149        // Tag-only layout (incorrect for AccountHash) must differ.
150        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}