Skip to content

Conversation

@codeflash-ai
Copy link

@codeflash-ai codeflash-ai bot commented Nov 13, 2025

📄 100% (1.00x) speedup for PartialTransaction.has_change in electrum/transaction.py

⏱️ Runtime : 217 microseconds 109 microseconds (best of 42 runs)

📝 Explanation and details

The optimization replaces the inefficient has_change() implementation with a short-circuit algorithm that delivers up to 99% speedup (217μs → 109μs).

Key Optimization:
The original has_change() called get_change_outputs() which builds a complete list of all change outputs via list comprehension, then checks len() > 0. The optimized version uses a direct for loop that returns True immediately upon finding the first change output, eliminating unnecessary work.

Why This Is Faster:

  1. Short-circuit evaluation: When a change output is found early, the optimized version stops immediately instead of processing all remaining outputs
  2. Memory efficiency: No intermediate list is created, avoiding allocation overhead
  3. Reduced function call overhead: Eliminates the extra method call to get_change_outputs()

Performance Characteristics by Test Case:

  • Best case scenarios (change output at start): Up to 3855% faster for large datasets
  • Worst case scenarios (no change outputs): Slight overhead (~8% slower) due to direct iteration vs. optimized list comprehension, but this is rare
  • Mixed scenarios: Substantial improvements when change outputs exist anywhere in the list

Impact Analysis:
This optimization is particularly beneficial for:

  • Transaction validation workflows where change detection is frequent
  • Large transactions with many outputs where early-exit provides maximum benefit
  • Bitcoin wallet operations where has_change() might be called repeatedly during transaction processing

The get_change_outputs() method remains unchanged to preserve existing behavior for callers that actually need the complete list of change outputs.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 🔘 None Found
🌀 Generated Regression Tests 92 Passed
⏪ Replay Tests 🔘 None Found
🔎 Concolic Coverage Tests 2 Passed
📊 Tests Coverage 100.0%
🌀 Generated Regression Tests and Runtime
import pytest
from electrum.transaction import PartialTransaction


# Dummy base classes for inputs/outputs, only 'is_change' is needed for outputs
class PartialTxOutput:
    def __init__(self, is_change):
        self.is_change = is_change

# -------------------------
# Unit tests for has_change
# -------------------------

# 1. Basic Test Cases

def test_no_outputs_returns_false():
    """No outputs: should return False."""
    tx = PartialTransaction()
    tx._outputs = []
    codeflash_output = tx.has_change() # 948ns -> 449ns (111% faster)

def test_one_non_change_output_returns_false():
    """One output, not change."""
    tx = PartialTransaction()
    tx._outputs = [PartialTxOutput(is_change=False)]
    codeflash_output = tx.has_change() # 1.04μs -> 510ns (104% faster)

def test_one_change_output_returns_true():
    """One output, is change."""
    tx = PartialTransaction()
    tx._outputs = [PartialTxOutput(is_change=True)]
    codeflash_output = tx.has_change() # 1.09μs -> 514ns (112% faster)

def test_multiple_outputs_one_change_returns_true():
    """Multiple outputs, one is change."""
    tx = PartialTransaction()
    tx._outputs = [
        PartialTxOutput(is_change=False),
        PartialTxOutput(is_change=True),
        PartialTxOutput(is_change=False)
    ]
    codeflash_output = tx.has_change() # 1.16μs -> 552ns (109% faster)

def test_multiple_outputs_all_non_change_returns_false():
    """Multiple outputs, none are change."""
    tx = PartialTransaction()
    tx._outputs = [
        PartialTxOutput(is_change=False),
        PartialTxOutput(is_change=False),
        PartialTxOutput(is_change=False)
    ]
    codeflash_output = tx.has_change() # 1.09μs -> 558ns (94.8% faster)

def test_multiple_outputs_all_change_returns_true():
    """Multiple outputs, all are change."""
    tx = PartialTransaction()
    tx._outputs = [
        PartialTxOutput(is_change=True),
        PartialTxOutput(is_change=True),
        PartialTxOutput(is_change=True)
    ]
    codeflash_output = tx.has_change() # 1.18μs -> 518ns (128% faster)

# 2. Edge Test Cases

def test_outputs_with_mixed_types_and_missing_is_change():
    """Outputs with/without is_change attribute (should treat missing as non-change)."""
    class OutputNoChangeAttr:
        pass

    tx = PartialTransaction()
    tx._outputs = [
        OutputNoChangeAttr(),  # no is_change attribute
        PartialTxOutput(is_change=False),
        PartialTxOutput(is_change=True)
    ]
    codeflash_output = tx.has_change()

def test_all_outputs_missing_is_change_returns_false():
    """All outputs missing is_change attribute: should return False."""
    class OutputNoChangeAttr:
        pass

    tx = PartialTransaction()
    tx._outputs = [OutputNoChangeAttr(), OutputNoChangeAttr()]
    codeflash_output = tx.has_change()

def test_output_is_change_none_treated_as_false():
    """Output with is_change=None should be treated as False."""
    tx = PartialTransaction()
    output = PartialTxOutput(is_change=None)
    tx._outputs = [output]
    codeflash_output = tx.has_change() # 1.66μs -> 727ns (128% faster)

def test_output_is_change_non_bool_true_value():
    """Output with is_change set to a truthy non-bool value should be treated as True."""
    tx = PartialTransaction()
    class Dummy:
        def __bool__(self): return True
    tx._outputs = [PartialTxOutput(is_change=Dummy())]
    codeflash_output = tx.has_change() # 1.57μs -> 1.03μs (51.7% faster)

def test_output_is_change_non_bool_false_value():
    """Output with is_change set to a falsy non-bool value should be treated as False."""
    tx = PartialTransaction()
    class Dummy:
        def __bool__(self): return False
    tx._outputs = [PartialTxOutput(is_change=Dummy())]
    codeflash_output = tx.has_change() # 1.24μs -> 759ns (63.0% faster)

def test_outputs_is_none_returns_false():
    """_outputs set to None should not raise and return False."""
    tx = PartialTransaction()
    tx._outputs = None
    # Defensive: get_change_outputs will fail if _outputs is None, so we handle this
    # But as per the provided code, _outputs is always a list, so this is an edge test
    try:
        codeflash_output = tx.has_change(); result = codeflash_output
    except Exception:
        result = False

# 3. Large Scale Test Cases

def test_large_outputs_no_change():
    """Large number of outputs, none are change."""
    tx = PartialTransaction()
    tx._outputs = [PartialTxOutput(is_change=False) for _ in range(999)]
    codeflash_output = tx.has_change() # 12.8μs -> 13.9μs (8.44% slower)

def test_large_outputs_one_change_at_start():
    """Large number of outputs, first is change."""
    tx = PartialTransaction()
    tx._outputs = [PartialTxOutput(is_change=True)] + [PartialTxOutput(is_change=False) for _ in range(998)]
    codeflash_output = tx.has_change() # 12.8μs -> 563ns (2165% faster)

def test_large_outputs_one_change_at_end():
    """Large number of outputs, last is change."""
    tx = PartialTransaction()
    tx._outputs = [PartialTxOutput(is_change=False) for _ in range(998)] + [PartialTxOutput(is_change=True)]
    codeflash_output = tx.has_change() # 12.5μs -> 14.0μs (10.3% slower)

def test_large_outputs_multiple_change():
    """Large number of outputs, several are change."""
    tx = PartialTransaction()
    tx._outputs = (
        [PartialTxOutput(is_change=False) for _ in range(500)] +
        [PartialTxOutput(is_change=True) for _ in range(10)] +
        [PartialTxOutput(is_change=False) for _ in range(489)]
    )
    codeflash_output = tx.has_change() # 12.9μs -> 7.48μs (72.6% faster)

def test_large_outputs_all_change():
    """Large number of outputs, all are change."""
    tx = PartialTransaction()
    tx._outputs = [PartialTxOutput(is_change=True) for _ in range(999)]
    codeflash_output = tx.has_change() # 21.2μs -> 537ns (3855% faster)

# Parametrized test for various small combinations
@pytest.mark.parametrize(
    "output_is_change_list,expected",
    [
        ([], False),
        ([False], False),
        ([True], True),
        ([False, False, False], False),
        ([False, True, False], True),
        ([True, True, True], True),
        ([False, False, True], True),
        ([True, False, False], True),
    ]
)
def test_various_small_combinations(output_is_change_list, expected):
    """Test various small combinations of is_change flags."""
    tx = PartialTransaction()
    tx._outputs = [PartialTxOutput(is_change=flag) for flag in output_is_change_list]
    codeflash_output = tx.has_change() # 8.40μs -> 4.25μs (97.5% faster)
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
from typing import Sequence

# imports
import pytest
from electrum.transaction import PartialTransaction


class PartialTxOutput:
    """Mock class for PartialTxOutput, simulating the 'is_change' property."""
    def __init__(self, is_change: bool):
        self.is_change = is_change

# -------------------- UNIT TESTS --------------------

# 1. Basic Test Cases

def test_no_outputs_returns_false():
    """Test with no outputs: should return False."""
    tx = PartialTransaction()
    codeflash_output = tx.has_change() # 1.47μs -> 617ns (138% faster)

def test_single_output_not_change_returns_false():
    """Test with one output, not a change output: should return False."""
    tx = PartialTransaction()
    tx._outputs.append(PartialTxOutput(is_change=False))
    codeflash_output = tx.has_change() # 1.11μs -> 547ns (103% faster)

def test_single_output_is_change_returns_true():
    """Test with one output, which is a change output: should return True."""
    tx = PartialTransaction()
    tx._outputs.append(PartialTxOutput(is_change=True))
    codeflash_output = tx.has_change() # 1.14μs -> 557ns (105% faster)

def test_multiple_outputs_none_change_returns_false():
    """Test with multiple outputs, none are change outputs: should return False."""
    tx = PartialTransaction()
    tx._outputs.extend([PartialTxOutput(False), PartialTxOutput(False), PartialTxOutput(False)])
    codeflash_output = tx.has_change() # 1.16μs -> 582ns (98.5% faster)

def test_multiple_outputs_one_change_returns_true():
    """Test with multiple outputs, one is a change output: should return True."""
    tx = PartialTransaction()
    tx._outputs.extend([PartialTxOutput(False), PartialTxOutput(True), PartialTxOutput(False)])
    codeflash_output = tx.has_change() # 1.20μs -> 587ns (105% faster)

def test_multiple_outputs_all_change_returns_true():
    """Test with multiple outputs, all are change outputs: should return True."""
    tx = PartialTransaction()
    tx._outputs.extend([PartialTxOutput(True), PartialTxOutput(True), PartialTxOutput(True)])
    codeflash_output = tx.has_change() # 1.19μs -> 505ns (136% faster)

# 2. Edge Test Cases

def test_empty_outputs_list_returns_false():
    """Edge: Outputs list is empty, should return False."""
    tx = PartialTransaction()
    tx._outputs = []
    codeflash_output = tx.has_change() # 922ns -> 473ns (94.9% faster)

def test_output_is_change_none_type_returns_false():
    """Edge: Output's is_change is None, should be treated as False."""
    class WeirdOutput:
        def __init__(self):
            self.is_change = None
    tx = PartialTransaction()
    tx._outputs.append(WeirdOutput())
    codeflash_output = tx.has_change() # 1.08μs -> 566ns (91.5% faster)

def test_output_is_change_non_boolean_returns_true():
    """Edge: Output's is_change is truthy non-bool value, should be treated as True."""
    class CustomOutput:
        def __init__(self):
            self.is_change = 1  # truthy, not strictly bool
    tx = PartialTransaction()
    tx._outputs.append(CustomOutput())
    codeflash_output = tx.has_change() # 1.13μs -> 606ns (86.5% faster)

def test_output_is_change_falsey_non_boolean_returns_false():
    """Edge: Output's is_change is falsey non-bool value, should be treated as False."""
    class CustomOutput:
        def __init__(self):
            self.is_change = 0  # falsey, not strictly bool
    tx = PartialTransaction()
    tx._outputs.append(CustomOutput())
    codeflash_output = tx.has_change() # 1.04μs -> 545ns (90.5% faster)

def test_output_missing_is_change_attribute_raises():
    """Edge: Output missing 'is_change' attribute should raise AttributeError."""
    class BadOutput:
        pass
    tx = PartialTransaction()
    tx._outputs.append(BadOutput())
    with pytest.raises(AttributeError):
        tx.has_change() # 1.91μs -> 1.30μs (46.3% faster)

def test_output_is_change_property_returns_true():
    """Edge: Output's is_change is a property that returns True."""
    class PropertyOutput:
        @property
        def is_change(self):
            return True
    tx = PartialTransaction()
    tx._outputs.append(PropertyOutput())
    codeflash_output = tx.has_change() # 1.40μs -> 723ns (93.6% faster)

def test_output_is_change_property_returns_false():
    """Edge: Output's is_change is a property that returns False."""
    class PropertyOutput:
        @property
        def is_change(self):
            return False
    tx = PartialTransaction()
    tx._outputs.append(PropertyOutput())
    codeflash_output = tx.has_change() # 1.27μs -> 644ns (97.8% faster)

def test_output_is_change_callable_returns_true():
    """Edge: Output's is_change is a callable that returns True (should not be called, should be treated as truthy)."""
    class CallableOutput:
        def is_change(self):
            return True
    tx = PartialTransaction()
    # Assign the method itself, not the result
    tx._outputs.append(type('Obj', (), {'is_change': CallableOutput.is_change})())
    # This will not be called, so it's truthy
    codeflash_output = tx.has_change() # 1.32μs -> 638ns (106% faster)

# 3. Large Scale Test Cases

def test_large_number_of_outputs_no_change_returns_false():
    """Large scale: 1000 outputs, none are change outputs."""
    tx = PartialTransaction()
    tx._outputs = [PartialTxOutput(False) for _ in range(1000)]
    codeflash_output = tx.has_change() # 13.2μs -> 14.4μs (8.48% slower)

def test_large_number_of_outputs_one_change_at_start_returns_true():
    """Large scale: 1000 outputs, first is change output."""
    tx = PartialTransaction()
    tx._outputs = [PartialTxOutput(True)] + [PartialTxOutput(False) for _ in range(999)]
    codeflash_output = tx.has_change() # 13.0μs -> 529ns (2356% faster)

def test_large_number_of_outputs_one_change_at_end_returns_true():
    """Large scale: 1000 outputs, last is change output."""
    tx = PartialTransaction()
    tx._outputs = [PartialTxOutput(False) for _ in range(999)] + [PartialTxOutput(True)]
    codeflash_output = tx.has_change() # 13.0μs -> 13.9μs (6.80% slower)

def test_large_number_of_outputs_one_change_in_middle_returns_true():
    """Large scale: 1000 outputs, change output in the middle."""
    tx = PartialTransaction()
    tx._outputs = [PartialTxOutput(False) for _ in range(500)] + [PartialTxOutput(True)] + [PartialTxOutput(False) for _ in range(499)]
    codeflash_output = tx.has_change() # 13.1μs -> 7.71μs (70.1% faster)

def test_large_number_of_outputs_all_change_returns_true():
    """Large scale: 1000 outputs, all are change outputs."""
    tx = PartialTransaction()
    tx._outputs = [PartialTxOutput(True) for _ in range(1000)]
    codeflash_output = tx.has_change() # 21.9μs -> 586ns (3636% faster)

def test_large_number_of_outputs_alternating_change_returns_true():
    """Large scale: 1000 outputs, alternating change/non-change outputs."""
    tx = PartialTransaction()
    tx._outputs = [PartialTxOutput(i % 2 == 0) for i in range(1000)]
    codeflash_output = tx.has_change() # 18.2μs -> 558ns (3154% faster)

def test_large_number_of_outputs_none_are_change_returns_false():
    """Large scale: 1000 outputs, all are not change outputs."""
    tx = PartialTransaction()
    tx._outputs = [PartialTxOutput(False) for _ in range(1000)]
    codeflash_output = tx.has_change() # 13.0μs -> 14.0μs (7.50% slower)
# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
from electrum.transaction import PartialTransaction

def test_PartialTransaction_has_change():
    PartialTransaction.has_change(PartialTransaction())
🔎 Concolic Coverage Tests and Runtime
Test File::Test Function Original ⏱️ Optimized ⏱️ Speedup
codeflash_concolic_6p7ovzz5/tmpar7e8lmc/test_concolic_coverage.py::test_PartialTransaction_has_change 931ns 452ns 106%✅

To edit these changes git checkout codeflash/optimize-PartialTransaction.has_change-mhxurdyl and push.

Codeflash Static Badge

The optimization replaces the inefficient `has_change()` implementation with a short-circuit algorithm that delivers up to **99% speedup** (217μs → 109μs).

**Key Optimization:**
The original `has_change()` called `get_change_outputs()` which builds a complete list of all change outputs via list comprehension, then checks `len() > 0`. The optimized version uses a direct `for` loop that returns `True` immediately upon finding the first change output, eliminating unnecessary work.

**Why This Is Faster:**
1. **Short-circuit evaluation**: When a change output is found early, the optimized version stops immediately instead of processing all remaining outputs
2. **Memory efficiency**: No intermediate list is created, avoiding allocation overhead
3. **Reduced function call overhead**: Eliminates the extra method call to `get_change_outputs()`

**Performance Characteristics by Test Case:**
- **Best case scenarios** (change output at start): Up to **3855% faster** for large datasets
- **Worst case scenarios** (no change outputs): Slight overhead (~8% slower) due to direct iteration vs. optimized list comprehension, but this is rare
- **Mixed scenarios**: Substantial improvements when change outputs exist anywhere in the list

**Impact Analysis:**
This optimization is particularly beneficial for:
- Transaction validation workflows where change detection is frequent
- Large transactions with many outputs where early-exit provides maximum benefit
- Bitcoin wallet operations where `has_change()` might be called repeatedly during transaction processing

The `get_change_outputs()` method remains unchanged to preserve existing behavior for callers that actually need the complete list of change outputs.
@codeflash-ai codeflash-ai bot requested a review from mashraf-222 November 13, 2025 19:59
@codeflash-ai codeflash-ai bot added ⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash labels Nov 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant