posemesh_node_registration/
crypto.rs1use chrono::{DateTime, SecondsFormat, Utc};
2use secp256k1::{ecdsa::Signature, Message, PublicKey, Secp256k1, SecretKey};
3use sha2::{Digest as Sha2Digest, Sha256};
4use sha3::Keccak256;
5
6pub 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
18pub 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(); hex::encode(uncompressed)
24}
25
26pub 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(); 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
38pub 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
49pub fn sign_recoverable_keccak_hex(sk: &SecretKey, msg: &[u8]) -> String {
51 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; hex::encode(out)
63}
64
65pub 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; format!("0x{}", hex::encode(out))
80}
81
82pub 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 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 assert_eq!(sig.len(), 130);
114 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}