From 04b0755da3cfbdc5088ff5bf2e7b122cba2c1ab3 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sat, 13 Jun 2026 01:33:34 +0200 Subject: [PATCH] test(drive-abci): fix direct-selling tests broken by empty-schedule validation PRs #3856 and #3865 merged independently and conflict semantically: #3856 added tests that plant an empty SetPrices schedule via a state transition, while #3865 now rejects that transition at structure validation with TokenPricingScheduleEmptyError. Update the tests to assert the new upfront rejection, plant the empty schedule directly through the drive API to keep the transformer defense-in-depth regression coverage, and use a non-empty tiered schedule in the multi-token pricing test. Co-Authored-By: Claude Fable 5 --- .../batch/tests/token/direct_selling/mod.rs | 82 +++++++++++++++---- 1 file changed, 66 insertions(+), 16 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs index 8a189eac249..75a121d51df 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/direct_selling/mod.rs @@ -3,6 +3,7 @@ use super::*; mod token_selling_tests { use std::collections::BTreeMap; + use crate::platform_types::state_transitions_processing_result::StateTransitionsProcessingResult; use crate::{rpc::core::MockCoreRPCLike, test::helpers::setup::TempPlatform}; use super::*; @@ -547,7 +548,7 @@ mod token_selling_tests { (100, dash_to_credits!(10)), (500, dash_to_credits!(5)), ])), - TokenPricingSchedule::SetPrices(BTreeMap::new()), + TokenPricingSchedule::SetPrices(BTreeMap::from([(1, dash_to_credits!(2))])), ]; // Setup the test @@ -832,6 +833,35 @@ mod token_selling_tests { pricing: Option, identity_contract_nonce: &mut u64, ) -> (DataContract, Identifier) { + let (contract, token_id, processing_result) = create_token_with_pricing_result( + platform_version, + platform, + seller, + seller_signer, + seller_key, + pricing, + identity_contract_nonce, + ) + .await; + + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + (contract, token_id) + } + + /// Same as [`create_token_with_pricing`], but returns the set-price processing + /// result instead of asserting it succeeded, for tests that expect rejection. + async fn create_token_with_pricing_result( + platform_version: &PlatformVersion, + platform: &mut TempPlatform, + seller: &Identity, + seller_signer: &SimpleSigner, + seller_key: &IdentityPublicKey, + pricing: Option, + identity_contract_nonce: &mut u64, + ) -> (DataContract, Identifier, StateTransitionsProcessingResult) { let (contract, token_id) = create_token_contract_with_owner_identity( platform, seller.id(), @@ -884,22 +914,21 @@ mod token_selling_tests { platform_version, ); - assert_matches!( - processing_result.execution_results().as_slice(), - [StateTransitionExecutionResult::SuccessfulExecution { .. }] - ); - (contract, token_id) + (contract, token_id, processing_result) } /// Regression test for the chain-halt where an empty `SetPrices` schedule /// caused a `.expect("Map is not empty")` panic in the direct-purchase /// transformer. /// - /// An empty `SetPrices` schedule passes structure validation (which - /// explicitly skips the price) and state validation (auth-only), and is - /// stored verbatim. The transformer runs on every validator during block - /// execution, so a panic there would crash all validators and halt the - /// chain. With the fix, a purchase against an empty schedule must instead be + /// An empty `SetPrices` schedule is now rejected at structure validation + /// with `TokenPricingScheduleEmptyError`, so it can no longer reach state + /// through a state transition — asserted first. The transformer must still + /// handle an empty schedule defensively (defense in depth, e.g. state + /// written before the structure check existed): the transformer runs on + /// every validator during block execution, so a panic there would crash + /// all validators and halt the chain. The schedule is therefore planted + /// directly through the drive API, and a purchase against it must be /// rejected gracefully as `TokenNotForDirectSale` — no panic. #[tokio::test] async fn test_direct_purchase_empty_set_prices_does_not_panic() { @@ -915,23 +944,44 @@ mod token_selling_tests { let (buyer, buyer_signer, buyer_key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(10.0)); - // Plant: set an EMPTY tiered pricing schedule. This is accepted and - // stored (structure validation skips the price; state validation is - // auth-only), as asserted inside `create_token_with_pricing`. let empty_set_prices = TokenPricingSchedule::SetPrices(BTreeMap::new()); + // Setting an EMPTY tiered pricing schedule via a state transition must + // be rejected at structure validation. let mut identity_contract_nonce: u64 = 2; - let (contract, token_id) = create_token_with_pricing( + let (contract, token_id, processing_result) = create_token_with_pricing_result( platform_version, &mut platform, &seller, &seller_signer, &seller_key, - Some(empty_set_prices), + Some(empty_set_prices.clone()), &mut identity_contract_nonce, ) .await; + assert_matches!( + processing_result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::BasicError(BasicError::TokenPricingScheduleEmptyError(_)) + )] + ); + + // Plant: write the empty schedule straight into state through the + // drive API, bypassing state transition validation — simulating state + // written before the structure check existed. + platform + .drive + .token_set_direct_purchase_price( + token_id.to_buffer(), + Some(empty_set_prices), + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("expected to set empty pricing schedule directly"); + // Detonate: any direct purchase used to panic at // `set_prices.keys().next().expect("Map is not empty")`. let platform_state = platform.state.load();