Skip to content
Merged
2 changes: 1 addition & 1 deletion book/src/error-handling/error-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Error codes are organized into ranges that correspond to error categories and su
|-------|----------|----------|
| 10000-10099 | Versioning | `UnsupportedVersionError` (10000), `ProtocolVersionParsingError` (10001), `IncompatibleProtocolVersionError` (10004) |
| 10100-10199 | Structure | `JsonSchemaCompilationError` (10100), `InvalidIdentifierError` (10102), `ValueError` (10103) |
| 10200-10276 | Data Contract | `DataContractMaxDepthExceedError` (10200), `DuplicateIndexError` (10201), `InvalidDataContractIdError` (10204), `DataContractInvalidRequiredFieldsUpdateError` (10276) |
| 10200-10277 | Data Contract | `DataContractMaxDepthExceedError` (10200), `DuplicateIndexError` (10201), `InvalidDataContractIdError` (10204), `DataContractInvalidRequiredFieldsUpdateError` (10276), `PreProgrammedDistributionAmountOverLimitError` (10277) |
| 10350-10359 | Groups | `GroupPositionDoesNotExistError` (10350), `GroupExceedsMaxMembersError` (10354) |
| 10360-10367 | Contract Groups | `ContractGroupMembershipsOverLimitError` (10360), `InvalidContractGroupAdminsError` (10364), `InvalidContractGroupDescriptionLengthError` (10367); 10365 unassigned |
| 10400-10418 | Documents | `DataContractNotPresentError` (10400), `DuplicateDocumentTransitionsWithIdsError` (10401) |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod validate_amounts;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use crate::data_contract::associated_token::token_pre_programmed_distribution::TokenPreProgrammedDistribution;
use crate::data_contract::TokenContractPosition;
use crate::validation::SimpleConsensusValidationResult;
use crate::ProtocolError;
use platform_version::version::PlatformVersion;

mod v0;

impl TokenPreProgrammedDistribution {
/// Validates that the amounts of every release can be stored.
///
/// Drive stores each release as a sum tree of its recipients' amounts, so an amount above
/// `i64::MAX`, or a release whose amounts total more than that, can not be written. Without
/// this check such a distribution passes validation and then fails inside Drive as an
/// internal error, which is never paid for and only makes the transition disappear.
///
/// `token_position` is the position of the token in its contract, reported in the error.
pub fn validate_amounts(
&self,
token_position: TokenContractPosition,
platform_version: &PlatformVersion,
) -> Result<SimpleConsensusValidationResult, ProtocolError> {
match platform_version
.dpp
.contract_versions
.token_versions
.validate_pre_programmed_distribution_amounts
{
0 => Ok(self.validate_amounts_v0(token_position)),
version => Err(ProtocolError::UnknownVersionMismatch {
method: "TokenPreProgrammedDistribution::validate_amounts".to_string(),
known_versions: vec![0],
received: version,
}),
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
use crate::balances::credits::TokenAmount;
use crate::consensus::basic::data_contract::PreProgrammedDistributionAmountOverLimitError;
use crate::data_contract::associated_token::token_pre_programmed_distribution::accessors::v0::TokenPreProgrammedDistributionV0Methods;
use crate::data_contract::associated_token::token_pre_programmed_distribution::TokenPreProgrammedDistribution;
use crate::data_contract::TokenContractPosition;
use crate::validation::SimpleConsensusValidationResult;

impl TokenPreProgrammedDistribution {
#[inline(always)]
pub(super) fn validate_amounts_v0(
&self,
token_position: TokenContractPosition,
) -> SimpleConsensusValidationResult {
for (timestamp, release) in self.distributions() {
// A single amount over the limit puts the total over it too, so the total is the
// only thing to check. A total that does not even fit a `u64` is over the limit.
let total_fits = release
.values()
.try_fold(0 as TokenAmount, |total, amount| total.checked_add(*amount))
.is_some_and(|total| total <= i64::MAX as TokenAmount);

if !total_fits {
return SimpleConsensusValidationResult::new_with_error(
PreProgrammedDistributionAmountOverLimitError::new(token_position, *timestamp)
.into(),
);
}
}

SimpleConsensusValidationResult::new()
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::consensus::basic::BasicError;
use crate::consensus::codes::ErrorWithCode;
use crate::consensus::ConsensusError;
use crate::data_contract::associated_token::token_pre_programmed_distribution::v0::TokenPreProgrammedDistributionV0;
use crate::prelude::TimestampMillis;
use assert_matches::assert_matches;
use platform_value::Identifier;
use platform_version::version::PlatformVersion;
use std::collections::BTreeMap;

const MAX_AMOUNT: TokenAmount = i64::MAX as TokenAmount;

fn distribution(
releases: impl IntoIterator<Item = (TimestampMillis, Vec<TokenAmount>)>,
) -> TokenPreProgrammedDistribution {
let distributions = releases
.into_iter()
.map(|(timestamp, amounts)| {
let release = amounts
.into_iter()
.enumerate()
.map(|(recipient, amount)| {
(Identifier::from([recipient as u8 + 1; 32]), amount)
})
.collect::<BTreeMap<_, _>>();
(timestamp, release)
})
.collect();
TokenPreProgrammedDistribution::V0(TokenPreProgrammedDistributionV0 { distributions })
}

#[test]
fn should_accept_releases_whose_totals_fit() {
let platform_version = PlatformVersion::latest();

let result = distribution([
(100, vec![MAX_AMOUNT]),
(200, vec![MAX_AMOUNT - 1, 1]),
(300, vec![]),
])
.validate_amounts(0, platform_version)
.expect("expected to validate");

assert!(result.is_valid(), "unexpected errors: {:?}", result.errors);
}

#[test]
fn should_reject_an_amount_over_the_limit() {
let platform_version = PlatformVersion::latest();

let result = distribution([(100, vec![5]), (200, vec![MAX_AMOUNT + 1])])
.validate_amounts(3, platform_version)
.expect("expected to validate");

assert_matches!(
result.errors.as_slice(),
[ConsensusError::BasicError(
BasicError::PreProgrammedDistributionAmountOverLimitError(e)
)] if e.token_position() == 3 && e.timestamp() == 200
);
assert_eq!(result.errors[0].code(), 10277);
}

#[test]
fn should_reject_a_release_whose_amounts_total_over_the_limit() {
let platform_version = PlatformVersion::latest();

let result = distribution([(100, vec![MAX_AMOUNT, 1])])
.validate_amounts(0, platform_version)
.expect("expected to validate");

assert_matches!(
result.errors.as_slice(),
[ConsensusError::BasicError(
BasicError::PreProgrammedDistributionAmountOverLimitError(e)
)] if e.token_position() == 0 && e.timestamp() == 100
);
}

#[test]
fn should_reject_a_release_whose_total_does_not_fit_a_u64() {
let platform_version = PlatformVersion::latest();

let result = distribution([(100, vec![u64::MAX, u64::MAX])])
.validate_amounts(0, platform_version)
.expect("expected to validate");

assert_matches!(
result.errors.as_slice(),
[ConsensusError::BasicError(
BasicError::PreProgrammedDistributionAmountOverLimitError(_)
)]
);
}

#[test]
fn should_judge_every_release_on_its_own_total() {
let platform_version = PlatformVersion::latest();

// Releases are separate sum trees, so two full releases do not add up.
let result = distribution([(100, vec![MAX_AMOUNT]), (200, vec![MAX_AMOUNT])])
.validate_amounts(0, platform_version)
.expect("expected to validate");

assert!(result.is_valid(), "unexpected errors: {:?}", result.errors);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize};
use std::fmt;

pub mod accessors;
mod methods;

pub mod v0;

Expand Down
Loading
Loading