Skip to content

fix(flaky_tests): fix race condition on promotion#1087

Merged
JesseTheRobot merged 2 commits into
masterfrom
jason/flaky_tests
Dec 29, 2025
Merged

fix(flaky_tests): fix race condition on promotion#1087
JesseTheRobot merged 2 commits into
masterfrom
jason/flaky_tests

Conversation

@glottologist

@glottologist glottologist commented Dec 23, 2025

Copy link
Copy Markdown
Contributor

Describe the changes

Summary

Before:
Tests that check is_promoted after mine_block() exhibited race conditions. The test would check promotion status before the mempool service processed the BlockConfirmed message, causing intermittent failures.

After:
Added wait_until_height_confirmed() call after mining to ensure BlockConfirmed message is processed before checking promotion status. Also added atomic set_promoted_height() method to mempool for thread-safe promotion updates.

Changes

Mempool Service (crates/actors/src/mempool_service.rs)

  • Added set_promoted_height() method that atomically sets promotion height while holding write lock
  • Prevents race conditions during concurrent promotion updates

Mempool Lifecycle (crates/actors/src/mempool_service/lifecycle.rs)

  • Refactored BlockConfirmed handler to use atomic set_promoted_height() first
  • Falls back to DB path only when tx not in mempool

Test Fix (crates/chain/tests/promotion/data_promotion_basic.rs)

  • heavy_promotion_validates_ingress_proof_anchor: Get height before mining, wait for height + 1 confirmation before checking is_promoted

Related Issue(s)
Please link to the issue(s) that will be closed with this PR.

Checklist

  • Tests have been added/updated for the changes.
  • Documentation has been updated for the changes (if applicable).
  • The code follows Rust's style guidelines.

Additional Context
Add any other context about the pull request here.

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced transaction promotion reliability with improved timing handling during block confirmation.
    • Improved transaction state consistency through more robust validation mechanisms.

✏️ Tip: You can customize this high-level summary in your review settings.

@glottologist glottologist requested review from DanMacDonald, JesseTheRobot and antouhou and removed request for JesseTheRobot December 23, 2025 16:26
@glottologist glottologist marked this pull request as ready for review December 23, 2025 16:27
@JesseTheRobot

Copy link
Copy Markdown
Member

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Dec 23, 2025

Copy link
Copy Markdown
Contributor
✅ Actions performed

Full review triggered.

@coderabbitai

coderabbitai Bot commented Dec 23, 2025

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This change introduces a new set_promoted_height method on AtomicMempoolState, refactors the promotion lifecycle to use a DB-first verification approach with mempool fallback, and updates tests to verify promotion with polling-based synchronization and ingress proof persistence checks.

Changes

Cohort / File(s) Summary
New Mempool API
crates/actors/src/mempool_service.rs
Adds public async method set_promoted_height(txid, height) that acquires write lock, finds transaction header in valid_submit_ledger_tx, conditionally sets promoted_height (if None), returns cloned header with logging, and marks txid as recently valid. Returns None if transaction not found.
Promotion Flow Refactor
crates/actors/src/mempool_service/lifecycle.rs
Changes promotion logic from mempool-first to DB-first: fetches header from DB, attempts mempool promotion via new set_promoted_height call, updates DB on mempool success, or falls back to ensure promoted_height in DB and insert into mempool. Adds debug/info logging for DB hits, promotions, and fallback paths.
Test Synchronization & Verification
crates/chain/tests/promotion/data_promotion_basic.rs
Adds pre-mine wait via wait_for_ingress_proofs_no_mining to ensure ingress persistence. Replaces immediate promotion check with polling loop using get_is_promoted at 100ms intervals with configurable timeout, asserting promotion only if detected within window.

Sequence Diagram

sequenceDiagram
    actor Lifecycle
    participant DB
    participant Mempool as AtomicMempoolState
    participant Cache as recent_valid_tx Cache

    rect rgb(240, 248, 255)
    note over Lifecycle,Cache: New DB-First Promotion Flow
    end

    Lifecycle->>DB: Fetch transaction header
    alt DB has header
        DB-->>Lifecycle: Return header
        note over Lifecycle: DB hit - header found
    else DB no header
        DB-->>Lifecycle: None
        note over Lifecycle: DB miss
    end

    rect rgb(220, 240, 220)
    note over Lifecycle,Cache: Attempt Mempool Promotion
    end
    
    Lifecycle->>Mempool: set_promoted_height(txid, height)
    alt Promotion successful
        Mempool->>Mempool: Acquire write lock
        Mempool->>Mempool: Find header in valid_submit_ledger_tx
        Mempool->>Mempool: Set promoted_height (if None)
        Mempool->>Cache: Mark as recently valid
        Mempool-->>Lifecycle: Return updated header
        note over Lifecycle: Mempool promotion success
        
        rect rgb(255, 240, 220)
        note over Lifecycle,DB: Update DB with mempool result
        end
        Lifecycle->>DB: Update header with promoted_height
    else Promotion failed (txid not in mempool)
        Mempool-->>Lifecycle: Return None
        note over Lifecycle: Fallback to DB path
        Lifecycle->>DB: Ensure promoted_height set, update
        Lifecycle->>Mempool: Insert header into mempool
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing a race condition in promotion checks that was causing test flakiness.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)

✅ Copyable Unit Test edits generated.

  • Create PR with unit tests
  • Commit unit tests in branch jason/flaky_tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0970e8f and c1c9d22.

📒 Files selected for processing (3)
  • crates/actors/src/mempool_service.rs
  • crates/actors/src/mempool_service/lifecycle.rs
  • crates/chain/tests/promotion/data_promotion_basic.rs
🧰 Additional context used
🧬 Code graph analysis (2)
crates/chain/tests/promotion/data_promotion_basic.rs (1)
crates/types/src/transaction.rs (6)
  • id (528-530)
  • id (804-806)
  • id (897-897)
  • id (932-934)
  • id (1010-1012)
  • id (1101-1106)
crates/actors/src/mempool_service/lifecycle.rs (1)
crates/database/src/database.rs (12)
  • tx (76-76)
  • tx (82-82)
  • tx (93-94)
  • tx (99-99)
  • tx (111-111)
  • tx (119-120)
  • tx (129-129)
  • tx (137-138)
  • tx (152-152)
  • tx (157-157)
  • tx (174-174)
  • insert_tx_header (110-112)
🔇 Additional comments (5)
crates/actors/src/mempool_service.rs (1)

2134-2151: LGTM! Well-designed atomic method prevents race conditions.

The set_promoted_height method correctly:

  • Holds the write lock for the entire operation to prevent TOCTOU races during concurrent promotion updates
  • Implements idempotent behavior by only setting promoted_height when None (preserves existing promotions)
  • Maintains cache consistency by updating recent_valid_tx
  • Provides clear debug logging for promotion traceability

This atomic approach ensures that if multiple BlockConfirmed messages arrive concurrently for the same transaction, only the first one successfully sets the promotion height.

crates/actors/src/mempool_service/lifecycle.rs (2)

51-69: Mempool-first promotion path is correct, but consider error handling.

The logic correctly:

  • Uses the new atomic set_promoted_height to prevent races
  • Only updates DB if the transaction existed there and wasn't already promoted (line 60)
  • Distinguishes mempool promotions in logs (line 67)

However, at lines 62-64, if the DB update fails (logged as error), the code continues processing. This means the mempool reflects promotion but the DB might not. Consider whether DB update failures should:

  1. Current approach (lenient): Log error and continue - allows other txs to process, treats DB as cache
  2. Stricter approach: Return error and halt block confirmation - ensures DB/mempool consistency

The current approach may be acceptable if the DB is treated as a secondary persistence layer, but this could lead to inconsistencies on node restart (mempool state lost, DB lacks promotion).

Based on learnings, verify if DB update failures during promotion have been observed in production and whether they cause issues on node restart.


71-93: DB fallback path correctly handles mempool misses.

The fallback logic appropriately:

  • Checks for DB header existence before proceeding (line 74)
  • Sets promoted_height if not already set (line 79-81)
  • Inserts into mempool for cache consistency (lines 89-91)
  • Distinguishes DB promotions in logs (line 93)

Similar error handling consideration as the mempool path: DB update failure at lines 84-86 is logged but doesn't halt processing. This maintains consistency with the mempool-first path but may lead to state divergence if updates fail.

The bounded_insert_data_tx call at line 89-91 is best-effort (can fail if mempool is full), which is appropriate since the DB is the source of truth in this path.

crates/chain/tests/promotion/data_promotion_basic.rs (2)

487-492: Good fix: ensures ingress proof persistence before mining.

The added wait correctly addresses the race condition where mine_block() could trigger BlockConfirmed processing before ingress proofs were persisted to the database. Since the mempool's promotion logic queries the DB for proofs (as seen in get_publish_txs_and_proofs), this synchronization point is essential.

The inline comment clearly documents the dependency, which helps future maintainers understand the timing requirement.


497-507: Polling approach correctly accommodates asynchronous promotion.

The polling loop properly handles the race condition where BlockConfirmed message processing is asynchronous:

  • 100ms intervals: Balances responsiveness (fast failure detection) with system load
  • 30-second timeout: Provides generous margin for message processing under test load
  • Early break: Exits immediately on success, avoiding unnecessary waits
  • Clear assertion: Failure message explicitly indicates timeout vs. logic error

This pattern aligns with the atomic set_promoted_height API introduced in the mempool service, allowing the test to observe promotion state as soon as the mempool processes the BlockConfirmed message.


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.

@coderabbitai

coderabbitai Bot commented Dec 23, 2025

Copy link
Copy Markdown
Contributor

Note

Unit test generation is an Early Access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

@coderabbitai

coderabbitai Bot commented Dec 23, 2025

Copy link
Copy Markdown
Contributor

Here are the copyable unit test edits:

Copyable Edits

coderabbit.markdownlint-cli2.jsonc

This is a new file.

{
  "outputFormatters": [
    [
      "markdownlint-cli2-formatter-json"
    ]
  ],
  "config": {
    "default": true,
    "line-length": false,
    "no-duplicate-heading": {
      "siblings_only": true
    },
    "no-trailing-punctuation": {
      "punctuation": ".,;:"
    },
    "ol-prefix": false,
    "list-marker-space": false,
    "no-inline-html": false,
    "first-line-h1": false,
    "no-trailing-spaces": false,
    "single-h1": false,
    "blank_lines": false
  }
}

crates/types/tests/commitment_transaction_tests.rs

This is a new file.

use irys_types::{
    CommitmentTransaction, CommitmentType, CommitmentValidationError, ConsensusConfig,
    IrysAddress, IrysSignature, Signature, H256, U256,
};
use pretty_assertions::assert_eq;

mod validation_tests {
    use super::*;

    #[test]
    fn test_validate_fee_sufficient() {
        let config = ConsensusConfig::testing();
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        tx.fee = config.mempool.commitment_fee;

        assert!(tx.validate_fee(&config).is_ok());
    }

    #[test]
    fn test_validate_fee_insufficient() {
        let config = ConsensusConfig::testing();
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        tx.fee = config.mempool.commitment_fee - 1;

        let result = tx.validate_fee(&config);
        assert!(result.is_err());
        
        match result.unwrap_err() {
            CommitmentValidationError::InsufficientFee { provided, required } => {
                assert_eq!(provided, config.mempool.commitment_fee - 1);
                assert_eq!(required, config.mempool.commitment_fee);
            }
            _ => panic!("Expected InsufficientFee error"),
        }
    }

    #[test]
    fn test_validate_fee_zero_when_required() {
        let mut config = ConsensusConfig::testing();
        config.mempool.commitment_fee = 100;
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        tx.fee = 0;

        let result = tx.validate_fee(&config);
        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            CommitmentValidationError::InsufficientFee { .. }
        ));
    }

    #[test]
    fn test_validate_stake_value_correct() {
        let config = ConsensusConfig::testing();
        let tx = CommitmentTransaction::new_stake(&config, H256::zero());

        assert!(tx.validate_value(&config).is_ok());
    }

    #[test]
    fn test_validate_stake_value_incorrect() {
        let config = ConsensusConfig::testing();
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        tx.value = config.stake_value.amount + U256::from(1u64);

        let result = tx.validate_value(&config);
        assert!(result.is_err());
        
        match result.unwrap_err() {
            CommitmentValidationError::InvalidStakeValue { provided, expected } => {
                assert_eq!(provided, config.stake_value.amount + U256::from(1u64));
                assert_eq!(expected, config.stake_value.amount);
            }
            _ => panic!("Expected InvalidStakeValue error"),
        }
    }

    #[test]
    fn test_validate_unstake_value_correct() {
        let config = ConsensusConfig::testing();
        let tx = CommitmentTransaction::new_unstake(&config, H256::zero());

        assert!(tx.validate_value(&config).is_ok());
    }

    #[test]
    fn test_validate_unstake_value_incorrect() {
        let config = ConsensusConfig::testing();
        let mut tx = CommitmentTransaction::new_unstake(&config, H256::zero());
        tx.value = U256::from(999u64);

        let result = tx.validate_value(&config);
        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            CommitmentValidationError::InvalidStakeValue { .. }
        ));
    }

    #[test]
    fn test_validate_pledge_value_correct() {
        let config = ConsensusConfig::testing();
        let pledge_count = 5u64;
        let expected_value = CommitmentTransaction::calculate_pledge_value_at_count(&config, pledge_count);
        
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        tx.commitment_type = CommitmentType::Pledge {
            pledge_count_before_executing: pledge_count,
        };
        tx.value = expected_value;

        assert!(tx.validate_value(&config).is_ok());
    }

    #[test]
    fn test_validate_pledge_value_incorrect() {
        let config = ConsensusConfig::testing();
        let pledge_count = 5u64;
        let wrong_value = U256::from(12345u64);
        
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        tx.commitment_type = CommitmentType::Pledge {
            pledge_count_before_executing: pledge_count,
        };
        tx.value = wrong_value;

        let result = tx.validate_value(&config);
        assert!(result.is_err());
        
        match result.unwrap_err() {
            CommitmentValidationError::InvalidPledgeValue { provided, expected, pledge_count: count } => {
                assert_eq!(provided, wrong_value);
                assert_eq!(count, pledge_count);
                assert_ne!(expected, wrong_value);
            }
            _ => panic!("Expected InvalidPledgeValue error"),
        }
    }

    #[test]
    fn test_validate_unpledge_value_correct() {
        let config = ConsensusConfig::testing();
        let pledge_count = 5u64;
        let expected_value = CommitmentTransaction::calculate_pledge_value_at_count(&config, pledge_count - 1);
        
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        tx.commitment_type = CommitmentType::Unpledge {
            pledge_count_before_executing: pledge_count,
            partition_hash: H256::zero(),
        };
        tx.value = expected_value;

        assert!(tx.validate_value(&config).is_ok());
    }

    #[test]
    fn test_validate_unpledge_value_incorrect() {
        let config = ConsensusConfig::testing();
        let pledge_count = 5u64;
        let wrong_value = U256::from(99999u64);
        
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        tx.commitment_type = CommitmentType::Unpledge {
            pledge_count_before_executing: pledge_count,
            partition_hash: H256::zero(),
        };
        tx.value = wrong_value;

        let result = tx.validate_value(&config);
        assert!(result.is_err());
        
        match result.unwrap_err() {
            CommitmentValidationError::InvalidUnpledgeValue { provided, expected, pledge_count: count } => {
                assert_eq!(provided, wrong_value);
                assert_eq!(count, pledge_count);
                assert_ne!(expected, wrong_value);
            }
            _ => panic!("Expected InvalidUnpledgeValue error"),
        }
    }

    #[test]
    fn test_validate_unpledge_count_zero() {
        let config = ConsensusConfig::testing();
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        tx.commitment_type = CommitmentType::Unpledge {
            pledge_count_before_executing: 0,
            partition_hash: H256::zero(),
        };

        let result = tx.validate_value(&config);
        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            CommitmentValidationError::InvalidUnpledgeCountZero
        ));
    }
}

mod constructor_tests {
    use super::*;

    #[test]
    fn test_new_creates_default_transaction() {
        let config = ConsensusConfig::testing();
        let tx = CommitmentTransaction::new(&config);

        assert_eq!(tx.id, H256::zero());
        assert_eq!(tx.anchor, H256::zero());
        assert_eq!(tx.chain_id, config.chain_id);
        assert_eq!(tx.fee, 0);
        assert_eq!(tx.value, U256::zero());
        assert_eq!(tx.commitment_type, CommitmentType::default());
    }

    #[test]
    fn test_new_stake_sets_correct_values() {
        let config = ConsensusConfig::testing();
        let anchor = H256::from([1_u8; 32]);
        let tx = CommitmentTransaction::new_stake(&config, anchor);

        assert_eq!(tx.anchor, anchor);
        assert_eq!(tx.commitment_type, CommitmentType::Stake);
        assert_eq!(tx.fee, config.mempool.commitment_fee);
        assert_eq!(tx.value, config.stake_value.amount);
        assert_eq!(tx.chain_id, config.chain_id);
    }

    #[test]
    fn test_new_unstake_sets_correct_values() {
        let config = ConsensusConfig::testing();
        let anchor = H256::from([2_u8; 32]);
        let tx = CommitmentTransaction::new_unstake(&config, anchor);

        assert_eq!(tx.anchor, anchor);
        assert_eq!(tx.commitment_type, CommitmentType::Unstake);
        assert_eq!(tx.fee, config.mempool.commitment_fee);
        assert_eq!(tx.value, config.stake_value.amount);
        assert_eq!(tx.chain_id, config.chain_id);
    }

    #[tokio::test]
    async fn test_new_pledge_calculates_correct_value() {
        use irys_types::transaction::PledgeDataProvider;
        use std::collections::HashMap;

        struct TestProvider {
            counts: HashMap<IrysAddress, u64>,
        }

        #[async_trait::async_trait]
        impl PledgeDataProvider for TestProvider {
            async fn pledge_count(&self, user_address: IrysAddress) -> u64 {
                self.counts.get(&user_address).copied().unwrap_or(0)
            }
        }

        let config = ConsensusConfig::testing();
        let anchor = H256::from([3_u8; 32]);
        let signer = IrysAddress::default();
        let existing_count = 7u64;
        
        let mut counts = HashMap::new();
        counts.insert(signer, existing_count);
        let provider = TestProvider { counts };

        let tx = CommitmentTransaction::new_pledge(&config, anchor, &provider, signer).await;

        assert_eq!(tx.anchor, anchor);
        assert_eq!(tx.fee, config.mempool.commitment_fee);
        assert_eq!(tx.chain_id, config.chain_id);
        
        match tx.commitment_type {
            CommitmentType::Pledge { pledge_count_before_executing } => {
                assert_eq!(pledge_count_before_executing, existing_count);
            }
            _ => panic!("Expected Pledge commitment type"),
        }

        let expected_value = CommitmentTransaction::calculate_pledge_value_at_count(&config, existing_count);
        assert_eq!(tx.value, expected_value);
    }

    #[tokio::test]
    async fn test_new_unpledge_calculates_correct_refund() {
        use irys_types::transaction::PledgeDataProvider;
        use std::collections::HashMap;

        struct TestProvider {
            counts: HashMap<IrysAddress, u64>,
        }

        #[async_trait::async_trait]
        impl PledgeDataProvider for TestProvider {
            async fn pledge_count(&self, user_address: IrysAddress) -> u64 {
                self.counts.get(&user_address).copied().unwrap_or(0)
            }
        }

        let config = ConsensusConfig::testing();
        let anchor = H256::from([4_u8; 32]);
        let partition_hash = H256::from([5_u8; 32]);
        let signer = IrysAddress::default();
        let existing_count = 10u64;
        
        let mut counts = HashMap::new();
        counts.insert(signer, existing_count);
        let provider = TestProvider { counts };

        let tx = CommitmentTransaction::new_unpledge(&config, anchor, &provider, signer, partition_hash).await;

        assert_eq!(tx.anchor, anchor);
        assert_eq!(tx.fee, config.mempool.commitment_fee);
        assert_eq!(tx.chain_id, config.chain_id);
        
        match tx.commitment_type {
            CommitmentType::Unpledge { pledge_count_before_executing, partition_hash: ph } => {
                assert_eq!(pledge_count_before_executing, existing_count);
                assert_eq!(ph, partition_hash);
            }
            _ => panic!("Expected Unpledge commitment type"),
        }

        // Unpledge refund is the value of the last pledge (count - 1)
        let expected_value = CommitmentTransaction::calculate_pledge_value_at_count(&config, existing_count - 1);
        assert_eq!(tx.value, expected_value);
    }
}

mod cost_calculation_tests {
    use super::*;

    #[test]
    fn test_user_fee_returns_fee_field() {
        let config = ConsensusConfig::testing();
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        tx.fee = 12345;

        assert_eq!(tx.user_fee(), U256::from(12345u64));
    }

    #[test]
    fn test_commitment_value_returns_value_field() {
        let config = ConsensusConfig::testing();
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        tx.value = U256::from(99999u64);

        assert_eq!(tx.commitment_value(), U256::from(99999u64));
    }

    #[test]
    fn test_total_cost_stake_includes_value() {
        let config = ConsensusConfig::testing();
        let tx = CommitmentTransaction::new_stake(&config, H256::zero());

        let expected = U256::from(tx.fee) + tx.value;
        assert_eq!(tx.total_cost(), expected);
    }

    #[test]
    fn test_total_cost_unstake_excludes_value() {
        let config = ConsensusConfig::testing();
        let tx = CommitmentTransaction::new_unstake(&config, H256::zero());

        // Unstake refunds the stake, so total cost is just the fee
        let expected = U256::from(tx.fee);
        assert_eq!(tx.total_cost(), expected);
    }

    #[test]
    fn test_total_cost_pledge_includes_value() {
        let config = ConsensusConfig::testing();
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        tx.commitment_type = CommitmentType::Pledge {
            pledge_count_before_executing: 5,
        };
        tx.fee = 100;
        tx.value = U256::from(5000u64);

        let expected = U256::from(100u64) + U256::from(5000u64);
        assert_eq!(tx.total_cost(), expected);
    }

    #[test]
    fn test_total_cost_unpledge_excludes_value() {
        let config = ConsensusConfig::testing();
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        tx.commitment_type = CommitmentType::Unpledge {
            pledge_count_before_executing: 5,
            partition_hash: H256::zero(),
        };
        tx.fee = 100;
        tx.value = U256::from(5000u64);

        // Unpledge refunds the pledge, so total cost is just the fee
        let expected = U256::from(100u64);
        assert_eq!(tx.total_cost(), expected);
    }
}

mod pledge_value_calculation_tests {
    use super::*;
    use rust_decimal_macros::dec;

    #[test]
    fn test_calculate_pledge_value_at_count_zero() {
        let config = ConsensusConfig::testing();
        let value = CommitmentTransaction::calculate_pledge_value_at_count(&config, 0);
        
        // At count 0, should return the base pledge value
        assert_eq!(value, config.pledge_base_value.amount);
    }

    #[test]
    fn test_calculate_pledge_value_decreases_with_count() {
        let mut config = ConsensusConfig::testing();
        config.pledge_base_value = irys_types::storage_pricing::Amount::token(dec!(1000.0)).unwrap();
        config.pledge_decay = irys_types::storage_pricing::Amount::percentage(dec!(0.9)).unwrap();

        let value_0 = CommitmentTransaction::calculate_pledge_value_at_count(&config, 0);
        let value_1 = CommitmentTransaction::calculate_pledge_value_at_count(&config, 1);
        let value_5 = CommitmentTransaction::calculate_pledge_value_at_count(&config, 5);
        let value_10 = CommitmentTransaction::calculate_pledge_value_at_count(&config, 10);

        // Values should decrease as count increases
        assert!(value_0 > value_1);
        assert!(value_1 > value_5);
        assert!(value_5 > value_10);
    }

    #[test]
    fn test_calculate_pledge_value_never_zero() {
        let config = ConsensusConfig::testing();
        
        // Test with large counts
        for count in [0, 1, 10, 100, 1000, 10000] {
            let value = CommitmentTransaction::calculate_pledge_value_at_count(&config, count);
            assert!(value > U256::ZERO, "Pledge value should never be zero at count {}", count);
        }
    }
}

mod versioned_wrapper_tests {
    use super::*;
    use irys_types::VersionDiscriminant;

    #[test]
    fn test_version_discriminant() {
        let config = ConsensusConfig::testing();
        let tx = CommitmentTransaction::new_stake(&config, H256::zero());
        
        assert_eq!(tx.version(), 1);
    }

    #[test]
    fn test_default_creates_v1() {
        let tx = CommitmentTransaction::default();
        
        assert_eq!(tx.version(), 1);
        assert!(matches!(tx, CommitmentTransaction::V1(_)));
    }

    #[test]
    fn test_deref_to_v1() {
        let config = ConsensusConfig::testing();
        let tx = CommitmentTransaction::new_stake(&config, H256::zero());
        
        // Should be able to access V1 fields directly through Deref
        let _id = tx.id;
        let _anchor = tx.anchor;
        let _signer = tx.signer;
        let _fee = tx.fee;
    }

    #[test]
    fn test_deref_mut_to_v1() {
        let config = ConsensusConfig::testing();
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        
        // Should be able to mutate V1 fields directly through DerefMut
        tx.id = H256::from([99_u8; 32]);
        tx.fee = 999;
        
        assert_eq!(tx.id, H256::from([99_u8; 32]));
        assert_eq!(tx.fee, 999);
    }

    #[test]
    fn test_ord_implementation() {
        let config = ConsensusConfig::testing();
        let tx1 = CommitmentTransaction::new_stake(&config, H256::zero());
        let tx2 = CommitmentTransaction::new_pledge(
            &config,
            H256::zero(),
            &0u64, // MockPledgeProvider returning 0
            IrysAddress::default(),
        );

        // Stake should come before pledge
        assert!(tx1 < tx2.await);
    }

    #[test]
    fn test_partial_ord_implementation() {
        let config = ConsensusConfig::testing();
        let tx1 = CommitmentTransaction::new_stake(&config, H256::zero());
        let tx2 = tx1.clone();

        assert_eq!(tx1.partial_cmp(&tx2), Some(std::cmp::Ordering::Equal));
    }

    #[test]
    fn test_eq_implementation() {
        let config = ConsensusConfig::testing();
        let tx1 = CommitmentTransaction::new_stake(&config, H256::zero());
        let tx2 = tx1.clone();

        assert_eq!(tx1, tx2);
    }

    #[test]
    fn test_hash_implementation() {
        use std::collections::HashSet;
        
        let config = ConsensusConfig::testing();
        let mut tx1 = CommitmentTransaction::new_stake(&config, H256::zero());
        tx1.id = H256::from([1_u8; 32]);
        
        let mut tx2 = CommitmentTransaction::new_stake(&config, H256::zero());
        tx2.id = H256::from([2_u8; 32]);

        let mut set = HashSet::new();
        set.insert(tx1.clone());
        set.insert(tx2.clone());

        assert_eq!(set.len(), 2);
        assert!(set.contains(&tx1));
        assert!(set.contains(&tx2));
    }
}

mod encoding_decoding_tests {
    use super::*;
    use irys_types::Compact;
    use alloy_rlp::{Encodable, Decodable};

    #[test]
    fn test_compact_roundtrip() {
        let config = ConsensusConfig::testing();
        let mut tx = CommitmentTransaction::new_stake(&config, H256::from([1_u8; 32]));
        tx.id = H256::from([2_u8; 32]);

        let mut buf = Vec::new();
        tx.to_compact(&mut buf);

        let (decoded, rest) = CommitmentTransaction::from_compact(&buf, buf.len());

        assert!(rest.is_empty());
        assert_eq!(tx, decoded);
    }

    #[test]
    fn test_compact_preserves_all_fields() {
        let config = ConsensusConfig::testing();
        let mut tx = CommitmentTransaction::new_pledge(
            &config,
            H256::from([3_u8; 32]),
            &5u64,
            IrysAddress::from_slice(&[4_u8; 20]),
        );
        tx.id = H256::from([5_u8; 32]);

        let mut buf = Vec::new();
        tx.to_compact(&mut buf);

        let (decoded, _) = CommitmentTransaction::from_compact(&buf, buf.len());

        assert_eq!(tx.id, decoded.id);
        assert_eq!(tx.anchor, decoded.anchor);
        assert_eq!(tx.signer, decoded.signer);
        assert_eq!(tx.commitment_type, decoded.commitment_type);
        assert_eq!(tx.fee, decoded.fee);
        assert_eq!(tx.value, decoded.value);
    }

    #[test]
    #[should_panic(expected = "UnsupportedVersion")]
    fn test_compact_panics_on_unsupported_version() {
        let mut buf = vec![99_u8]; // Unsupported version
        buf.extend_from_slice(&[0_u8; 100]);

        let _ = CommitmentTransaction::from_compact(&buf, buf.len());
    }

    #[test]
    fn test_rlp_roundtrip() {
        let config = ConsensusConfig::testing();
        let mut tx = CommitmentTransaction::new_stake(&config, H256::from([1_u8; 32]));
        tx.id = H256::from([2_u8; 32]);

        let mut buf = Vec::new();
        tx.encode(&mut buf);

        let decoded = CommitmentTransaction::decode(&mut &buf[..]).unwrap();

        assert_eq!(tx, decoded);
    }

    #[test]
    fn test_rlp_includes_version_discriminant() {
        let config = ConsensusConfig::testing();
        let tx = CommitmentTransaction::new_stake(&config, H256::zero());

        let mut buf = Vec::new();
        tx.encode(&mut buf);

        // The encoded data should contain version information
        assert!(!buf.is_empty());
        
        // Verify it can be decoded
        let decoded = CommitmentTransaction::decode(&mut &buf[..]).unwrap();
        assert_eq!(decoded.version(), 1);
    }

    #[test]
    fn test_rlp_decode_unsupported_version() {
        // Create a buffer with an unsupported version in RLP format
        let mut buf = Vec::new();
        // This would need to be a properly formatted RLP with wrong version
        // For now, just verify decode handles errors gracefully
        let empty: &[u8] = &[];
        let result = CommitmentTransaction::decode(&mut &empty[..]);
        
        assert!(result.is_err());
    }
}

mod getter_methods_tests {
    use super::*;

    #[test]
    fn test_id_getter() {
        let config = ConsensusConfig::testing();
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        let expected_id = H256::from([42_u8; 32]);
        tx.id = expected_id;

        assert_eq!(tx.id(), expected_id);
    }

    #[test]
    fn test_signer_getter() {
        let config = ConsensusConfig::testing();
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        let expected_signer = IrysAddress::from_slice(&[99_u8; 20]);
        tx.signer = expected_signer;

        assert_eq!(tx.signer(), expected_signer);
    }

    #[test]
    fn test_signature_getter() {
        let config = ConsensusConfig::testing();
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        let expected_sig = IrysSignature::new(Signature::test_signature());
        tx.signature = expected_sig.clone();

        assert_eq!(tx.signature(), &expected_sig);
    }

    #[test]
    fn test_anchor_getter() {
        let config = ConsensusConfig::testing();
        let expected_anchor = H256::from([77_u8; 32]);
        let tx = CommitmentTransaction::new_stake(&config, expected_anchor);

        assert_eq!(tx.anchor(), expected_anchor);
    }
}

mod edge_cases_tests {
    use super::*;

    #[test]
    fn test_zero_fee_transaction() {
        let config = ConsensusConfig::testing();
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        tx.fee = 0;

        assert_eq!(tx.user_fee(), U256::ZERO);
    }

    #[test]
    fn test_maximum_fee_value() {
        let config = ConsensusConfig::testing();
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        tx.fee = u64::MAX;

        assert_eq!(tx.user_fee(), U256::from(u64::MAX));
    }

    #[test]
    fn test_zero_value_transaction() {
        let config = ConsensusConfig::testing();
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        tx.value = U256::ZERO;

        assert_eq!(tx.commitment_value(), U256::ZERO);
    }

    #[test]
    fn test_maximum_u256_value() {
        let config = ConsensusConfig::testing();
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        tx.value = U256::MAX;

        assert_eq!(tx.commitment_value(), U256::MAX);
    }

    #[test]
    fn test_all_zeros_hash() {
        let config = ConsensusConfig::testing();
        let tx = CommitmentTransaction::new_stake(&config, H256::zero());

        assert_eq!(tx.anchor, H256::zero());
    }

    #[test]
    fn test_all_ones_hash() {
        let config = ConsensusConfig::testing();
        let anchor = H256::from([0xFF_u8; 32]);
        let tx = CommitmentTransaction::new_stake(&config, anchor);

        assert_eq!(tx.anchor, H256::from([0xFF_u8; 32]));
    }

    #[test]
    fn test_clone_creates_independent_copy() {
        let config = ConsensusConfig::testing();
        let mut tx1 = CommitmentTransaction::new_stake(&config, H256::zero());
        tx1.id = H256::from([1_u8; 32]);

        let mut tx2 = tx1.clone();
        tx2.id = H256::from([2_u8; 32]);

        assert_ne!(tx1.id, tx2.id);
        assert_eq!(tx1.id, H256::from([1_u8; 32]));
        assert_eq!(tx2.id, H256::from([2_u8; 32]));
    }
}

mod ordering_comprehensive_tests {
    use super::*;
    use irys_types::CommitmentType;

    fn create_commitment(
        id_byte: u8,
        commitment_type: CommitmentType,
        fee: u64,
    ) -> CommitmentTransaction {
        let config = ConsensusConfig::testing();
        let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
        tx.id = H256::from([id_byte; 32]);
        tx.commitment_type = commitment_type;
        tx.fee = fee;
        tx
    }

    #[test]
    fn test_stake_always_before_pledge() {
        let stake_low_fee = create_commitment(1, CommitmentType::Stake, 10);
        let pledge_high_fee = create_commitment(
            2,
            CommitmentType::Pledge {
                pledge_count_before_executing: 0,
            },
            1000,
        );

        assert!(stake_low_fee < pledge_high_fee);
    }

    #[test]
    fn test_pledge_always_before_unpledge() {
        let pledge = create_commitment(
            1,
            CommitmentType::Pledge {
                pledge_count_before_executing: 5,
            },
            100,
        );
        let unpledge = create_commitment(
            2,
            CommitmentType::Unpledge {
                pledge_count_before_executing: 5,
                partition_hash: H256::zero(),
            },
            100,
        );

        assert!(pledge < unpledge);
    }

    #[test]
    fn test_unpledge_always_before_unstake() {
        let unpledge = create_commitment(
            1,
            CommitmentType::Unpledge {
                pledge_count_before_executing: 1,
                partition_hash: H256::zero(),
            },
            10,
        );
        let unstake = create_commitment(2, CommitmentType::Unstake, 1000);

        assert!(unpledge < unstake);
    }

    #[test]
    fn test_stakes_sorted_by_fee_descending() {
        let stake_100 = create_commitment(1, CommitmentType::Stake, 100);
        let stake_200 = create_commitment(2, CommitmentType::Stake, 200);
        let stake_50 = create_commitment(3, CommitmentType::Stake, 50);

        let mut stakes = vec![stake_100.clone(), stake_200.clone(), stake_50.clone()];
        stakes.sort();

        assert_eq!(stakes[0].id, stake_200.id);
        assert_eq!(stakes[1].id, stake_100.id);
        assert_eq!(stakes[2].id, stake_50.id);
    }

    #[test]
    fn test_pledges_sorted_by_count_ascending_then_fee_descending() {
        let pledge_count5_fee100 = create_commitment(
            1,
            CommitmentType::Pledge {
                pledge_count_before_executing: 5,
            },
            100,
        );
        let pledge_count5_fee200 = create_commitment(
            2,
            CommitmentType::Pledge {
                pledge_count_before_executing: 5,
            },
            200,
        );
        let pledge_count10_fee300 = create_commitment(
            3,
            CommitmentType::Pledge {
                pledge_count_before_executing: 10,
            },
            300,
        );

        let mut pledges = vec![
            pledge_count10_fee300.clone(),
            pledge_count5_fee100.clone(),
            pledge_count5_fee200.clone(),
        ];
        pledges.sort();

        // Lower count first, then within same count, higher fee first
        assert_eq!(pledges[0].id, pledge_count5_fee200.id);
        assert_eq!(pledges[1].id, pledge_count5_fee100.id);
        assert_eq!(pledges[2].id, pledge_count10_fee300.id);
    }

    #[test]
    fn test_unpledges_sorted_by_count_descending_then_fee_descending() {
        let unpledge_count5_fee100 = create_commitment(
            1,
            CommitmentType::Unpledge {
                pledge_count_before_executing: 5,
                partition_hash: H256::from([1_u8; 32]),
            },
            100,
        );
        let unpledge_count5_fee200 = create_commitment(
            2,
            CommitmentType::Unpledge {
                pledge_count_before_executing: 5,
                partition_hash: H256::from([2_u8; 32]),
            },
            200,
        );
        let unpledge_count10_fee300 = create_commitment(
            3,
            CommitmentType::Unpledge {
                pledge_count_before_executing: 10,
                partition_hash: H256::from([3_u8; 32]),
            },
            300,
        );

        let mut unpledges = vec![
            unpledge_count5_fee100.clone(),
            unpledge_count10_fee300.clone(),
            unpledge_count5_fee200.clone(),
        ];
        unpledges.sort();

        // Higher count first, then within same count, higher fee first
        assert_eq!(unpledges[0].id, unpledge_count10_fee300.id);
        assert_eq!(unpledges[1].id, unpledge_count5_fee200.id);
        assert_eq!(unpledges[2].id, unpledge_count5_fee100.id);
    }

    #[test]
    fn test_same_type_same_fee_uses_id_tiebreaker() {
        let stake_a = create_commitment(1, CommitmentType::Stake, 100);
        let stake_b = create_commitment(2, CommitmentType::Stake, 100);

        // With same fee, should sort by ID
        assert!(stake_a < stake_b);
    }

    #[test]
    fn test_full_ordering_mixed_types() {
        let stake_high = create_commitment(1, CommitmentType::Stake, 200);
        let stake_low = create_commitment(2, CommitmentType::Stake, 50);
        let pledge_2_high = create_commitment(
            3,
            CommitmentType::Pledge {
                pledge_count_before_executing: 2,
            },
            150,
        );
        let pledge_2_low = create_commitment(
            4,
            CommitmentType::Pledge {
                pledge_count_before_executing: 2,
            },
            50,
        );
        let pledge_10 = create_commitment(
            5,
            CommitmentType::Pledge {
                pledge_count_before_executing: 10,
            },
            100,
        );
        let unpledge_5 = create_commitment(
            6,
            CommitmentType::Unpledge {
                pledge_count_before_executing: 5,
                partition_hash: H256::zero(),
            },
            75,
        );
        let unpledge_2 = create_commitment(
            7,
            CommitmentType::Unpledge {
                pledge_count_before_executing: 2,
                partition_hash: H256::zero(),
            },
            75,
        );
        let unstake = create_commitment(8, CommitmentType::Unstake, 300);

        let mut all = vec![
            unstake.clone(),
            pledge_10.clone(),
            stake_low.clone(),
            unpledge_2.clone(),
            pledge_2_low.clone(),
            stake_high.clone(),
            pledge_2_high.clone(),
            unpledge_5.clone(),
        ];

        all.sort();

        // Expected order:
        // 1. Stakes (fee desc): stake_high, stake_low
        // 2. Pledges (count asc, fee desc): pledge_2_high, pledge_2_low, pledge_10
        // 3. Unpledges (count desc, fee desc): unpledge_5, unpledge_2
        // 4. Unstakes: unstake
        assert_eq!(all[0].id, stake_high.id);
        assert_eq!(all[1].id, stake_low.id);
        assert_eq!(all[2].id, pledge_2_high.id);
        assert_eq!(all[3].id, pledge_2_low.id);
        assert_eq!(all[4].id, pledge_10.id);
        assert_eq!(all[5].id, unpledge_5.id);
        assert_eq!(all[6].id, unpledge_2.id);
        assert_eq!(all[7].id, unstake.id);
    }

    #[test]
    fn test_ordering_is_transitive() {
        let a = create_commitment(1, CommitmentType::Stake, 100);
        let b = create_commitment(
            2,
            CommitmentType::Pledge {
                pledge_count_before_executing: 5,
            },
            100,
        );
        let c = create_commitment(3, CommitmentType::Unstake, 100);

        // If a < b and b < c, then a < c (transitivity)
        assert!(a < b);
        assert!(b < c);
        assert!(a < c);
    }

    #[test]
    fn test_ordering_is_antisymmetric() {
        let a = create_commitment(1, CommitmentType::Stake, 100);
        let b = create_commitment(2, CommitmentType::Stake, 50);

        // If a < b, then !(b < a)
        assert!(a < b);
        assert!(!(b < a));
    }

    #[test]
    fn test_equal_transactions_have_equal_ordering() {
        let a = create_commitment(1, CommitmentType::Stake, 100);
        let b = a.clone();

        assert_eq!(a.cmp(&b), std::cmp::Ordering::Equal);
        assert_eq!(b.cmp(&a), std::cmp::Ordering::Equal);
    }
}

#[cfg(test)]
mod proptest_tests {
    use super::*;
    use proptest::prelude::*;
    use irys_types::CommitmentType;

    proptest! {
        #[test]
        fn test_validate_fee_always_consistent(fee in 0u64..1_000_000u64) {
            let mut config = ConsensusConfig::testing();
            config.mempool.commitment_fee = 500;
            
            let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
            tx.fee = fee;

            let result = tx.validate_fee(&config);
            
            if fee >= 500 {
                prop_assert!(result.is_ok());
            } else {
                prop_assert!(result.is_err());
            }
        }

        #[test]
        fn test_user_fee_always_equals_fee_field(fee in any::<u64>()) {
            let config = ConsensusConfig::testing();
            let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
            tx.fee = fee;

            prop_assert_eq!(tx.user_fee(), U256::from(fee));
        }

        #[test]
        fn test_total_cost_never_less_than_fee(
            fee in 1u64..1_000_000u64,
            value in 0u64..1_000_000u64,
        ) {
            let config = ConsensusConfig::testing();
            let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
            tx.fee = fee;
            tx.value = U256::from(value);

            let total = tx.total_cost();
            let fee_u256 = U256::from(fee);
            
            prop_assert!(total >= fee_u256);
        }

        #[test]
        fn test_pledge_value_decreases_monotonically(count1 in 0u64..100, count2 in 0u64..100) {
            let config = ConsensusConfig::testing();
            
            let value1 = CommitmentTransaction::calculate_pledge_value_at_count(&config, count1);
            let value2 = CommitmentTransaction::calculate_pledge_value_at_count(&config, count2);

            if count1 < count2 {
                prop_assert!(value1 >= value2, "Pledge value should decrease with count");
            } else if count1 > count2 {
                prop_assert!(value1 <= value2, "Pledge value should increase with lower count");
            } else {
                prop_assert_eq!(value1, value2, "Same count should give same value");
            }
        }

        #[test]
        fn test_compact_encoding_roundtrip_preserves_all_fields(
            id_byte in any::<u8>(),
            anchor_byte in any::<u8>(),
            fee in any::<u64>(),
        ) {
            let config = ConsensusConfig::testing();
            let mut tx = CommitmentTransaction::new_stake(&config, H256::from([anchor_byte; 32]));
            tx.id = H256::from([id_byte; 32]);
            tx.fee = fee;

            let mut buf = Vec::new();
            tx.to_compact(&mut buf);
            let (decoded, _) = CommitmentTransaction::from_compact(&buf, buf.len());

            prop_assert_eq!(tx.id, decoded.id);
            prop_assert_eq!(tx.anchor, decoded.anchor);
            prop_assert_eq!(tx.fee, decoded.fee);
            prop_assert_eq!(tx.commitment_type, decoded.commitment_type);
        }

        #[test]
        fn test_ordering_is_total(fee1 in 0u64..1000, fee2 in 0u64..1000) {
            let config = ConsensusConfig::testing();
            let mut tx1 = CommitmentTransaction::new_stake(&config, H256::zero());
            let mut tx2 = CommitmentTransaction::new_stake(&config, H256::zero());
            tx1.fee = fee1;
            tx2.fee = fee2;
            tx1.id = H256::from([1_u8; 32]);
            tx2.id = H256::from([2_u8; 32]);

            // Every pair should have a defined ordering
            let ord = tx1.cmp(&tx2);
            prop_assert!(matches!(ord, std::cmp::Ordering::Less | std::cmp::Ordering::Greater | std::cmp::Ordering::Equal));
        }
    }
}

#[cfg(test)]
mod error_display_tests {
    use super::*;

    #[test]
    fn test_insufficient_fee_error_display() {
        let err = CommitmentValidationError::InsufficientFee {
            provided: 50,
            required: 100,
        };
        
        let display = format!("{}", err);
        assert!(display.contains("50"));
        assert!(display.contains("100"));
        assert!(display.contains("Insufficient fee"));
    }

    #[test]
    fn test_invalid_stake_value_error_display() {
        let err = CommitmentValidationError::InvalidStakeValue {
            provided: U256::from(1000u64),
            expected: U256::from(2000u64),
        };
        
        let display = format!("{}", err);
        assert!(display.contains("Invalid stake value"));
    }

    #[test]
    fn test_invalid_pledge_value_error_display() {
        let err = CommitmentValidationError::InvalidPledgeValue {
            provided: U256::from(500u64),
            expected: U256::from(1000u64),
            pledge_count: 5,
        };
        
        let display = format!("{}", err);
        assert!(display.contains("Invalid pledge value"));
        assert!(display.contains("5"));
    }

    #[test]
    fn test_invalid_unpledge_value_error_display() {
        let err = CommitmentValidationError::InvalidUnpledgeValue {
            provided: U256::from(300u64),
            expected: U256::from(600u64),
            pledge_count: 3,
        };
        
        let display = format!("{}", err);
        assert!(display.contains("Invalid unpledge value"));
        assert!(display.contains("3"));
    }

    #[test]
    fn test_invalid_unpledge_count_zero_error_display() {
        let err = CommitmentValidationError::InvalidUnpledgeCountZero;
        
        let display = format!("{}", err);
        assert!(display.contains("pledge_count_before_executing must be > 0"));
    }

    #[test]
    fn test_forbidden_signer_error_display() {
        let err = CommitmentValidationError::ForbiddenSigner;
        
        let display = format!("{}", err);
        assert!(display.contains("not allowed"));
    }
}

#[cfg(test)]
mod async_constructor_tests {
    use super::*;
    use irys_types::transaction::PledgeDataProvider;

    struct DynamicProvider {
        base_count: u64,
    }

    #[async_trait::async_trait]
    impl PledgeDataProvider for DynamicProvider {
        async fn pledge_count(&self, _user_address: IrysAddress) -> u64 {
            self.base_count
        }
    }

    #[tokio::test]
    async fn test_new_pledge_with_zero_existing_pledges() {
        let config = ConsensusConfig::testing();
        let provider = DynamicProvider { base_count: 0 };
        let tx = CommitmentTransaction::new_pledge(
            &config,
            H256::zero(),
            &provider,
            IrysAddress::default(),
        )
        .await;

        match tx.commitment_type {
            CommitmentType::Pledge {
                pledge_count_before_executing,
            } => {
                assert_eq!(pledge_count_before_executing, 0);
            }
            _ => panic!("Expected Pledge type"),
        }

        // Value should be the base pledge value (no decay for first pledge)
        let expected = CommitmentTransaction::calculate_pledge_value_at_count(&config, 0);
        assert_eq!(tx.value, expected);
    }

    #[tokio::test]
    async fn test_new_pledge_with_many_existing_pledges() {
        let config = ConsensusConfig::testing();
        let provider = DynamicProvider { base_count: 100 };
        let tx = CommitmentTransaction::new_pledge(
            &config,
            H256::zero(),
            &provider,
            IrysAddress::default(),
        )
        .await;

        match tx.commitment_type {
            CommitmentType::Pledge {
                pledge_count_before_executing,
            } => {
                assert_eq!(pledge_count_before_executing, 100);
            }
            _ => panic!("Expected Pledge type"),
        }

        let expected = CommitmentTransaction::calculate_pledge_value_at_count(&config, 100);
        assert_eq!(tx.value, expected);
    }

    #[tokio::test]
    async fn test_new_unpledge_with_single_existing_pledge() {
        let config = ConsensusConfig::testing();
        let provider = DynamicProvider { base_count: 1 };
        let partition_hash = H256::from([42_u8; 32]);
        
        let tx = CommitmentTransaction::new_unpledge(
            &config,
            H256::zero(),
            &provider,
            IrysAddress::default(),
            partition_hash,
        )
        .await;

        match tx.commitment_type {
            CommitmentType::Unpledge {
                pledge_count_before_executing,
                partition_hash: ph,
            } => {
                assert_eq!(pledge_count_before_executing, 1);
                assert_eq!(ph, partition_hash);
            }
            _ => panic!("Expected Unpledge type"),
        }

        // Refund should be for the value at count 0 (the first pledge)
        let expected = CommitmentTransaction::calculate_pledge_value_at_count(&config, 0);
        assert_eq!(tx.value, expected);
    }

    #[tokio::test]
    async fn test_multiple_pledge_providers_with_different_addresses() {
        use std::collections::HashMap;

        struct MultiAddressProvider {
            counts: HashMap<IrysAddress, u64>,
        }

        #[async_trait::async_trait]
        impl PledgeDataProvider for MultiAddressProvider {
            async fn pledge_count(&self, user_address: IrysAddress) -> u64 {
                self.counts.get(&user_address).copied().unwrap_or(0)
            }
        }

        let config = ConsensusConfig::testing();
        let addr1 = IrysAddress::from_slice(&[1_u8; 20]);
        let addr2 = IrysAddress::from_slice(&[2_u8; 20]);

        let mut counts = HashMap::new();
        counts.insert(addr1, 10);
        counts.insert(addr2, 20);
        let provider = MultiAddressProvider { counts };

        let tx1 = CommitmentTransaction::new_pledge(&config, H256::zero(), &provider, addr1).await;
        let tx2 = CommitmentTransaction::new_pledge(&config, H256::zero(), &provider, addr2).await;

        // Different addresses should have different pledge counts
        match tx1.commitment_type {
            CommitmentType::Pledge {
                pledge_count_before_executing,
            } => assert_eq!(pledge_count_before_executing, 10),
            _ => panic!("Expected Pledge type"),
        }

        match tx2.commitment_type {
            CommitmentType::Pledge {
                pledge_count_before_executing,
            } => assert_eq!(pledge_count_before_executing, 20),
            _ => panic!("Expected Pledge type"),
        }
    }
}

#[cfg(test)]
mod backwards_compatibility_tests {
    use super::*;
    use irys_types::{Compact, VersionDiscriminant};

    #[test]
    fn test_v1_is_only_supported_version() {
        let config = ConsensusConfig::testing();
        let tx = CommitmentTransaction::new_stake(&config, H256::zero());
        
        assert_eq!(tx.version(), 1);
        assert!(matches!(tx, CommitmentTransaction::V1(_)));
    }

    #[test]
    fn test_default_transaction_is_v1() {
        let tx = CommitmentTransaction::default();
        assert_eq!(tx.version(), 1);
    }

    #[test]
    fn test_all_constructors_produce_v1() {
        let config = ConsensusConfig::testing();
        
        let new_tx = CommitmentTransaction::new(&config);
        assert_eq!(new_tx.version(), 1);
        
        let stake_tx = CommitmentTransaction::new_stake(&config, H256::zero());
        assert_eq!(stake_tx.version(), 1);
        
        let unstake_tx = CommitmentTransaction::new_unstake(&config, H256::zero());
        assert_eq!(unstake_tx.version(), 1);
    }

    #[test]
    fn test_compact_encoding_starts_with_version_discriminant() {
        let config = ConsensusConfig::testing();
        let tx = CommitmentTransaction::new_stake(&config, H256::zero());
        
        let mut buf = Vec::new();
        tx.to_compact(&mut buf);
        
        assert!(!buf.is_empty());
        assert_eq!(buf[0], 1, "First byte should be version discriminant 1");
    }

    #[test]
    fn test_rlp_encoding_includes_version() {
        use alloy_rlp::Encodable;
        
        let config = ConsensusConfig::testing();
        let tx = CommitmentTransaction::new_stake(&config, H256::zero());
        
        let mut buf = Vec::new();
        tx.encode(&mut buf);
        
        assert!(!buf.is_empty());
        // RLP encoding should include version information
    }

    #[test]
    fn test_json_serialization_includes_version() {
        let config = ConsensusConfig::testing();
        let tx = CommitmentTransaction::new_stake(&config, H256::zero());
        
        let json = serde_json::to_string(&tx).unwrap();
        assert!(json.contains("\"version\""));
    }
}

#[cfg(test)]
mod stress_tests {
    use super::*;

    #[test]
    fn test_sort_large_mixed_commitment_list() {
        use irys_types::CommitmentType;
        
        let config = ConsensusConfig::testing();
        let mut txs = Vec::new();

        // Create 1000 mixed transactions
        for i in 0..1000 {
            let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
            tx.id = H256::from([(i % 256) as u8; 32]);
            tx.fee = (i * 7) % 500; // Varied fees
            
            tx.commitment_type = match i % 4 {
                0 => CommitmentType::Stake,
                1 => CommitmentType::Pledge {
                    pledge_count_before_executing: (i % 50) as u64,
                },
                2 => CommitmentType::Unpledge {
                    pledge_count_before_executing: (i % 50) as u64,
                    partition_hash: H256::from([(i % 256) as u8; 32]),
                },
                _ => CommitmentType::Unstake,
            };
            
            txs.push(tx);
        }

        // Sort should complete without panic
        txs.sort();

        // Verify ordering constraints
        let mut last_type_priority = 0u8;
        for tx in &txs {
            let current_priority = match tx.commitment_type {
                CommitmentType::Stake => 0,
                CommitmentType::Pledge { .. } => 1,
                CommitmentType::Unpledge { .. } => 2,
                CommitmentType::Unstake => 3,
            };
            assert!(
                current_priority >= last_type_priority,
                "Transactions should be grouped by type"
            );
            last_type_priority = current_priority;
        }
    }

    #[test]
    fn test_encode_decode_many_transactions() {
        use irys_types::Compact;
        
        let config = ConsensusConfig::testing();
        let mut txs = Vec::new();

        for i in 0..100 {
            let mut tx = CommitmentTransaction::new_stake(&config, H256::from([i as u8; 32]));
            tx.id = H256::from([i as u8; 32]);
            tx.fee = i as u64 * 10;
            txs.push(tx);
        }

        // Encode and decode all transactions
        for tx in &txs {
            let mut buf = Vec::new();
            tx.to_compact(&mut buf);
            let (decoded, rest) = CommitmentTransaction::from_compact(&buf, buf.len());
            assert!(rest.is_empty());
            assert_eq!(tx, &decoded);
        }
    }
}

crates/types/tests/transaction_signing_versioned_tests.rs

@@ -114,3 +114,186 @@
         .signature
         .validate_signature(sig_hash, signer.address()));
 }
+
+#[test]
+fn commitment_tx_signature_validation_after_signing() {
+    use irys_types::irys::IrysSigner;
+    use irys_types::transaction::IrysTransactionCommon;
+    
+    let config = ConsensusConfig::testing();
+    let signer = IrysSigner::random_signer(&config);
+    let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
+
+    // Sign the transaction
+    signer.sign_commitment(&mut tx).expect("signing should succeed");
+
+    // Verify the signature is valid
+    assert!(tx.is_signature_valid(), "Signature should be valid after signing");
+    
+    // Verify ID is set correctly
+    assert_ne!(tx.id, H256::zero(), "ID should be set after signing");
+    
+    // Verify signer is set correctly
+    assert_eq!(tx.signer, signer.address(), "Signer should match");
+}
+
+#[test]
+fn commitment_tx_signature_invalidated_by_field_changes() {
+    use irys_types::irys::IrysSigner;
+    use irys_types::transaction::IrysTransactionCommon;
+    
+    let config = ConsensusConfig::testing();
+    let signer = IrysSigner::random_signer(&config);
+    let mut tx = CommitmentTransaction::new_stake(&config, H256::zero());
+
+    signer.sign_commitment(&mut tx).expect("signing should succeed");
+    assert!(tx.is_signature_valid());
+
+    // Changing the anchor should invalidate the signature
+    tx.anchor = H256::from([99_u8; 32]);
+    assert!(!tx.is_signature_valid(), "Signature should be invalid after modifying anchor");
+}
+
+#[test]
+fn commitment_tx_different_types_have_different_signatures() {
+    use irys_types::irys::IrysSigner;
+    use irys_types::transaction::IrysTransactionCommon;
+    use irys_types::CommitmentType;
+    
+    let config = ConsensusConfig::testing();
+    let signer = IrysSigner::random_signer(&config);
+    let anchor = H256::from([1_u8; 32]);
+
+    let mut stake_tx = CommitmentTransaction::new_stake(&config, anchor);
+    signer.sign_commitment(&mut stake_tx).expect("signing should succeed");
+
+    let mut unstake_tx = CommitmentTransaction::new_unstake(&config, anchor);
+    signer.sign_commitment(&mut unstake_tx).expect("signing should succeed");
+
+    // Same signer, same anchor, but different commitment types should have different signatures
+    assert_ne!(stake_tx.signature, unstake_tx.signature);
+    assert_ne!(stake_tx.id, unstake_tx.id);
+}
+
+#[test]
+fn commitment_tx_pledge_signature_includes_count() {
+    use irys_types::irys::IrysSigner;
+    use irys_types::transaction::IrysTransactionCommon;
+    use irys_types::CommitmentType;
+    
+    let config = ConsensusConfig::testing();
+    let signer = IrysSigner::random_signer(&config);
+    let anchor = H256::from([1_u8; 32]);
+
+    let mut pledge_tx1 = CommitmentTransaction::new_stake(&config, anchor);
+    pledge_tx1.commitment_type = CommitmentType::Pledge {
+        pledge_count_before_executing: 5,
+    };
+    signer.sign_commitment(&mut pledge_tx1).expect("signing should succeed");
+
+    let mut pledge_tx2 = CommitmentTransaction::new_stake(&config, anchor);
+    pledge_tx2.commitment_type = CommitmentType::Pledge {
+        pledge_count_before_executing: 10,
+    };
+    signer.sign_commitment(&mut pledge_tx2).expect("signing should succeed");
+
+    // Different pledge counts should produce different signatures
+    assert_ne!(pledge_tx1.signature, pledge_tx2.signature);
+    assert_ne!(pledge_tx1.id, pledge_tx2.id);
+}
+
+#[test]
+fn commitment_tx_unpledge_signature_includes_partition_hash() {
+    use irys_types::irys::IrysSigner;
+    use irys_types::transaction::IrysTransactionCommon;
+    use irys_types::CommitmentType;
+    
+    let config = ConsensusConfig::testing();
+    let signer = IrysSigner::random_signer(&config);
+    let anchor = H256::from([1_u8; 32]);
+
+    let mut unpledge_tx1 = CommitmentTransaction::new_stake(&config, anchor);
+    unpledge_tx1.commitment_type = CommitmentType::Unpledge {
+        pledge_count_before_executing: 5,
+        partition_hash: H256::from([1_u8; 32]),
+    };
+    signer.sign_commitment(&mut unpledge_tx1).expect("signing should succeed");
+
+    let mut unpledge_tx2 = CommitmentTransaction::new_stake(&config, anchor);
+    unpledge_tx2.commitment_type = CommitmentType::Unpledge {
+        pledge_count_before_executing: 5,
+        partition_hash: H256::from([2_u8; 32]),
+    };
+    signer.sign_commitment(&mut unpledge_tx2).expect("signing should succeed");
+
+    // Different partition hashes should produce different signatures
+    assert_ne!(unpledge_tx1.signature, unpledge_tx2.signature);
+    assert_ne!(unpledge_tx1.id, unpledge_tx2.id);
+}
+
+#[test]
+fn commitment_tx_fee_affects_signature() {
+    use irys_types::irys::IrysSigner;
+    use irys_types::transaction::IrysTransactionCommon;
+    
+    let config = ConsensusConfig::testing();
+    let signer = IrysSigner::random_signer(&config);
+    let anchor = H256::from([1_u8; 32]);
+
+    let mut tx1 = CommitmentTransaction::new_stake(&config, anchor);
+    tx1.fee = 100;
+    signer.sign_commitment(&mut tx1).expect("signing should succeed");
+
+    let mut tx2 = CommitmentTransaction::new_stake(&config, anchor);
+    tx2.fee = 200;
+    signer.sign_commitment(&mut tx2).expect("signing should succeed");
+
+    // Different fees should produce different signatures
+    assert_ne!(tx1.signature, tx2.signature);
+    assert_ne!(tx1.id, tx2.id);
+}
+
+#[test]
+fn commitment_tx_value_affects_signature() {
+    use irys_types::irys::IrysSigner;
+    use irys_types::transaction::IrysTransactionCommon;
+    use irys_types::U256;
+    
+    let config = ConsensusConfig::testing();
+    let signer = IrysSigner::random_signer(&config);
+    let anchor = H256::from([1_u8; 32]);
+
+    let mut tx1 = CommitmentTransaction::new_stake(&config, anchor);
+    tx1.value = U256::from(1000u64);
+    signer.sign_commitment(&mut tx1).expect("signing should succeed");
+
+    let mut tx2 = CommitmentTransaction::new_stake(&config, anchor);
+    tx2.value = U256::from(2000u64);
+    signer.sign_commitment(&mut tx2).expect("signing should succeed");
+
+    // Different values should produce different signatures
+    assert_ne!(tx1.signature, tx2.signature);
+    assert_ne!(tx1.id, tx2.id);
+}
+
+#[test]
+fn commitment_tx_chain_id_affects_signature() {
+    use irys_types::irys::IrysSigner;
+    use irys_types::transaction::IrysTransactionCommon;
+    
+    let config = ConsensusConfig::testing();
+    let signer = IrysSigner::random_signer(&config);
+    let anchor = H256::from([1_u8; 32]);
+
+    let mut tx1 = CommitmentTransaction::new_stake(&config, anchor);
+    tx1.chain_id = 1;
+    signer.sign_commitment(&mut tx1).expect("signing should succeed");
+
+    let mut tx2 = CommitmentTransaction::new_stake(&config, anchor);
+    tx2.chain_id = 2;
+    signer.sign_commitment(&mut tx2).expect("signing should succeed");
+
+    // Different chain IDs should produce different signatures (replay protection)
+    assert_ne!(tx1.signature, tx2.signature);
+    assert_ne!(tx1.id, tx2.id);
+}

crates/types/tests/versioned_compact_roundtrip_tests.rs

@@ -154,3 +154,97 @@
     // This should panic with UnsupportedVersion error
     let _ = CommitmentTransaction::from_compact(&buf, buf.len());
 }
+
+#[test]
+fn test_commitment_transaction_compact_with_pledge() {
+    use irys_types::CommitmentType;
+    
+    let config = ConsensusConfig::testing();
+    let mut versioned = CommitmentTransaction::new_stake(&config, H256::random());
+    versioned.commitment_type = CommitmentType::Pledge {
+        pledge_count_before_executing: 7,
+    };
+    versioned.id = H256::random();
+
+    let mut buf = Vec::new();
+    versioned.to_compact(&mut buf);
+
+    assert!(!buf.is_empty(), "buffer should contain encoded data");
+    assert_eq!(buf[0], 1, "first byte should be the discriminant");
+
+    let (decoded_versioned, rest) = CommitmentTransaction::from_compact(&buf, buf.len());
+
+    assert!(rest.is_empty(), "entire buffer should be consumed");
+    assert_eq!(decoded_versioned, versioned);
+    assert_eq!(decoded_versioned.version(), 1);
+    
+    // Verify pledge-specific fields
+    match decoded_versioned.commitment_type {
+        CommitmentType::Pledge { pledge_count_before_executing } => {
+            assert_eq!(pledge_count_before_executing, 7);
+        }
+        _ => panic!("Expected Pledge commitment type"),
+    }
+}
+
+#[test]
+fn test_commitment_transaction_compact_with_unpledge() {
+    use irys_types::CommitmentType;
+    
+    let config = ConsensusConfig::testing();
+    let partition_hash = H256::from([42_u8; 32]);
+    let mut versioned = CommitmentTransaction::new_stake(&config, H256::random());
+    versioned.commitment_type = CommitmentType::Unpledge {
+        pledge_count_before_executing: 10,
+        partition_hash,
+    };
+    versioned.id = H256::random();
+
+    let mut buf = Vec::new();
+    versioned.to_compact(&mut buf);
+
+    let (decoded_versioned, rest) = CommitmentTransaction::from_compact(&buf, buf.len());
+
+    assert!(rest.is_empty());
+    assert_eq!(decoded_versioned, versioned);
+    
+    // Verify unpledge-specific fields
+    match decoded_versioned.commitment_type {
+        CommitmentType::Unpledge { pledge_count_before_executing, partition_hash: ph } => {
+            assert_eq!(pledge_count_before_executing, 10);
+            assert_eq!(ph, partition_hash);
+        }
+        _ => panic!("Expected Unpledge commitment type"),
+    }
+}
+
+#[test]
+fn test_commitment_transaction_compact_with_all_commitment_types() {
+    use irys_types::CommitmentType;
+    
+    let config = ConsensusConfig::testing();
+    let test_cases = vec![
+        ("Stake", CommitmentType::Stake),
+        ("Unstake", CommitmentType::Unstake),
+        ("Pledge", CommitmentType::Pledge { pledge_count_before_executing: 5 }),
+        ("Unpledge", CommitmentType::Unpledge {
+            pledge_count_before_executing: 3,
+            partition_hash: H256::from([99_u8; 32]),
+        }),
+    ];
+
+    for (name, commitment_type) in test_cases {
+        let mut tx = CommitmentTransaction::new_stake(&config, H256::random());
+        tx.commitment_type = commitment_type.clone();
+        tx.id = H256::random();
+
+        let mut buf = Vec::new();
+        tx.to_compact(&mut buf);
+
+        let (decoded, rest) = CommitmentTransaction::from_compact(&buf, buf.len());
+
+        assert!(rest.is_empty(), "{}: entire buffer should be consumed", name);
+        assert_eq!(decoded, tx, "{}: decoded should match original", name);
+        assert_eq!(decoded.commitment_type, commitment_type, "{}: commitment type should match", name);
+    }
+}

crates/types/tests/versioned_json_serde_tests.rs

@@ -102,3 +102,89 @@
         "Version should be 1"
     );
 }
+
+#[test]
+fn test_commitment_transaction_pledge_json_roundtrip() {
+    use irys_types::CommitmentType;
+    
+    let config = ConsensusConfig::testing();
+    let mut commitment_tx = CommitmentTransaction::new_stake(&config, H256::zero());
+    commitment_tx.commitment_type = CommitmentType::Pledge {
+        pledge_count_before_executing: 5,
+    };
+    commitment_tx.id = H256::from([3_u8; 32]);
+
+    let json = serde_json::to_string(&commitment_tx).unwrap();
+    let deserialized: CommitmentTransaction = serde_json::from_str(&json).unwrap();
+
+    assert_eq!(commitment_tx.id, deserialized.id);
+    assert_eq!(commitment_tx.commitment_type, deserialized.commitment_type);
+    assert_eq!(commitment_tx.version(), deserialized.version());
+}
+
+#[test]
+fn test_commitment_transaction_unpledge_json_roundtrip() {
+    use irys_types::CommitmentType;
+    
+    let config = ConsensusConfig::testing();
+    let mut commitment_tx = CommitmentTransaction::new_stake(&config, H256::zero());
+    commitment_tx.commitment_type = CommitmentType::Unpledge {
+        pledge_count_before_executing: 10,
+        partition_hash: H256::from([42_u8; 32]),
+    };
+    commitment_tx.id = H256::from([4_u8; 32]);
+
+    let json = serde_json::to_string(&commitment_tx).unwrap();
+    let deserialized: CommitmentTransaction = serde_json::from_str(&json).unwrap();
+
+    assert_eq!(commitment_tx.id, deserialized.id);
+    assert_eq!(commitment_tx.commitment_type, deserialized.commitment_type);
+    
+    // Verify partition_hash is preserved
+    if let CommitmentType::Unpledge { partition_hash, .. } = deserialized.commitment_type {
+        assert_eq!(partition_hash, H256::from([42_u8; 32]));
+    } else {
+        panic!("Expected Unpledge commitment type");
+    }
+}
+
+#[test]
+fn test_commitment_transaction_unstake_json_roundtrip() {
+    use irys_types::CommitmentType;
+    
+    let config = ConsensusConfig::testing();
+    let mut commitment_tx = CommitmentTransaction::new_unstake(&config, H256::from([5_u8; 32]));
+    commitment_tx.id = H256::from([6_u8; 32]);
+
+    let json = serde_json::to_string(&commitment_tx).unwrap();
+    let deserialized: CommitmentTransaction = serde_json::from_str(&json).unwrap();
+
+    assert_eq!(commitment_tx.id, deserialized.id);
+    assert_eq!(commitment_tx.commitment_type, CommitmentType::Unstake);
+    assert_eq!(deserialized.commitment_type, CommitmentType::Unstake);
+}
+
+#[test]
+fn test_commitment_transaction_preserves_all_fields_json() {
+    use irys_types::{CommitmentType, IrysAddress, IrysSignature, Signature, U256};
+    
+    let config = ConsensusConfig::testing();
+    let mut commitment_tx = CommitmentTransaction::new_stake(&config, H256::from([7_u8; 32]));
+    commitment_tx.id = H256::from([8_u8; 32]);
+    commitment_tx.signer = IrysAddress::from_slice(&[9_u8; 20]);
+    commitment_tx.fee = 12345;
+    commitment_tx.value = U256::from(67890u64);
+    commitment_tx.chain_id = 999;
+    commitment_tx.signature = IrysSignature::new(Signature::test_signature());
+
+    let json = serde_json::to_string(&commitment_tx).unwrap();
+    let deserialized: CommitmentTransaction = serde_json::from_str(&json).unwrap();
+
+    assert_eq!(commitment_tx.id, deserialized.id);
+    assert_eq!(commitment_tx.anchor, deserialized.anchor);
+    assert_eq!(commitment_tx.signer, deserialized.signer);
+    assert_eq!(commitment_tx.fee, deserialized.fee);
+    assert_eq!(commitment_tx.value, deserialized.value);
+    assert_eq!(commitment_tx.chain_id, deserialized.chain_id);
+    assert_eq!(commitment_tx.commitment_type, deserialized.commitment_type);
+}

docs/testing/commitment_transaction_tests.md

This is a new file.

# CommitmentTransaction Test Suite Documentation

## Overview
This document describes the comprehensive test suite created for the `CommitmentTransaction` and `CommitmentTransactionV1` types that were refactored from a separate file into `crates/types/src/transaction.rs`.

## Test Statistics
- **Total Tests Added**: 101
- **New Test File**: `crates/types/tests/commitment_transaction_tests.rs` (1,489 lines, 86 tests)
- **Enhanced Files**: 3 existing test files with 15 additional tests
- **Test Modules**: 14 focused test modules covering different aspects

## Running the Tests

\`\`\`bash
# Run all tests in the types crate
cd crates/types && cargo test

# Run specific test modules
cargo test validation_tests
cargo test ordering_comprehensive_tests
cargo test proptest_tests

# Run with verbose output
cargo test -- --nocapture

# Run only commitment transaction tests
cargo test commitment_transaction
\`\`\`

## Test Coverage Summary

### Core Business Logic (34 tests)
- Validation Tests (13) - Fee and value validation
- Constructor Tests (6) - Factory methods
- Cost Calculation Tests (6) - Fees and costs
- Pledge Value Calculation Tests (3) - Decay formula
- Ordering Tests (12) - Transaction prioritization

### Data Integrity (28 tests)
- Versioned Wrapper Tests (9) - Version handling
- Encoding/Decoding Tests (8) - Serialization
- Getter Methods Tests (4) - Public interface
- Signature Tests (7) - Cryptographic integrity

### Quality Assurance (39 tests)
- Property-Based Tests (7) - Invariants
- Error Display Tests (6) - Error messages
- Async Constructor Tests (5) - Async operations
- Edge Cases Tests (8) - Boundary values
- Backwards Compatibility Tests (6) - Versioning
- Stress Tests (2) - Performance
- Integration Tests (5) - Cross-cutting

## Key Features Tested

✅ All commitment types (Stake, Unstake, Pledge, Unpledge)
✅ Fee validation and cost calculations
✅ Pledge value decay formula
✅ Transaction ordering/prioritization
✅ JSON, Compact, and RLP serialization
✅ Signature validation and tampering detection
✅ Async constructor integration
✅ Version compatibility

## Maintenance Guidelines

When modifying `CommitmentTransaction`:
1. Update validation tests if validation logic changes
2. Update ordering tests if prioritization changes
3. Update encoding tests if serialization changes
4. Ensure all property tests still pass

markdownlint-cli2-results.json

This is a new file.

[]

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