Skip to main content

posemesh_node_registration/
crypto.rs

1use chrono::{DateTime, SecondsFormat, Utc};
2use secp256k1::{ecdsa::Signature, Message, PublicKey, Secp256k1, SecretKey};
3use sha2::{Digest as Sha2Digest, Sha256};
4use sha3::Keccak256;
5
6/// Load a secp256k1 private key from lowercase hex (optionally 0x-prefixed).
7pub fn load_secp256k1_privhex(hex_str: &str) -> anyhow::Result<SecretKey> {
8    let s = hex_str.trim();
9    let s = s.strip_prefix("0x").unwrap_or(s);
10    let bytes = hex::decode(s)?;
11    if bytes.len() != 32 {
12        anyhow::bail!("invalid secp256k1 secret length: {}", bytes.len());
13    }
14    let sk = SecretKey::from_slice(&bytes)?;
15    Ok(sk)
16}
17
18/// Derive the uncompressed public key (0x04 || X || Y) as lowercase hex.
19pub fn secp256k1_pubkey_uncompressed_hex(sk: &SecretKey) -> String {
20    let secp = Secp256k1::new();
21    let pk = PublicKey::from_secret_key(&secp, sk);
22    let uncompressed = pk.serialize_uncompressed(); // 65 bytes, leading 0x04
23    hex::encode(uncompressed)
24}
25
26/// Derive the Ethereum address (0x-prefixed, lowercase) from a secp256k1 secret key.
27pub fn derive_eth_address(sk: &SecretKey) -> String {
28    let secp = Secp256k1::new();
29    let pk = PublicKey::from_secret_key(&secp, sk);
30    let uncompressed = pk.serialize_uncompressed(); // 65 bytes, leading 0x04
31    let mut hasher = Keccak256::new();
32    hasher.update(&uncompressed[1..]);
33    let hash = hasher.finalize();
34    let address = &hash[12..];
35    format!("0x{}", hex::encode(address))
36}
37
38/// Sign arbitrary message bytes using RFC6979 deterministic ECDSA over SHA-256.
39/// Returns the compact 64-byte signature as lowercase hex (r||s).
40pub fn sign_compact_hex(sk: &SecretKey, msg: &[u8]) -> String {
41    let digest = Sha256::digest(msg);
42    let message = Message::from_digest_slice(&digest).expect("sha256 is 32 bytes");
43    let secp = Secp256k1::new();
44    let sig: Signature = secp.sign_ecdsa(&message, sk);
45    let compact = sig.serialize_compact();
46    hex::encode(compact)
47}
48
49/// Sign using Ethereum-style Keccak-256 digest and return 65-byte (r||s||v) hex.
50pub fn sign_recoverable_keccak_hex(sk: &SecretKey, msg: &[u8]) -> String {
51    // Keccak-256 of the raw message bytes (no prefixing)
52    let mut hasher = Keccak256::new();
53    hasher.update(msg);
54    let hash = hasher.finalize();
55    let message = Message::from_digest_slice(&hash).expect("keccak256 is 32 bytes");
56    let secp = Secp256k1::new();
57    let rsig = secp.sign_ecdsa_recoverable(&message, sk);
58    let (rid, sig_bytes) = rsig.serialize_compact();
59    let mut out = [0u8; 65];
60    out[..64].copy_from_slice(&sig_bytes);
61    out[64] = rid.to_i32() as u8; // 0 or 1
62    hex::encode(out)
63}
64
65/// Sign using Ethereum EIP-191 message prefix and return 65-byte (r||s||v) hex with 0x prefix.
66pub fn sign_eip191_recoverable_hex(sk: &SecretKey, message: &str) -> String {
67    let prefix = format!("\u{19}Ethereum Signed Message:\n{}", message.len());
68    let mut hasher = Keccak256::new();
69    hasher.update(prefix.as_bytes());
70    hasher.update(message.as_bytes());
71    let hash = hasher.finalize();
72    let message = Message::from_digest_slice(&hash).expect("keccak256 is 32 bytes");
73    let secp = Secp256k1::new();
74    let rsig = secp.sign_ecdsa_recoverable(&message, sk);
75    let (rid, sig_bytes) = rsig.serialize_compact();
76    let mut out = [0u8; 65];
77    out[..64].copy_from_slice(&sig_bytes);
78    out[64] = rid.to_i32() as u8 + 27; // Ethereum v in {27, 28}
79    format!("0x{}", hex::encode(out))
80}
81
82/// RFC3339 with nanoseconds and Z suffix.
83pub fn format_timestamp_nanos(ts: DateTime<Utc>) -> String {
84    ts.to_rfc3339_opts(SecondsFormat::Nanos, true)
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use chrono::{NaiveDate, NaiveTime};
91
92    #[test]
93    fn timestamp_nanos_format() {
94        let date = NaiveDate::from_ymd_opt(2024, 1, 2).unwrap();
95        let time = NaiveTime::from_hms_nano_opt(3, 4, 5, 6_007_008).unwrap();
96        let dt = DateTime::<Utc>::from_naive_utc_and_offset(date.and_time(time), Utc);
97        let s = format_timestamp_nanos(dt);
98        assert_eq!(s, "2024-01-02T03:04:05.006007008Z");
99    }
100
101    #[test]
102    fn sign_fixed_keccak_recoverable_hex_has_expected_shape() {
103        // Fixed key and message; ensure output shape and stability invariants.
104        let sk = load_secp256k1_privhex(
105            "e331b6d69882b4ed5bb7f55b585d7d0f7dc3aeca4a3deee8d16bde3eca51aace",
106        )
107        .expect("key");
108        let url = "https://node.example.com";
109        let ts = "2024-01-02T03:04:05.000000000Z";
110        let msg = format!("{}{}", url, ts);
111        let sig = sign_recoverable_keccak_hex(&sk, msg.as_bytes());
112        // Expect 65-byte signature hex => 130 hex chars
113        assert_eq!(sig.len(), 130);
114        // All lowercase hex
115        assert!(sig
116            .chars()
117            .all(|c| c.is_ascii_hexdigit() && c.is_ascii_lowercase() || c.is_ascii_digit()));
118    }
119
120    #[test]
121    fn derive_eth_address_matches_expected_value() {
122        let sk = load_secp256k1_privhex(
123            "4c0883a69102937d6231471b5dbb6204fe5129617082798ce3f4fdf2548b6f90",
124        )
125        .expect("key");
126        let addr = derive_eth_address(&sk);
127        assert_eq!(addr, "0xfdbb6caf01414300c16ea14859fec7736d95355f");
128    }
129
130    #[test]
131    fn sign_eip191_recoverable_has_expected_shape() {
132        let sk = load_secp256k1_privhex(
133            "4c0883a69102937d6231471b5dbb6204fe5129617082798ce3f4fdf2548b6f90",
134        )
135        .expect("key");
136        let sig = sign_eip191_recoverable_hex(&sk, "hello");
137        assert!(sig.starts_with("0x"));
138        assert_eq!(sig.len(), 132);
139    }
140}