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, sourceId, 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(
28    payload: &DataUpdate,
29    signers_bitmap: [u8; 32],
30    signatures_required: u8,
31) -> [u8; 32] {
32    let registry_version_bytes = payload.registry_version.to_be_bytes();
33    let signatures_required_bytes = u32::from(signatures_required).to_be_bytes();
34    let canonical_timestamp_bytes = (payload.canonical_timestamp as u64).to_be_bytes();
35
36    hashv(&[
37        MESSAGE_PREFIX.as_slice(),
38        payload.source_id.as_slice(),
39        registry_version_bytes.as_slice(),
40        signatures_required_bytes.as_slice(),
41        signers_bitmap.as_slice(),
42        payload.value.as_slice(),
43        canonical_timestamp_bytes.as_slice(),
44    ])
45    .to_bytes()
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    fn fixture_payload() -> DataUpdate {
53        DataUpdate {
54            // "solana-compat-job" right-padded to 32 bytes.
55            source_id: [
56                0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2d, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x74, 0x2d,
57                0x6a, 0x6f, 0x62, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
58                0x00, 0x00, 0x00, 0x00,
59            ],
60            registry_version: 1,
61            // "solana-compat-val" right-padded to 32 bytes.
62            value: vec![
63                0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x2d, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x74, 0x2d,
64                0x76, 0x61, 0x6c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
65                0x00, 0x00, 0x00, 0x00,
66            ],
67            canonical_timestamp: 1_700_000_123,
68            signatures_required: 8,
69        }
70    }
71
72    fn fixture_signers_bitmap() -> [u8; 32] {
73        // uint256(255) big-endian — bits 0..7 set.
74        [
75            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
76            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
77            0x00, 0x00, 0x00, 0xff,
78        ]
79    }
80
81    #[test]
82    fn message_prefix_is_keccak_of_domain() {
83        let expected = hashv(&[b"MOLPHA_MESSAGE_V1"]).to_bytes();
84        assert_eq!(MESSAGE_PREFIX, expected);
85    }
86
87    #[test]
88    fn compute_message_hash_is_deterministic() {
89        let p = fixture_payload();
90        assert_eq!(
91            compute_message_hash(&p, fixture_signers_bitmap(), p.signatures_required),
92            compute_message_hash(&p, fixture_signers_bitmap(), p.signatures_required)
93        );
94    }
95
96    #[test]
97    fn compute_message_hash_is_sensitive_to_each_field() {
98        let base = fixture_payload();
99        let base_hash =
100            compute_message_hash(&base, fixture_signers_bitmap(), base.signatures_required);
101
102        let mut a = fixture_payload();
103        a.registry_version += 1;
104        assert_ne!(
105            compute_message_hash(&a, fixture_signers_bitmap(), a.signatures_required),
106            base_hash
107        );
108
109        let b = fixture_payload();
110        assert_ne!(
111            compute_message_hash(
112                &b,
113                fixture_signers_bitmap(),
114                b.signatures_required.saturating_sub(1)
115            ),
116            base_hash
117        );
118
119        let c = fixture_payload();
120        let mut signers_bitmap = fixture_signers_bitmap();
121        signers_bitmap[31] ^= 0x01;
122        assert_ne!(
123            compute_message_hash(&c, signers_bitmap, c.signatures_required),
124            base_hash
125        );
126
127        let mut d = fixture_payload();
128        d.value[0] ^= 0xff;
129        assert_ne!(
130            compute_message_hash(&d, fixture_signers_bitmap(), d.signatures_required),
131            base_hash
132        );
133
134        let mut e = fixture_payload();
135        e.canonical_timestamp += 1;
136        assert_ne!(
137            compute_message_hash(&e, fixture_signers_bitmap(), e.signatures_required),
138            base_hash
139        );
140
141        let mut f = fixture_payload();
142        f.source_id[0] ^= 0xff;
143        assert_ne!(
144            compute_message_hash(&f, fixture_signers_bitmap(), f.signatures_required),
145            base_hash
146        );
147    }
148}