Skip to main content

molpha_verifier/
message.rs

1//! EVM-compatible `DataUpdate` message hash.
2
3use solana_keccak_hasher::hashv;
4
5use crate::payload::DataUpdate;
6
7/// `bytes32(keccak256("MOLPHA_MESSAGE_V1"))` — EVM `Validator._constructMessage` prefix.
8///
9/// Value: `keccak256(bytes("MOLPHA_MESSAGE_V1"))`, verified by the unit test below.
10pub const MESSAGE_PREFIX: [u8; 32] = [
11    0xa7, 0x55, 0x23, 0xa2, 0xab, 0x7b, 0x71, 0x8d, 0x9c, 0xff, 0xd2, 0xfa, 0x97, 0xed, 0x06, 0x9f,
12    0xc1, 0x21, 0x84, 0xea, 0xbe, 0xe7, 0xd5, 0x07, 0x85, 0x4d, 0x09, 0x22, 0xf7, 0x0e, 0x7f, 0xe7,
13];
14
15/// Compute the EVM-compatible `DataUpdate` message hash.
16///
17/// Matches `Validator._constructMessage` in the EVM reference implementation:
18/// ```text
19/// keccak256(abi.encodePacked(
20///     MESSAGE_PREFIX, feedId, registryVersion, signaturesRequired,
21///     signersBitmap, value, canonicalTimestamp
22/// ))
23/// ```
24///
25/// `signatures_required` is passed explicitly (not read from `payload`) because callers may
26/// verify against a value distinct from `payload.signatures_required` (e.g. `job.signatures_required`).
27pub fn compute_message_hash(payload: &DataUpdate, signatures_required: u8) -> [u8; 32] {
28    let registry_version_bytes = payload.registry_version.to_be_bytes();
29    let signatures_required_bytes = u32::from(signatures_required).to_be_bytes();
30    let canonical_timestamp_bytes = (payload.canonical_timestamp as u64).to_be_bytes();
31
32    hashv(&[
33        MESSAGE_PREFIX.as_slice(),
34        payload.feed_id.as_slice(),
35        registry_version_bytes.as_slice(),
36        signatures_required_bytes.as_slice(),
37        payload.signers_bitmap.as_slice(),
38        payload.value.as_slice(),
39        canonical_timestamp_bytes.as_slice(),
40    ])
41    .to_bytes()
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47
48    fn fixture_payload() -> DataUpdate {
49        DataUpdate {
50            // "solana-compat-job" right-padded to 32 bytes.
51            feed_id: [
52                0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2d, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x74, 0x2d,
53                0x6a, 0x6f, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
54                0x00, 0x00, 0x00, 0x00,
55            ],
56            registry_version: 1,
57            // "solana-compat-val" right-padded to 32 bytes.
58            value: [
59                0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2d, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x74, 0x2d,
60                0x76, 0x61, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
61                0x00, 0x00, 0x00, 0x00,
62            ],
63            canonical_timestamp: 1_700_000_123,
64            signatures_required: 8,
65            agg_sig_s: [0u8; 32],
66            commitment_addr: [0u8; 20],
67            // uint256(255) big-endian — bits 0..7 set.
68            signers_bitmap: [
69                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
70                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
71                0x00, 0x00, 0x00, 0xff,
72            ],
73        }
74    }
75
76    #[test]
77    fn message_prefix_is_keccak_of_domain() {
78        let expected = hashv(&[b"MOLPHA_MESSAGE_V1"]).to_bytes();
79        assert_eq!(MESSAGE_PREFIX, expected);
80    }
81
82    #[test]
83    fn compute_message_hash_is_deterministic() {
84        let p = fixture_payload();
85        assert_eq!(
86            compute_message_hash(&p, p.signatures_required),
87            compute_message_hash(&p, p.signatures_required)
88        );
89    }
90
91    #[test]
92    fn compute_message_hash_is_sensitive_to_each_field() {
93        let base = fixture_payload();
94        let base_hash = compute_message_hash(&base, base.signatures_required);
95
96        let mut a = fixture_payload();
97        a.registry_version += 1;
98        assert_ne!(compute_message_hash(&a, a.signatures_required), base_hash);
99
100        let b = fixture_payload();
101        assert_ne!(
102            compute_message_hash(&b, b.signatures_required.saturating_sub(1)),
103            base_hash
104        );
105
106        let mut c = fixture_payload();
107        c.signers_bitmap[31] ^= 0x01;
108        assert_ne!(compute_message_hash(&c, c.signatures_required), base_hash);
109
110        let mut d = fixture_payload();
111        d.value[0] ^= 0xff;
112        assert_ne!(compute_message_hash(&d, d.signatures_required), base_hash);
113
114        let mut e = fixture_payload();
115        e.canonical_timestamp += 1;
116        assert_ne!(compute_message_hash(&e, e.signatures_required), base_hash);
117
118        let mut f = fixture_payload();
119        f.feed_id[0] ^= 0xff;
120        assert_ne!(compute_message_hash(&f, f.signatures_required), base_hash);
121    }
122}