dpp/state_transition/state_transitions/shielded/
common_validation.rs

1use crate::consensus::basic::state_transition::{
2    ShieldedEmptyProofError, ShieldedEncryptedNoteSizeMismatchError, ShieldedNoActionsError,
3    ShieldedTooManyActionsError, ShieldedZeroAnchorError,
4};
5use crate::consensus::basic::BasicError;
6use crate::shielded::SerializedAction;
7use crate::validation::SimpleConsensusValidationResult;
8
9/// Expected size of the encrypted_note field in each SerializedAction.
10/// This is epk (32) + enc_ciphertext (104) + out_ciphertext (80) = 216 bytes.
11/// Canonical source of truth — drive-abci imports this constant.
12pub const ENCRYPTED_NOTE_SIZE: usize = 216;
13
14/// Validate that the actions list is not empty and does not exceed the maximum.
15pub fn validate_actions_count(
16    actions: &[SerializedAction],
17    max_actions: u16,
18) -> SimpleConsensusValidationResult {
19    if actions.is_empty() {
20        SimpleConsensusValidationResult::new_with_error(
21            BasicError::ShieldedNoActionsError(ShieldedNoActionsError::new()).into(),
22        )
23    } else if actions.len() > max_actions as usize {
24        SimpleConsensusValidationResult::new_with_error(
25            BasicError::ShieldedTooManyActionsError(ShieldedTooManyActionsError::new(
26                actions.len().min(u16::MAX as usize) as u16,
27                max_actions,
28            ))
29            .into(),
30        )
31    } else {
32        SimpleConsensusValidationResult::new()
33    }
34}
35
36/// Validate that the proof is not empty.
37pub fn validate_proof_not_empty(proof: &[u8]) -> SimpleConsensusValidationResult {
38    if proof.is_empty() {
39        SimpleConsensusValidationResult::new_with_error(
40            BasicError::ShieldedEmptyProofError(ShieldedEmptyProofError::new()).into(),
41        )
42    } else {
43        SimpleConsensusValidationResult::new()
44    }
45}
46
47/// Validate that the anchor is not all zeros (for transitions that consume notes).
48pub fn validate_anchor_not_zero(anchor: &[u8; 32]) -> SimpleConsensusValidationResult {
49    if *anchor == [0u8; 32] {
50        SimpleConsensusValidationResult::new_with_error(
51            BasicError::ShieldedZeroAnchorError(ShieldedZeroAnchorError::new()).into(),
52        )
53    } else {
54        SimpleConsensusValidationResult::new()
55    }
56}
57
58/// Defense-in-depth: validate that every action's `encrypted_note` field is exactly
59/// `ENCRYPTED_NOTE_SIZE` (216) bytes. This rejects malformed data early at the DPP
60/// layer before it reaches the ABCI bundle reconstruction, saving network bandwidth.
61pub fn validate_encrypted_note_sizes(
62    actions: &[SerializedAction],
63) -> SimpleConsensusValidationResult {
64    for action in actions {
65        if action.encrypted_note.len() != ENCRYPTED_NOTE_SIZE {
66            return SimpleConsensusValidationResult::new_with_error(
67                BasicError::ShieldedEncryptedNoteSizeMismatchError(
68                    ShieldedEncryptedNoteSizeMismatchError::new(
69                        ENCRYPTED_NOTE_SIZE as u32,
70                        action.encrypted_note.len() as u32,
71                    ),
72                )
73                .into(),
74            );
75        }
76    }
77    SimpleConsensusValidationResult::new()
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::consensus::ConsensusError;
84    use assert_matches::assert_matches;
85
86    fn dummy_action() -> SerializedAction {
87        SerializedAction {
88            nullifier: [1u8; 32],
89            rk: [2u8; 32],
90            cmx: [3u8; 32],
91            encrypted_note: vec![4u8; 216],
92            cv_net: [5u8; 32],
93            spend_auth_sig: [6u8; 64],
94        }
95    }
96
97    // --- validate_actions_count ---
98
99    #[test]
100    fn validate_actions_count_should_reject_empty_actions() {
101        let result = validate_actions_count(&[], 100);
102        assert_matches!(
103            result.errors.as_slice(),
104            [ConsensusError::BasicError(
105                BasicError::ShieldedNoActionsError(_)
106            )]
107        );
108    }
109
110    #[test]
111    fn validate_actions_count_should_accept_single_action() {
112        let actions = vec![dummy_action()];
113        let result = validate_actions_count(&actions, 100);
114        assert!(
115            result.is_valid(),
116            "Expected valid, got: {:?}",
117            result.errors
118        );
119    }
120
121    #[test]
122    fn validate_actions_count_should_accept_exactly_max_actions() {
123        let actions = vec![dummy_action(); 5];
124        let result = validate_actions_count(&actions, 5);
125        assert!(
126            result.is_valid(),
127            "Expected valid, got: {:?}",
128            result.errors
129        );
130    }
131
132    #[test]
133    fn validate_actions_count_should_reject_more_than_max_actions() {
134        let actions = vec![dummy_action(); 6];
135        let result = validate_actions_count(&actions, 5);
136        assert_matches!(
137            result.errors.as_slice(),
138            [ConsensusError::BasicError(
139                BasicError::ShieldedTooManyActionsError(_)
140            )]
141        );
142    }
143
144    // --- validate_proof_not_empty ---
145
146    #[test]
147    fn validate_proof_not_empty_should_reject_empty_proof() {
148        let result = validate_proof_not_empty(&[]);
149        assert_matches!(
150            result.errors.as_slice(),
151            [ConsensusError::BasicError(
152                BasicError::ShieldedEmptyProofError(_)
153            )]
154        );
155    }
156
157    #[test]
158    fn validate_proof_not_empty_should_accept_non_empty_proof() {
159        let result = validate_proof_not_empty(&[1u8; 100]);
160        assert!(
161            result.is_valid(),
162            "Expected valid, got: {:?}",
163            result.errors
164        );
165    }
166
167    // --- validate_anchor_not_zero ---
168
169    #[test]
170    fn validate_anchor_not_zero_should_reject_all_zero_anchor() {
171        let result = validate_anchor_not_zero(&[0u8; 32]);
172        assert_matches!(
173            result.errors.as_slice(),
174            [ConsensusError::BasicError(
175                BasicError::ShieldedZeroAnchorError(_)
176            )]
177        );
178    }
179
180    #[test]
181    fn validate_anchor_not_zero_should_accept_non_zero_anchor() {
182        let result = validate_anchor_not_zero(&[7u8; 32]);
183        assert!(
184            result.is_valid(),
185            "Expected valid, got: {:?}",
186            result.errors
187        );
188    }
189
190    #[test]
191    fn validate_anchor_not_zero_should_accept_single_bit_set() {
192        let mut anchor = [0u8; 32];
193        anchor[31] = 1;
194        let result = validate_anchor_not_zero(&anchor);
195        assert!(
196            result.is_valid(),
197            "Expected valid, got: {:?}",
198            result.errors
199        );
200    }
201
202    // --- validate_encrypted_note_sizes ---
203
204    #[test]
205    fn validate_encrypted_note_sizes_should_accept_correct_size() {
206        let actions = vec![dummy_action()];
207        let result = validate_encrypted_note_sizes(&actions);
208        assert!(
209            result.is_valid(),
210            "Expected valid, got: {:?}",
211            result.errors
212        );
213    }
214
215    #[test]
216    fn validate_encrypted_note_sizes_should_accept_multiple_correct_actions() {
217        let actions = vec![dummy_action(); 3];
218        let result = validate_encrypted_note_sizes(&actions);
219        assert!(
220            result.is_valid(),
221            "Expected valid, got: {:?}",
222            result.errors
223        );
224    }
225
226    #[test]
227    fn validate_encrypted_note_sizes_should_reject_too_short() {
228        let mut action = dummy_action();
229        action.encrypted_note = vec![4u8; 100]; // Too short
230        let actions = vec![action];
231        let result = validate_encrypted_note_sizes(&actions);
232        assert_matches!(
233            result.errors.as_slice(),
234            [ConsensusError::BasicError(
235                BasicError::ShieldedEncryptedNoteSizeMismatchError(e)
236            )] => {
237                assert_eq!(e.expected_size(), ENCRYPTED_NOTE_SIZE as u32);
238                assert_eq!(e.actual_size(), 100);
239            }
240        );
241    }
242
243    #[test]
244    fn validate_encrypted_note_sizes_should_reject_too_long() {
245        let mut action = dummy_action();
246        action.encrypted_note = vec![4u8; 300]; // Too long
247        let actions = vec![action];
248        let result = validate_encrypted_note_sizes(&actions);
249        assert_matches!(
250            result.errors.as_slice(),
251            [ConsensusError::BasicError(
252                BasicError::ShieldedEncryptedNoteSizeMismatchError(e)
253            )] => {
254                assert_eq!(e.expected_size(), ENCRYPTED_NOTE_SIZE as u32);
255                assert_eq!(e.actual_size(), 300);
256            }
257        );
258    }
259
260    #[test]
261    fn validate_encrypted_note_sizes_should_reject_empty() {
262        let mut action = dummy_action();
263        action.encrypted_note = vec![]; // Empty
264        let actions = vec![action];
265        let result = validate_encrypted_note_sizes(&actions);
266        assert_matches!(
267            result.errors.as_slice(),
268            [ConsensusError::BasicError(
269                BasicError::ShieldedEncryptedNoteSizeMismatchError(e)
270            )] => {
271                assert_eq!(e.expected_size(), ENCRYPTED_NOTE_SIZE as u32);
272                assert_eq!(e.actual_size(), 0);
273            }
274        );
275    }
276
277    #[test]
278    fn validate_encrypted_note_sizes_should_reject_second_invalid_action() {
279        let good_action = dummy_action();
280        let mut bad_action = dummy_action();
281        bad_action.encrypted_note = vec![4u8; 100];
282        let actions = vec![good_action, bad_action];
283        let result = validate_encrypted_note_sizes(&actions);
284        assert_matches!(
285            result.errors.as_slice(),
286            [ConsensusError::BasicError(
287                BasicError::ShieldedEncryptedNoteSizeMismatchError(_)
288            )]
289        );
290    }
291
292    #[test]
293    fn validate_encrypted_note_sizes_should_accept_empty_actions_list() {
294        let result = validate_encrypted_note_sizes(&[]);
295        assert!(
296            result.is_valid(),
297            "Expected valid for empty actions list, got: {:?}",
298            result.errors
299        );
300    }
301}