Skip to main content

molpha_verifier/
message.rs

1//! EVM-compatible attestation message hash.
2
3use solana_keccak_hasher::hashv;
4
5use crate::payload::AttestationPayload;
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 attestation 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: &AttestationPayload,
29    signers_bitmap: [u8; 32],
30    signatures_required: u32,
31) -> [u8; 32] {
32    let registry_version_bytes = payload.registry_version.to_be_bytes();
33    let signatures_required_bytes = signatures_required.to_be_bytes();
34    let canonical_timestamp_bytes = payload.canonical_timestamp.to_be_bytes();
35
36    hashv(&[
37        MESSAGE_PREFIX.as_slice(),
38        payload.value.as_slice(),
39        payload.source_id.as_slice(),
40        registry_version_bytes.as_slice(),
41        signatures_required_bytes.as_slice(),
42        canonical_timestamp_bytes.as_slice(),
43        signers_bitmap.as_slice(),
44    ])
45    .to_bytes()
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    fn fixture_payload() -> AttestationPayload {
53        AttestationPayload {
54            value: [
55                0xe1, 0xcd, 0x5b, 0x4f, 0x67, 0xac, 0xdc, 0x78, 0x68, 0xc3, 0xb1, 0x5f, 0x7b, 0x6c,
56                0xc2, 0xdc, 0x27, 0x70, 0x54, 0x53, 0x71, 0x34, 0x2c, 0xab, 0x76, 0x62, 0x71, 0xbb,
57                0x3f, 0xd5, 0xe7, 0x34,
58            ],
59            source_id: [
60                0x0b, 0x0c, 0x5c, 0x4a, 0x0e, 0x67, 0x58, 0x69, 0xda, 0xc2, 0x27, 0x2a, 0x40, 0x04,
61                0x63, 0x65, 0xa2, 0x9c, 0x8a, 0xe7, 0x63, 0x5e, 0x52, 0xc4, 0x94, 0xd8, 0x40, 0xda,
62                0x2e, 0xc8, 0x26, 0xcb,
63            ],
64            registry_version: 12,
65            signatures_required: 5,
66            canonical_timestamp: 1_708_525_180,
67        }
68    }
69
70    fn fixture_signers_bitmap() -> [u8; 32] {
71        // uint256(3613) big-endian — bits 0, 2, 3, 4, 9, 10, 11 set.
72        [
73            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
74            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
75            0x00, 0x00, 0x0e, 0x1d,
76        ]
77    }
78
79    #[test]
80    fn message_prefix_is_keccak_of_domain() {
81        let expected = hashv(&[b"MOLPHA_MESSAGE_V1"]).to_bytes();
82        assert_eq!(MESSAGE_PREFIX, expected);
83    }
84
85    #[test]
86    fn compute_message_hash_is_deterministic() {
87        let p = fixture_payload();
88        assert_eq!(
89            compute_message_hash(&p, fixture_signers_bitmap(), p.signatures_required),
90            compute_message_hash(&p, fixture_signers_bitmap(), p.signatures_required)
91        );
92    }
93
94    #[test]
95    fn compute_message_hash_is_sensitive_to_each_field() {
96        let base = fixture_payload();
97        let base_hash =
98            compute_message_hash(&base, fixture_signers_bitmap(), base.signatures_required);
99
100        let mut a = fixture_payload();
101        a.registry_version += 1;
102        assert_ne!(
103            compute_message_hash(&a, fixture_signers_bitmap(), a.signatures_required),
104            base_hash
105        );
106
107        let b = fixture_payload();
108        assert_ne!(
109            compute_message_hash(
110                &b,
111                fixture_signers_bitmap(),
112                b.signatures_required.saturating_sub(1)
113            ),
114            base_hash
115        );
116
117        let c = fixture_payload();
118        let mut signers_bitmap = fixture_signers_bitmap();
119        signers_bitmap[31] ^= 0x01;
120        assert_ne!(
121            compute_message_hash(&c, signers_bitmap, c.signatures_required),
122            base_hash
123        );
124
125        let mut d = fixture_payload();
126        d.value[0] ^= 0xff;
127        assert_ne!(
128            compute_message_hash(&d, fixture_signers_bitmap(), d.signatures_required),
129            base_hash
130        );
131
132        let mut e = fixture_payload();
133        e.canonical_timestamp += 1;
134        assert_ne!(
135            compute_message_hash(&e, fixture_signers_bitmap(), e.signatures_required),
136            base_hash
137        );
138
139        let mut f = fixture_payload();
140        f.source_id[0] ^= 0xff;
141        assert_ne!(
142            compute_message_hash(&f, fixture_signers_bitmap(), f.signatures_required),
143            base_hash
144        );
145    }
146}