Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/rs-dpp/src/errors/consensus/basic/basic_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ use crate::consensus::basic::state_transition::{
InputWitnessCountMismatchError, InputsNotLessThanOutputsError, InsufficientFundingAmountError,
InvalidRemainderOutputCountError, InvalidStateTransitionTypeError,
MissingStateTransitionTypeError, OutputAddressAlsoInputError, OutputBelowMinimumError,
OutputsNotGreaterThanInputsError, ShieldedEmptyProofError, ShieldedInvalidValueBalanceError,
OutputsNotGreaterThanInputsError, ShieldedEmptyProofError,
ShieldedEncryptedNoteSizeMismatchError, ShieldedInvalidValueBalanceError,
ShieldedNoActionsError, ShieldedTooManyActionsError, ShieldedZeroAnchorError,
StateTransitionMaxSizeExceededError, StateTransitionNotActiveError, TransitionNoInputsError,
TransitionNoOutputsError, TransitionOverMaxInputsError, TransitionOverMaxOutputsError,
Expand Down Expand Up @@ -673,6 +674,9 @@ pub enum BasicError {

#[error(transparent)]
ShieldedInvalidValueBalanceError(ShieldedInvalidValueBalanceError),

#[error(transparent)]
ShieldedEncryptedNoteSizeMismatchError(ShieldedEncryptedNoteSizeMismatchError),
}

impl From<BasicError> for ConsensusError {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod output_address_also_input_error;
mod output_below_minimum_error;
mod outputs_not_greater_than_inputs_error;
mod shielded_empty_proof_error;
mod shielded_encrypted_note_size_mismatch_error;
mod shielded_invalid_value_balance_error;
mod shielded_no_actions_error;
mod shielded_too_many_actions_error;
Expand Down Expand Up @@ -43,6 +44,7 @@ pub use output_address_also_input_error::*;
pub use output_below_minimum_error::*;
pub use outputs_not_greater_than_inputs_error::*;
pub use shielded_empty_proof_error::*;
pub use shielded_encrypted_note_size_mismatch_error::*;
pub use shielded_invalid_value_balance_error::*;
pub use shielded_no_actions_error::*;
pub use shielded_too_many_actions_error::*;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use crate::consensus::basic::BasicError;
use crate::consensus::ConsensusError;
use crate::errors::ProtocolError;
use bincode::{Decode, Encode};
use platform_serialization_derive::{PlatformDeserialize, PlatformSerialize};
use thiserror::Error;

#[derive(
Error, Debug, Clone, PartialEq, Eq, Encode, Decode, PlatformSerialize, PlatformDeserialize,
)]
#[error(
"Shielded action encrypted_note has invalid size: expected {expected_size} bytes, got {actual_size} bytes"
)]
#[platform_serialize(unversioned)]
pub struct ShieldedEncryptedNoteSizeMismatchError {
/*

DO NOT CHANGE ORDER OF FIELDS WITHOUT INTRODUCING OF NEW VERSION

*/
expected_size: u32,
actual_size: u32,
}

impl ShieldedEncryptedNoteSizeMismatchError {
pub fn new(expected_size: u32, actual_size: u32) -> Self {
Self {
expected_size,
actual_size,
}
}

pub fn expected_size(&self) -> u32 {
self.expected_size
}

pub fn actual_size(&self) -> u32 {
self.actual_size
}
}

impl From<ShieldedEncryptedNoteSizeMismatchError> for ConsensusError {
fn from(err: ShieldedEncryptedNoteSizeMismatchError) -> Self {
Self::BasicError(BasicError::ShieldedEncryptedNoteSizeMismatchError(err))
}
}
1 change: 1 addition & 0 deletions packages/rs-dpp/src/errors/consensus/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ impl ErrorWithCode for BasicError {
Self::ShieldedZeroAnchorError(_) => 10821,
Self::ShieldedInvalidValueBalanceError(_) => 10822,
Self::ShieldedTooManyActionsError(_) => 10825,
Self::ShieldedEncryptedNoteSizeMismatchError(_) => 10823,
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
use crate::consensus::basic::state_transition::{
ShieldedEmptyProofError, ShieldedNoActionsError, ShieldedTooManyActionsError,
ShieldedZeroAnchorError,
ShieldedEmptyProofError, ShieldedEncryptedNoteSizeMismatchError, ShieldedNoActionsError,
ShieldedTooManyActionsError, ShieldedZeroAnchorError,
};
use crate::consensus::basic::BasicError;
use crate::shielded::SerializedAction;
use crate::validation::SimpleConsensusValidationResult;

/// Expected size of the encrypted_note field in each SerializedAction.
/// This is epk (32) + enc_ciphertext (104) + out_ciphertext (80) = 216 bytes.
/// Matches the ENCRYPTED_NOTE_SIZE constant in drive-abci's shielded_common module.
pub const ENCRYPTED_NOTE_SIZE: usize = 216;
Comment thread
QuantumExplorer marked this conversation as resolved.

/// Validate that the actions list is not empty and does not exceed the maximum.
pub fn validate_actions_count(
actions: &[SerializedAction],
Expand Down Expand Up @@ -50,6 +55,28 @@ pub fn validate_anchor_not_zero(anchor: &[u8; 32]) -> SimpleConsensusValidationR
}
}

/// Defense-in-depth: validate that every action's `encrypted_note` field is exactly
/// `ENCRYPTED_NOTE_SIZE` (216) bytes. This rejects malformed data early at the DPP
/// layer before it reaches the ABCI bundle reconstruction, saving network bandwidth.
pub fn validate_encrypted_note_sizes(
actions: &[SerializedAction],
) -> SimpleConsensusValidationResult {
for action in actions {
if action.encrypted_note.len() != ENCRYPTED_NOTE_SIZE {
return SimpleConsensusValidationResult::new_with_error(
BasicError::ShieldedEncryptedNoteSizeMismatchError(
ShieldedEncryptedNoteSizeMismatchError::new(
ENCRYPTED_NOTE_SIZE as u32,
action.encrypted_note.len() as u32,
),
)
.into(),
);
}
}
SimpleConsensusValidationResult::new()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -171,4 +198,95 @@ mod tests {
result.errors
);
}

// --- validate_encrypted_note_sizes ---

#[test]
fn validate_encrypted_note_sizes_should_accept_correct_size() {
let actions = vec![dummy_action()];
let result = validate_encrypted_note_sizes(&actions);
assert!(
result.is_valid(),
"Expected valid, got: {:?}",
result.errors
);
}

#[test]
fn validate_encrypted_note_sizes_should_accept_multiple_correct_actions() {
let actions = vec![dummy_action(); 3];
let result = validate_encrypted_note_sizes(&actions);
assert!(
result.is_valid(),
"Expected valid, got: {:?}",
result.errors
);
}

#[test]
fn validate_encrypted_note_sizes_should_reject_too_short() {
let mut action = dummy_action();
action.encrypted_note = vec![4u8; 100]; // Too short
let actions = vec![action];
let result = validate_encrypted_note_sizes(&actions);
assert_matches!(
result.errors.as_slice(),
[ConsensusError::BasicError(
BasicError::ShieldedEncryptedNoteSizeMismatchError(_)
)]
);
Comment thread
QuantumExplorer marked this conversation as resolved.
}

#[test]
fn validate_encrypted_note_sizes_should_reject_too_long() {
let mut action = dummy_action();
action.encrypted_note = vec![4u8; 300]; // Too long
let actions = vec![action];
let result = validate_encrypted_note_sizes(&actions);
assert_matches!(
result.errors.as_slice(),
[ConsensusError::BasicError(
BasicError::ShieldedEncryptedNoteSizeMismatchError(_)
)]
);
}

#[test]
fn validate_encrypted_note_sizes_should_reject_empty() {
let mut action = dummy_action();
action.encrypted_note = vec![]; // Empty
let actions = vec![action];
let result = validate_encrypted_note_sizes(&actions);
assert_matches!(
result.errors.as_slice(),
[ConsensusError::BasicError(
BasicError::ShieldedEncryptedNoteSizeMismatchError(_)
)]
);
}

#[test]
fn validate_encrypted_note_sizes_should_reject_second_invalid_action() {
let good_action = dummy_action();
let mut bad_action = dummy_action();
bad_action.encrypted_note = vec![4u8; 100];
let actions = vec![good_action, bad_action];
let result = validate_encrypted_note_sizes(&actions);
assert_matches!(
result.errors.as_slice(),
[ConsensusError::BasicError(
BasicError::ShieldedEncryptedNoteSizeMismatchError(_)
)]
);
}

#[test]
fn validate_encrypted_note_sizes_should_accept_empty_actions_list() {
let result = validate_encrypted_note_sizes(&[]);
assert!(
result.is_valid(),
"Expected valid for empty actions list, got: {:?}",
result.errors
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use crate::consensus::basic::state_transition::ShieldedInvalidValueBalanceError;
use crate::consensus::basic::BasicError;
use crate::state_transition::shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0;
use crate::state_transition::state_transitions::shielded::common_validation::{
validate_actions_count, validate_anchor_not_zero, validate_proof_not_empty,
validate_actions_count, validate_anchor_not_zero, validate_encrypted_note_sizes,
validate_proof_not_empty,
};
use crate::state_transition::StateTransitionStructureValidation;
use crate::validation::SimpleConsensusValidationResult;
Expand All @@ -24,6 +25,12 @@ impl StateTransitionStructureValidation for ShieldFromAssetLockTransitionV0 {
return result;
}

// Each action's encrypted_note must be exactly ENCRYPTED_NOTE_SIZE bytes
let result = validate_encrypted_note_sizes(&self.actions);
if !result.is_valid() {
return result;
}

// value_balance must be > 0 (credits flowing into pool)
if self.value_balance == 0 {
return SimpleConsensusValidationResult::new_with_error(
Expand Down Expand Up @@ -114,6 +121,21 @@ mod tests {
);
}

#[test]
fn should_reject_invalid_encrypted_note_size() {
let platform_version = PlatformVersion::latest();
let mut transition = valid_shield_from_asset_lock_transition();
transition.actions[0].encrypted_note = vec![4u8; 100]; // Wrong size

let result = transition.validate_structure(platform_version);
assert_matches!(
result.errors.as_slice(),
[ConsensusError::BasicError(
BasicError::ShieldedEncryptedNoteSizeMismatchError(_)
)]
);
}

#[test]
fn should_reject_empty_actions() {
let platform_version = PlatformVersion::latest();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ use crate::consensus::basic::state_transition::{
use crate::consensus::basic::BasicError;
use crate::state_transition::shield_transition::v0::ShieldTransitionV0;
use crate::state_transition::state_transitions::shielded::common_validation::{
validate_actions_count, validate_anchor_not_zero, validate_proof_not_empty,
validate_actions_count, validate_anchor_not_zero, validate_encrypted_note_sizes,
validate_proof_not_empty,
};
use crate::state_transition::StateTransitionStructureValidation;
use crate::validation::SimpleConsensusValidationResult;
Expand All @@ -29,6 +30,12 @@ impl StateTransitionStructureValidation for ShieldTransitionV0 {
return result;
}

// Each action's encrypted_note must be exactly ENCRYPTED_NOTE_SIZE bytes
let result = validate_encrypted_note_sizes(&self.actions);
if !result.is_valid() {
return result;
}

// Inputs must not be empty (shield requires address funding)
if self.inputs.is_empty() {
return SimpleConsensusValidationResult::new_with_error(
Expand Down Expand Up @@ -218,6 +225,21 @@ mod tests {
);
}

#[test]
fn should_reject_invalid_encrypted_note_size() {
let platform_version = PlatformVersion::latest();
let mut transition = valid_shield_transition();
transition.actions[0].encrypted_note = vec![4u8; 100]; // Wrong size

let result = transition.validate_structure(platform_version);
assert_matches!(
result.errors.as_slice(),
[ConsensusError::BasicError(
BasicError::ShieldedEncryptedNoteSizeMismatchError(_)
)]
);
}

#[test]
fn should_reject_empty_actions() {
let platform_version = PlatformVersion::latest();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use crate::consensus::basic::state_transition::ShieldedInvalidValueBalanceError;
use crate::consensus::basic::BasicError;
use crate::state_transition::shielded_transfer_transition::v0::ShieldedTransferTransitionV0;
use crate::state_transition::state_transitions::shielded::common_validation::{
validate_actions_count, validate_anchor_not_zero, validate_proof_not_empty,
validate_actions_count, validate_anchor_not_zero, validate_encrypted_note_sizes,
validate_proof_not_empty,
};
use crate::state_transition::StateTransitionStructureValidation;
use crate::validation::SimpleConsensusValidationResult;
Expand All @@ -24,6 +25,12 @@ impl StateTransitionStructureValidation for ShieldedTransferTransitionV0 {
return result;
}

// Each action's encrypted_note must be exactly ENCRYPTED_NOTE_SIZE bytes
let result = validate_encrypted_note_sizes(&self.actions);
if !result.is_valid() {
return result;
}

// value_balance must be positive (it IS the fee for shielded transfers)
if self.value_balance == 0 {
return SimpleConsensusValidationResult::new_with_error(
Expand Down Expand Up @@ -103,6 +110,21 @@ mod tests {
);
}

#[test]
fn should_reject_invalid_encrypted_note_size() {
let platform_version = PlatformVersion::latest();
let mut transition = valid_shielded_transfer_transition();
transition.actions[0].encrypted_note = vec![4u8; 100]; // Wrong size

let result = transition.validate_structure(platform_version);
assert_matches!(
result.errors.as_slice(),
[ConsensusError::BasicError(
BasicError::ShieldedEncryptedNoteSizeMismatchError(_)
)]
);
}

#[test]
fn should_reject_empty_actions() {
let platform_version = PlatformVersion::latest();
Expand Down
Loading
Loading