Skip to content

Comments

test(dpp): add validate_structure error path tests for withdrawal and transfer transitions#3124

Open
thepastaclaw wants to merge 5 commits intodashpay:v3.1-devfrom
thepastaclaw:test/validation-error-paths-transitions
Open

test(dpp): add validate_structure error path tests for withdrawal and transfer transitions#3124
thepastaclaw wants to merge 5 commits intodashpay:v3.1-devfrom
thepastaclaw:test/validation-error-paths-transitions

Conversation

@thepastaclaw
Copy link
Contributor

@thepastaclaw thepastaclaw commented Feb 20, 2026

Issue Being Fixed

Tracker: thepastaclaw/tracker#14 (parent: #11)

Addresses findings I-03 and I-04 from the rs-dpp test quality audit.

What was done

Added missing validation error path tests for two state transition validate_structure methods.

I-04: IdentityCreditTransferToAddressesTransitionV0::validate_structure — 1 test → 5 tests

  • should_return_invalid_result_if_no_recipients — empty recipient map → TransitionNoOutputsError
  • should_return_invalid_result_if_over_max_recipients — max+1 recipients → TransitionOverMaxOutputsError
  • should_return_invalid_result_if_amount_below_minimum — amount=1 → OutputBelowMinimumError
  • should_return_valid_result_for_valid_transition — happy path with min_output_amount
  • (existing) should_return_invalid_result_if_recipient_sum_overflows

I-03: AddressCreditWithdrawalTransitionV0::validate_structure — 1 test → 16 tests

Added a valid_withdrawal_transition() baseline helper, then one-mutation-per-test:

  • should_return_invalid_result_if_no_inputsTransitionNoInputsError
  • should_return_invalid_result_if_over_max_inputsTransitionOverMaxInputsError
  • should_return_invalid_result_if_witness_count_mismatchInputWitnessCountMismatchError
  • should_return_invalid_result_if_output_address_also_inputOutputAddressAlsoInputError
  • should_return_invalid_result_if_fee_strategy_emptyFeeStrategyEmptyError
  • should_return_invalid_result_if_fee_strategy_too_many_stepsFeeStrategyTooManyStepsError
  • should_return_invalid_result_if_fee_strategy_duplicateFeeStrategyDuplicateError
  • should_return_invalid_result_if_fee_strategy_index_out_of_boundsFeeStrategyIndexOutOfBoundsError
  • should_return_invalid_result_if_input_below_minimumInputBelowMinimumError
  • should_return_invalid_result_if_output_below_minimumOutputBelowMinimumError
  • should_return_invalid_result_if_pooling_not_neverNotImplementedCreditWithdrawalTransitionPoolingError
  • should_return_invalid_result_if_core_fee_not_fibonacciInvalidCreditWithdrawalTransitionCoreFeeError
  • should_return_invalid_result_if_output_script_invalidInvalidCreditWithdrawalTransitionOutputScriptError
  • should_return_invalid_result_if_withdrawal_below_minWithdrawalBelowMinAmountError
  • should_return_valid_result_for_valid_transition — happy path
  • (existing) should_return_invalid_result_if_input_sum_overflows

All tests compile and pass.

Summary by CodeRabbit

  • Tests
    • Expanded test suite for address credit withdrawal transition validation with comprehensive edge case coverage.
    • Added validation tests for identity credit transfer transitions covering recipient limits and amount constraints.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 20, 2026

📝 Walkthrough

Walkthrough

This PR adds comprehensive unit tests for validation functions across two state transition modules: address credit withdrawal and identity credit transfer. The test suites cover numerous validation scenarios including edge cases, failure paths, and success cases, with no changes to production code.

Changes

Cohort / File(s) Summary
Address Credit Withdrawal Validation Tests
packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/state_transition_validation.rs
Added 347 lines of comprehensive unit tests for validate_structure, including helper constructors and utilities. Tests cover 15+ validation scenarios: no/excess inputs, witness mismatches, output duplication, fee strategy violations, overflow detection, amount constraints, script validation, and a valid transition case.
Identity Credit Transfer Validation Tests
packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/state_transition_validation.rs
Added 71 lines of unit tests for validate_structure, introducing four new test cases: no recipients, exceeding max recipients, amounts below minimum, and valid transitions. Complements existing overflow test.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

Poem

🐰 With whiskers twitching and tests in mind,
I hop through validation, all edge cases find,
No inputs, no outputs, fees gone awry—
Every error path tested, not a branch left dry!
Coverage blooming like clover so green, 🍀

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: adding validate_structure error path tests for two state-transition types (withdrawal and transfer transitions).
Linked Issues check ✅ Passed The PR fully implements coding objectives from issue #14: adds comprehensive error-path tests for AddressCreditWithdrawalTransitionV0 (I-03: 15 tests) and IdentityCreditTransferToAddressesTransitionV0 (I-04: 4 tests) validation methods.
Out of Scope Changes check ✅ Passed All changes are scoped to test additions for two validators; no unrelated modifications to production code, other validators, or unspecified functionality.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (5)
packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/state_transition_validation.rs (3)

257-273: Remove redundant use rand::SeedableRng; inside the function body.

rand::SeedableRng is already imported at the enclosing module level (line 254), so the inner use at line 258 is redundant.

🧹 Suggested cleanup
 fn valid_withdrawal_transition() -> AddressCreditWithdrawalTransitionV0 {
-    use rand::SeedableRng;
-
     let input_amount = 1_000_000_000u64;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/state_transition_validation.rs`
around lines 257 - 273, In the function valid_withdrawal_transition() remove the
redundant inner import "use rand::SeedableRng;" — SeedableRng is already
imported at the module level, so delete that line inside the function and keep
the existing use of rand::rngs::StdRng::seed_from_u64 unchanged (no other logic
needs modification).

571-603: WithdrawalBalanceMismatchError branch is not tested.

Production code lines 217–226 return WithdrawalBalanceMismatchError when input_sum <= output_amount. None of the new or existing tests exercise this path. A minimal case: set output to Some((different_addr, input_amount)) so input_sum == output_amount.

✅ Suggested test to add
#[test]
fn should_return_invalid_result_if_output_equals_input_sum() {
    let platform_version = PlatformVersion::latest();
    let mut transition = valid_withdrawal_transition();
    // output_amount == input_sum → withdrawal would be zero → balance mismatch
    let input_amount = *transition.inputs.values().next().unwrap();
    transition.output = Some((PlatformAddress::P2pkh([2u8; 20]), input_amount.1));

    let result = transition.validate_structure(platform_version);

    assert_matches!(
        result.errors.as_slice(),
        [crate::consensus::ConsensusError::BasicError(
            BasicError::WithdrawalBalanceMismatchError(_)
        )]
    );
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/state_transition_validation.rs`
around lines 571 - 603, Add a unit test that exercises the
WithdrawalBalanceMismatchError branch by creating a withdrawal transition via
valid_withdrawal_transition(), then set transition.output to Some((different
address than the input, input_amount)) so that input_sum == output_amount and
validate_structure(platform_version) returns the error; assert that
result.errors contains a BasicError::WithdrawalBalanceMismatchError coming from
validate_structure. Use the existing pattern from other tests
(PlatformVersion::latest(), assert_matches! on result.errors.as_slice()) and
reference transition.inputs to obtain the input_amount to assign to
transition.output.

392-417: The test currently works correctly, but the concern about validation ordering is worth addressing for future robustness.

The test uses PlatformVersion::latest() which returns v12, using STATE_TRANSITION_VERSIONS_V3 with max_address_inputs = 16 and max_address_fee_strategies = 4. Since the test creates 5 inputs (step_count = 4 + 1), it safely passes the inputs check (5 ≤ 16) and correctly triggers FeeStrategyTooManyStepsError at the fee-strategy check.

However, your concern is valid from a maintainability perspective. The validation order at lines 39 and 82 means that if these constants ever become misaligned (i.e., max_address_fee_strategies >= max_address_inputs), the test would silently pass the wrong error. While all current versions (v1, v2, v3) are safe, consider adding an explicit assertion or comment documenting the required invariant: max_address_fee_strategies < max_address_inputs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/state_transition_validation.rs`
around lines 392 - 417, The test should assert the invariant that
max_address_fee_strategies < max_address_inputs so the fee-strategy check runs
after the inputs-count check; inside
should_return_invalid_result_if_fee_strategy_too_many_steps, after computing
max_fee_strategies and step_count, read max_address_inputs from
PlatformVersion::latest().dpp.state_transitions.max_address_inputs and add a
short assertion like debug_assert!(max_fee_strategies < max_address_inputs) (or
a plain assert!() in the test) and/or a clarifying comment referencing
max_address_fee_strategies and max_address_inputs to prevent future regressions
where validation ordering could make the test assert the wrong error.
packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/state_transition_validation.rs (2)

100-117: Address encoding only covers 2 bytes — collapses if max_address_outputs ever reaches 65 536.

(i as u16) silently truncates, so entries with i = 0 and i = 65 536 (etc.) produce the same 20-byte key. BTreeMap deduplicates them, the map ends up with fewer than max + 1 entries, and the test would never reach TransitionOverMaxOutputsError. The withdrawal file's p2pkh_address helper avoids this by spreading the index across 4 bytes; the same pattern should be used here.

🛡️ Proposed fix
-        for i in 0..=max {
-            let mut addr = [0u8; 20];
-            addr[0..2].copy_from_slice(&(i as u16).to_le_bytes());
-            recipient_addresses.insert(PlatformAddress::P2pkh(addr), 1_000_000_000);
-        }
+        for i in 0..=max {
+            let mut addr = [0u8; 20];
+            let b = (i as u32).to_le_bytes();
+            addr[0..4].copy_from_slice(&b);
+            recipient_addresses.insert(PlatformAddress::P2pkh(addr), 1_000_000_000);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/state_transition_validation.rs`
around lines 100 - 117, The test builds recipient_addresses with
PlatformAddress::P2pkh keys but encodes the loop index into only 2 bytes
(addr[0..2].copy_from_slice(&(i as u16).to_le_bytes())), which will wrap at
65_536 and cause BTreeMap deduplication; update the address encoding to use 4
bytes (e.g., addr[0..4].copy_from_slice(&(i as u32).to_le_bytes())) or reuse the
existing p2pkh_address helper so each i produces a unique 20-byte key; update
the construction in the IdentityCreditTransferToAddressesTransitionV0 test to
use the widened index encoding for recipient_addresses so the map actually grows
to max + 1 entries and triggers TransitionOverMaxOutputsError.

135-149: Use assert_matches! consistently with every other test in the module.

All other tests (and the parallel withdrawal suite) use assert_matches!(result.errors.as_slice(), [...]) for both error and success assertions. The happy-path test here uses assert! with a manual {:?} format, which is inconsistent.

♻️ Proposed refactor
-        assert!(
-            result.errors.is_empty(),
-            "expected valid result, got: {:?}",
-            result.errors
-        );
+        assert_matches!(result.errors.as_slice(), []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/state_transition_validation.rs`
around lines 135 - 149, The test should use the same assert_matches! style as
the other tests: in should_return_valid_result_for_valid_transition replace the
assert!(result.errors.is_empty(), ...) with
assert_matches!(result.errors.as_slice(), []), ensuring the module imports/uses
the assert_matches! macro and matches the empty-slice pattern for success;
locate the test function should_return_valid_result_for_valid_transition and
update its assertion to assert_matches!(result.errors.as_slice(), []) to keep
consistency with the other tests that assert errors via
result.errors.as_slice().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/state_transition_validation.rs`:
- Around line 257-273: In the function valid_withdrawal_transition() remove the
redundant inner import "use rand::SeedableRng;" — SeedableRng is already
imported at the module level, so delete that line inside the function and keep
the existing use of rand::rngs::StdRng::seed_from_u64 unchanged (no other logic
needs modification).
- Around line 571-603: Add a unit test that exercises the
WithdrawalBalanceMismatchError branch by creating a withdrawal transition via
valid_withdrawal_transition(), then set transition.output to Some((different
address than the input, input_amount)) so that input_sum == output_amount and
validate_structure(platform_version) returns the error; assert that
result.errors contains a BasicError::WithdrawalBalanceMismatchError coming from
validate_structure. Use the existing pattern from other tests
(PlatformVersion::latest(), assert_matches! on result.errors.as_slice()) and
reference transition.inputs to obtain the input_amount to assign to
transition.output.
- Around line 392-417: The test should assert the invariant that
max_address_fee_strategies < max_address_inputs so the fee-strategy check runs
after the inputs-count check; inside
should_return_invalid_result_if_fee_strategy_too_many_steps, after computing
max_fee_strategies and step_count, read max_address_inputs from
PlatformVersion::latest().dpp.state_transitions.max_address_inputs and add a
short assertion like debug_assert!(max_fee_strategies < max_address_inputs) (or
a plain assert!() in the test) and/or a clarifying comment referencing
max_address_fee_strategies and max_address_inputs to prevent future regressions
where validation ordering could make the test assert the wrong error.

In
`@packages/rs-dpp/src/state_transition/state_transitions/identity/identity_credit_transfer_to_addresses_transition/v0/state_transition_validation.rs`:
- Around line 100-117: The test builds recipient_addresses with
PlatformAddress::P2pkh keys but encodes the loop index into only 2 bytes
(addr[0..2].copy_from_slice(&(i as u16).to_le_bytes())), which will wrap at
65_536 and cause BTreeMap deduplication; update the address encoding to use 4
bytes (e.g., addr[0..4].copy_from_slice(&(i as u32).to_le_bytes())) or reuse the
existing p2pkh_address helper so each i produces a unique 20-byte key; update
the construction in the IdentityCreditTransferToAddressesTransitionV0 test to
use the widened index encoding for recipient_addresses so the map actually grows
to max + 1 entries and triggers TransitionOverMaxOutputsError.
- Around line 135-149: The test should use the same assert_matches! style as the
other tests: in should_return_valid_result_for_valid_transition replace the
assert!(result.errors.is_empty(), ...) with
assert_matches!(result.errors.as_slice(), []), ensuring the module imports/uses
the assert_matches! macro and matches the empty-slice pattern for success;
locate the test function should_return_valid_result_for_valid_transition and
update its assertion to assert_matches!(result.errors.as_slice(), []) to keep
consistency with the other tests that assert errors via
result.errors.as_slice().

@thepastaclaw thepastaclaw force-pushed the test/validation-error-paths-transitions branch from 4a849f1 to d0c6914 Compare February 20, 2026 21:39
@thepastaclaw
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 20, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/state_transition_validation.rs (3)

257-295: p2pkh_address: prefer to_le_bytes() over manual bit extraction.

The same address-generation pattern in the transfer file (line 103) uses (i as u32).to_le_bytes(), which is cleaner and less error-prone.

♻️ Suggested simplification
 fn p2pkh_address(index: usize) -> PlatformAddress {
     let mut bytes = [0u8; 20];
-    bytes[0] = (index & 0xff) as u8;
-    bytes[1] = ((index >> 8) & 0xff) as u8;
-    bytes[2] = ((index >> 16) & 0xff) as u8;
-    bytes[3] = ((index >> 24) & 0xff) as u8;
+    bytes[0..4].copy_from_slice(&(index as u32).to_le_bytes());
     PlatformAddress::P2pkh(bytes)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/state_transition_validation.rs`
around lines 257 - 295, Replace the manual bit extraction in p2pkh_address with
a little-endian byte conversion to simplify and make it consistent with the
transfer code: compute the 4-byte little-endian representation of index (e.g.,
(index as u32).to_le_bytes()) and copy those bytes into the first four slots of
the 20-byte array in p2pkh_address; ensure behavior of make_inputs_and_witnesses
(which relies on p2pkh_address and casts i to u32) remains unchanged so tests
continue to generate the same addresses.

430-448: Missing defensive ordering assertion in should_return_invalid_result_if_fee_strategy_duplicate.

The test implicitly requires max_address_fee_strategies >= 2. If that constant is ever set to 1, the FeeStrategyTooManyStepsError check fires first, and the test would pass for the wrong reason. The sibling test should_return_invalid_result_if_fee_strategy_too_many_steps (lines 403–409) already establishes the pattern of making this class of ordering invariant explicit with an assert!. Add the same guard here for consistency.

🛡️ Suggested fix
 #[test]
 fn should_return_invalid_result_if_fee_strategy_duplicate() {
     let platform_version = PlatformVersion::latest();
+    let max_fee_strategies = platform_version
+        .dpp
+        .state_transitions
+        .max_address_fee_strategies as usize;
+    assert!(
+        max_fee_strategies >= 2,
+        "test requires max_address_fee_strategies >= 2, got {}",
+        max_fee_strategies
+    );

     let mut transition = valid_withdrawal_transition();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/state_transition_validation.rs`
around lines 430 - 448, The test
should_return_invalid_result_if_fee_strategy_duplicate can spuriously pass if
max_address_fee_strategies == 1 because FeeStrategyTooManyStepsError would
trigger first; add a defensive assert to ensure max_address_fee_strategies >= 2
before building the duplicate fee strategy. Locate the test function
should_return_invalid_result_if_fee_strategy_duplicate and insert an
assert!(max_address_fee_strategies() >= 2) (or
assert!(MAX_ADDRESS_FEE_STRATEGIES >= 2) matching the constant/accessor used
elsewhere) at the top of the test so the test verifies the duplicate-step error
rather than the too-many-steps error.

586-608: WithdrawalBelowMinAmountError above-max path not covered.

The production code at lines 230–231 uses a combined condition:

if withdrawal_amount < MIN_WITHDRAWAL_AMOUNT
    || withdrawal_amount > platform_version.system_limits.max_withdrawal_amount

should_return_invalid_result_if_withdrawal_below_min exercises the < MIN_WITHDRAWAL_AMOUNT branch. The > max_withdrawal_amount branch has no test. Consider adding:

#[test]
fn should_return_invalid_result_if_withdrawal_above_max() {
    let platform_version = PlatformVersion::latest();
    let max = platform_version.system_limits.max_withdrawal_amount;

    let mut transition = valid_withdrawal_transition();
    // Use a single input just over max; no change output so withdrawal == input.
    transition.inputs = [(PlatformAddress::P2pkh([1u8; 20]), (0, max + 1))].into();
    transition.output = None;

    let result = transition.validate_structure(platform_version);

    assert_matches!(
        result.errors.as_slice(),
        [crate::consensus::ConsensusError::BasicError(
            BasicError::WithdrawalBelowMinAmountError(_)
        )]
    );
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/state_transition_validation.rs`
around lines 586 - 608, Add a new unit test mirroring
should_return_invalid_result_if_withdrawal_below_min that exercises the >
max_withdrawal_amount branch: create a PlatformVersion::latest(), read max from
platform_version.system_limits.max_withdrawal_amount, build a transition via
valid_withdrawal_transition(), set transition.inputs to a single input with
amount = max + 1 and transition.output = None, call
transition.validate_structure(platform_version) and assert the returned errors
slice contains the same
crate::consensus::ConsensusError::BasicError(BasicError::WithdrawalBelowMinAmountError(_))
variant (use the existing valid_withdrawal_transition and validate_structure
symbols to locate code).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/state_transition_validation.rs`:
- Around line 257-295: Replace the manual bit extraction in p2pkh_address with a
little-endian byte conversion to simplify and make it consistent with the
transfer code: compute the 4-byte little-endian representation of index (e.g.,
(index as u32).to_le_bytes()) and copy those bytes into the first four slots of
the 20-byte array in p2pkh_address; ensure behavior of make_inputs_and_witnesses
(which relies on p2pkh_address and casts i to u32) remains unchanged so tests
continue to generate the same addresses.
- Around line 430-448: The test
should_return_invalid_result_if_fee_strategy_duplicate can spuriously pass if
max_address_fee_strategies == 1 because FeeStrategyTooManyStepsError would
trigger first; add a defensive assert to ensure max_address_fee_strategies >= 2
before building the duplicate fee strategy. Locate the test function
should_return_invalid_result_if_fee_strategy_duplicate and insert an
assert!(max_address_fee_strategies() >= 2) (or
assert!(MAX_ADDRESS_FEE_STRATEGIES >= 2) matching the constant/accessor used
elsewhere) at the top of the test so the test verifies the duplicate-step error
rather than the too-many-steps error.
- Around line 586-608: Add a new unit test mirroring
should_return_invalid_result_if_withdrawal_below_min that exercises the >
max_withdrawal_amount branch: create a PlatformVersion::latest(), read max from
platform_version.system_limits.max_withdrawal_amount, build a transition via
valid_withdrawal_transition(), set transition.inputs to a single input with
amount = max + 1 and transition.output = None, call
transition.validate_structure(platform_version) and assert the returned errors
slice contains the same
crate::consensus::ConsensusError::BasicError(BasicError::WithdrawalBelowMinAmountError(_))
variant (use the existing valid_withdrawal_transition and validate_structure
symbols to locate code).

- Remove redundant `use rand::SeedableRng` inside valid_withdrawal_transition()
- Add WithdrawalBalanceMismatchError test (output == input_sum path)
- Assert max_address_fee_strategies < max_address_inputs invariant in fee strategy test
- Use u32 (4 bytes) for address index encoding in transfer max-recipients test
- Use assert_matches! consistently in transfer happy-path test
@thepastaclaw thepastaclaw force-pushed the test/validation-error-paths-transitions branch from fb98fbb to c00a7f9 Compare February 21, 2026 18:02
@github-actions github-actions bot added this to the v3.1.0 milestone Feb 21, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/state_transition_validation.rs (3)

430-448: Add a guard assertion consistent with the adjacent fee_strategy_too_many_steps test.

This test sets 2 fee-strategy steps, but the too-many-steps check runs before the duplicate check in the production code. If max_address_fee_strategies is ever set to 1, validation would return FeeStrategyTooManyStepsError instead of FeeStrategyDuplicateError, making the assert_matches! fail for the wrong reason.

The adjacent should_return_invalid_result_if_fee_strategy_too_many_steps test guards against exactly this kind of ordering assumption. Apply the same pattern here:

🛡️ Proposed guard
 fn should_return_invalid_result_if_fee_strategy_duplicate() {
     let platform_version = PlatformVersion::latest();
+    let max_fee_strategies = platform_version
+        .dpp
+        .state_transitions
+        .max_address_fee_strategies as usize;
+    assert!(
+        max_fee_strategies >= 2,
+        "max_address_fee_strategies ({}) must be >= 2 for this test to reach the duplicate check",
+        max_fee_strategies
+    );
 
     let mut transition = valid_withdrawal_transition();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/state_transition_validation.rs`
around lines 430 - 448, The test should include a guard assertion to ensure the
max allowed fee strategy steps is greater than the number used in the test so
the duplicate-check path runs; in the
should_return_invalid_result_if_fee_strategy_duplicate test, fetch the
PlatformVersion constant (max_address_fee_strategies) and assert it is >
transition.fee_strategy.len() (or > 2) before calling validate_structure so the
test cannot accidentally hit FeeStrategyTooManyStepsError instead of
FeeStrategyDuplicateError.

450-465: ReduceOutput variant of FeeStrategyIndexOutOfBoundsError is not covered.

The production code has two arms: DeductFromInput and ReduceOutput (lines 108–136). Only DeductFromInput(999) is tested here. The ReduceOutput(0) case with output: None (output_count = 0, so 0 >= 0 fires the same error) is an uncovered branch.

📋 Suggested additional test
#[test]
fn should_return_invalid_result_if_fee_strategy_reduce_output_index_out_of_bounds() {
    let platform_version = PlatformVersion::latest();

    let mut transition = valid_withdrawal_transition();
    // output is None (output_count = 0), so any ReduceOutput index is out of bounds
    transition.fee_strategy = vec![AddressFundsFeeStrategyStep::ReduceOutput(0)];

    let result = transition.validate_structure(platform_version);

    assert_matches!(
        result.errors.as_slice(),
        [crate::consensus::ConsensusError::BasicError(
            BasicError::FeeStrategyIndexOutOfBoundsError(_)
        )]
    );
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/state_transition_validation.rs`
around lines 450 - 465, Add a test that covers the ReduceOutput branch of
AddressFundsFeeStrategyStep by creating a valid_withdrawal_transition with no
outputs (output_count = 0), setting transition.fee_strategy =
vec![AddressFundsFeeStrategyStep::ReduceOutput(0)], calling
transition.validate_structure(PlatformVersion::latest()), and asserting the
resulting error matches BasicError::FeeStrategyIndexOutOfBoundsError; name the
test something like
should_return_invalid_result_if_fee_strategy_reduce_output_index_out_of_bounds
and mirror the existing DeductFromInput test structure to ensure the
ReduceOutput arm is exercised.

586-608: Missing coverage for the above-max_withdrawal_amount branch.

The production condition at line 230–231 fires WithdrawalBelowMinAmountError for two distinct cases: withdrawal_amount < MIN_WITHDRAWAL_AMOUNT and withdrawal_amount > max_withdrawal_amount. This test covers only the below-min path. The above-max path is an untested branch.

📋 Suggested additional test
#[test]
fn should_return_invalid_result_if_withdrawal_above_max() {
    let platform_version = PlatformVersion::latest();
    let max_withdrawal = platform_version.system_limits.max_withdrawal_amount;

    let input_amount = max_withdrawal + 1;
    let mut transition = valid_withdrawal_transition();
    transition.inputs = [(PlatformAddress::P2pkh([1u8; 20]), (0, input_amount))].into();
    transition.output = None;

    let result = transition.validate_structure(platform_version);

    assert_matches!(
        result.errors.as_slice(),
        [crate::consensus::ConsensusError::BasicError(
            BasicError::WithdrawalBelowMinAmountError(_)
        )]
    );
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/state_transition_validation.rs`
around lines 586 - 608, Add a new unit test that mirrors
should_return_invalid_result_if_withdrawal_below_min but sets an input amount
above the configured max withdrawal (use PlatformVersion::latest() and
platform_version.system_limits.max_withdrawal_amount), e.g. input_amount =
max_withdrawal + 1, set transition.inputs and transition.output = None on
valid_withdrawal_transition(), call
transition.validate_structure(platform_version), and assert the returned error
slice contains
crate::consensus::ConsensusError::BasicError(BasicError::WithdrawalBelowMinAmountError(_));
this ensures the branch in validate_structure that treats withdrawal_amount >
max_withdrawal_amount is covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@packages/rs-dpp/src/state_transition/state_transitions/address_funds/address_credit_withdrawal_transition/v0/state_transition_validation.rs`:
- Around line 430-448: The test should include a guard assertion to ensure the
max allowed fee strategy steps is greater than the number used in the test so
the duplicate-check path runs; in the
should_return_invalid_result_if_fee_strategy_duplicate test, fetch the
PlatformVersion constant (max_address_fee_strategies) and assert it is >
transition.fee_strategy.len() (or > 2) before calling validate_structure so the
test cannot accidentally hit FeeStrategyTooManyStepsError instead of
FeeStrategyDuplicateError.
- Around line 450-465: Add a test that covers the ReduceOutput branch of
AddressFundsFeeStrategyStep by creating a valid_withdrawal_transition with no
outputs (output_count = 0), setting transition.fee_strategy =
vec![AddressFundsFeeStrategyStep::ReduceOutput(0)], calling
transition.validate_structure(PlatformVersion::latest()), and asserting the
resulting error matches BasicError::FeeStrategyIndexOutOfBoundsError; name the
test something like
should_return_invalid_result_if_fee_strategy_reduce_output_index_out_of_bounds
and mirror the existing DeductFromInput test structure to ensure the
ReduceOutput arm is exercised.
- Around line 586-608: Add a new unit test that mirrors
should_return_invalid_result_if_withdrawal_below_min but sets an input amount
above the configured max withdrawal (use PlatformVersion::latest() and
platform_version.system_limits.max_withdrawal_amount), e.g. input_amount =
max_withdrawal + 1, set transition.inputs and transition.output = None on
valid_withdrawal_transition(), call
transition.validate_structure(platform_version), and assert the returned error
slice contains
crate::consensus::ConsensusError::BasicError(BasicError::WithdrawalBelowMinAmountError(_));
this ensures the branch in validate_structure that treats withdrawal_amount >
max_withdrawal_amount is covered.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants