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 `(value_hash, value_len)` commitment for a raw feed value.
16///
17/// This is the single place signing nodes and verifiers derive the commitment that is hashed into
18/// the EVM-compatible message. The wire [`DataUpdate`] does not store these fields.
19///
20/// # Panics
21/// If `raw_value` is longer than `u32::MAX` bytes.
22pub fn value_commitment(raw_value: &[u8]) -> ([u8; 32], u32) {
23    let len = u32::try_from(raw_value.len()).expect("raw value length exceeds u32::MAX");
24    (hashv(&[raw_value]).to_bytes(), len)
25}
26
27/// Compute the EVM-compatible `DataUpdate` message hash.
28///
29/// Matches `Validator._constructMessage` in the EVM reference implementation:
30/// ```text
31/// keccak256(abi.encodePacked(
32///     MESSAGE_PREFIX, feedId, registryVersion, signaturesRequired,
33///     signersBitmap, valueHash, valueLen, canonicalTimestamp
34/// ))
35/// ```
36/// where `valueHash = keccak256(rawValue)` and `valueLen = uint32(rawValue.length)` — derived from
37/// `raw_value` (not stored on the wire payload).
38pub fn compute_message_hash(payload: &DataUpdate, raw_value: &[u8]) -> [u8; 32] {
39    let (value_hash, value_len) = value_commitment(raw_value);
40    let registry_version_bytes = payload.registry_version.to_be_bytes();
41    let signatures_required_bytes = u32::from(payload.signatures_required).to_be_bytes();
42    let value_len_bytes = value_len.to_be_bytes();
43    let canonical_timestamp_bytes = (payload.canonical_timestamp as u64).to_be_bytes();
44
45    hashv(&[
46        MESSAGE_PREFIX.as_slice(),
47        payload.feed_id.as_slice(),
48        registry_version_bytes.as_slice(),
49        signatures_required_bytes.as_slice(),
50        payload.signers_bitmap.as_slice(),
51        value_hash.as_slice(),
52        value_len_bytes.as_slice(),
53        canonical_timestamp_bytes.as_slice(),
54    ])
55    .to_bytes()
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    fn fixture_payload() -> DataUpdate {
63        DataUpdate {
64            // "solana-compat-job" right-padded to 32 bytes.
65            feed_id: [
66                0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2d, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x74, 0x2d,
67                0x6a, 0x6f, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
68                0x00, 0x00, 0x00, 0x00,
69            ],
70            registry_version: 1,
71            canonical_timestamp: 1_700_000_123,
72            signatures_required: 8,
73            agg_sig_s: [0u8; 32],
74            commitment_addr: [0u8; 20],
75            // uint256(255) big-endian — bits 0..7 set.
76            signers_bitmap: [
77                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
78                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
79                0x00, 0x00, 0x00, 0xff,
80            ],
81        }
82    }
83
84    const FIXTURE_RAW: &[u8] = b"solana-compat-val exceeding thirty-two bytes";
85
86    #[test]
87    fn message_prefix_is_keccak_of_domain() {
88        let expected = hashv(&[b"MOLPHA_MESSAGE_V1"]).to_bytes();
89        assert_eq!(MESSAGE_PREFIX, expected);
90    }
91
92    #[test]
93    fn value_commitment_is_keccak_and_len() {
94        let raw = b"molpha raw value longer than thirty-two bytes";
95        let (hash, len) = value_commitment(raw);
96        assert_eq!(hash, hashv(&[raw]).to_bytes());
97        assert_eq!(len, raw.len() as u32);
98    }
99
100    #[test]
101    fn compute_message_hash_is_deterministic() {
102        let p = fixture_payload();
103        assert_eq!(
104            compute_message_hash(&p, FIXTURE_RAW),
105            compute_message_hash(&p, FIXTURE_RAW)
106        );
107    }
108
109    #[test]
110    fn compute_message_hash_is_sensitive_to_each_field() {
111        let base = fixture_payload();
112        let base_hash = compute_message_hash(&base, FIXTURE_RAW);
113
114        let mut a = fixture_payload();
115        a.registry_version += 1;
116        assert_ne!(compute_message_hash(&a, FIXTURE_RAW), base_hash);
117
118        let mut b = fixture_payload();
119        b.signatures_required = b.signatures_required.saturating_sub(1);
120        assert_ne!(compute_message_hash(&b, FIXTURE_RAW), base_hash);
121
122        let mut c = fixture_payload();
123        c.signers_bitmap[31] ^= 0x01;
124        assert_ne!(compute_message_hash(&c, FIXTURE_RAW), base_hash);
125
126        let mut tampered_raw = FIXTURE_RAW.to_vec();
127        tampered_raw[0] ^= 0xff;
128        assert_ne!(compute_message_hash(&base, &tampered_raw), base_hash);
129
130        let shortened = &FIXTURE_RAW[..FIXTURE_RAW.len() - 1];
131        assert_ne!(compute_message_hash(&base, shortened), base_hash);
132
133        let mut f = fixture_payload();
134        f.canonical_timestamp += 1;
135        assert_ne!(compute_message_hash(&f, FIXTURE_RAW), base_hash);
136
137        let mut g = fixture_payload();
138        g.feed_id[0] ^= 0xff;
139        assert_ne!(compute_message_hash(&g, FIXTURE_RAW), base_hash);
140    }
141}