Skip to main content

tronz_primitives/
message.rs

1//! TronWeb `signMessageV2`-compatible message hashing and verification.
2//!
3//! Digest = `keccak256("\x19TRON Signed Message:\n" || ascii(byte_len) || message)`
4//! (the TRON-prefixed format documented as TIP-191-compatible).
5
6use alloy_primitives::keccak256;
7
8use crate::{Address, B256, RecoverableSignature, error::SignatureError};
9
10/// The TRON personal-message prefix used by TronWeb `signMessageV2`.
11pub const TRON_MESSAGE_PREFIX: &[u8] = b"\x19TRON Signed Message:\n";
12
13/// Hash a message the way TronWeb `signMessageV2` does.
14///
15/// Strings are hashed as UTF-8 bytes: `"0x1234"` is 6 plaintext bytes, not hex.
16pub fn hash_message(message: impl AsRef<[u8]>) -> B256 {
17    let message = message.as_ref();
18    let len = message.len().to_string();
19
20    let mut prefixed = Vec::with_capacity(TRON_MESSAGE_PREFIX.len() + len.len() + message.len());
21    prefixed.extend_from_slice(TRON_MESSAGE_PREFIX);
22    prefixed.extend_from_slice(len.as_bytes());
23    prefixed.extend_from_slice(message);
24
25    keccak256(prefixed)
26}
27
28/// Recover the TRON address that signed `message` (TronWeb `verifyMessageV2`).
29///
30/// Accepts a `v` byte of `0`/`1` or the legacy `27`/`28`.
31pub fn recover_message_address(
32    message: impl AsRef<[u8]>,
33    signature: &RecoverableSignature,
34) -> Result<Address, SignatureError> {
35    signature.recover_address_from_prehash(hash_message(message))
36}
37
38/// Verify that `signature` over `message` was produced by `address`.
39///
40/// Returns `false` on any recovery error; use [`recover_message_address`] to
41/// inspect the error.
42pub fn verify_message(
43    message: impl AsRef<[u8]>,
44    signature: &RecoverableSignature,
45    address: Address,
46) -> bool {
47    recover_message_address(message, signature)
48        .map(|recovered| recovered == address)
49        .unwrap_or(false)
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    // Golden vectors from TronWeb `Trx.signMessageV2`, private key 0x00..01.
57    const TRONWEB_ADDR: &str = "TMVQGm1qAQYVdetCeGRRkTWYYrLXuHK2HC";
58    const VECTORS: &[(&str, &str)] = &[
59        (
60            "hello world",
61            "0x0dc0b53d525e0103a6013061cf18e60cf158809149f2b8994a545af65a7004cb1eeaff560e801ab51b28df5d42549aa024c2aa7e9d34de1e01294b9afb5e6c7e1c",
62        ),
63        (
64            "",
65            "0x5fc8883facaeb9dbebe71ec179c2744f83dc0e4868c6c9a6d63d47b949f33ae4317dc553f28a9c64b06838dc1adf7f24da330394f05e4ea6318d18608293ffe91b",
66        ),
67        (
68            "你好",
69            "0xa1a3ca757f1a6e52018bff5de55903be6073072f3e1fc82bea75f9f9e491bab4516d9a02f448f13127175fd2e91df6a9ea7da6422fc3c8f89353b64beb24c6321b",
70        ),
71        (
72            "0x1234",
73            "0x76f9b3883c5fc5415a5f132fe6916d089441c27c2438bee9c8461409a2002c950d1882ed3b3468cfe8d77b303241446179e60971287fdd882ea566ef7e517a101c",
74        ),
75    ];
76
77    fn sig_from_hex(s: &str) -> RecoverableSignature {
78        let bytes = hex::decode(s.trim_start_matches("0x")).unwrap();
79        RecoverableSignature::from_bytes(&bytes).unwrap()
80    }
81
82    #[test]
83    fn hash_message_matches_manual_assembly() {
84        for (msg, _) in VECTORS {
85            let mut expected = Vec::new();
86            expected.extend_from_slice(b"\x19TRON Signed Message:\n");
87            expected.extend_from_slice(msg.len().to_string().as_bytes());
88            expected.extend_from_slice(msg.as_bytes());
89            assert_eq!(hash_message(msg), keccak256(&expected), "msg={msg:?}");
90        }
91    }
92
93    #[test]
94    fn byte_length_prefix_is_utf8_not_char_count() {
95        assert_eq!("你好".len(), 6);
96        assert_eq!("0x1234".len(), 6);
97    }
98
99    #[test]
100    fn recovers_tronweb_signatures() {
101        let expected = Address::from_base58(TRONWEB_ADDR).unwrap();
102        for (msg, sig_hex) in VECTORS {
103            let sig = sig_from_hex(sig_hex);
104            assert_eq!(recover_message_address(msg, &sig).unwrap(), expected, "msg={msg:?}");
105            assert!(verify_message(msg, &sig, expected), "msg={msg:?}");
106        }
107    }
108
109    #[test]
110    fn verify_rejects_tampered_message_and_wrong_address() {
111        let expected = Address::from_base58(TRONWEB_ADDR).unwrap();
112        let sig = sig_from_hex(VECTORS[0].1); // "hello world"
113        assert!(!verify_message("hello worlx", &sig, expected));
114        assert!(!verify_message("hello world", &sig, Address::ZERO));
115    }
116}