Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 @@ -237,6 +237,7 @@ impl ErrorWithCode for BasicError {
Self::ShieldedEmptyProofError(_) => 10820,
Self::ShieldedZeroAnchorError(_) => 10821,
Self::ShieldedInvalidValueBalanceError(_) => 10822,
Self::ShieldedEncryptedNoteSizeMismatchError(_) => 10823,
Self::ShieldedTooManyActionsError(_) => 10825,
}
}
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.
/// Canonical source of truth — drive-abci imports this constant.
pub const ENCRYPTED_NOTE_SIZE: usize = 216;

/// 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,104 @@ 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(e)
)] => {
assert_eq!(e.expected_size(), ENCRYPTED_NOTE_SIZE as u32);
assert_eq!(e.actual_size(), 100);
}
);
}

#[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(e)
)] => {
assert_eq!(e.expected_size(), ENCRYPTED_NOTE_SIZE as u32);
assert_eq!(e.actual_size(), 300);
}
);
}

#[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(e)
)] => {
assert_eq!(e.expected_size(), ENCRYPTED_NOTE_SIZE as u32);
assert_eq!(e.actual_size(), 0);
}
);
}

#[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
@@ -1,4 +1,4 @@
pub(crate) mod common_validation;
pub mod common_validation;
pub mod shield_from_asset_lock_transition;
pub mod shield_transition;
pub mod shielded_transfer_transition;
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::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