Skip to content

Conversation

@mkalinin
Copy link
Contributor

No description provided.

@mkalinin mkalinin requested a review from eth-bot as a code owner October 30, 2025 09:10
@github-actions github-actions bot added c-new Creates a brand new proposal s-draft This EIP is a Draft t-core labels Oct 30, 2025
@eth-bot
Copy link
Collaborator

eth-bot commented Oct 30, 2025

File EIPS/eip-8071.md

Requires 1 more reviewers from @g11tech, @lightclient, @SamWilsn

@eth-bot eth-bot added e-consensus Waiting on editor consensus e-review Waiting on editor to review labels Oct 30, 2025
@github-actions
Copy link

The commit 0f612a4 (as a parent of 1674f64) contains errors.
Please inspect the Run Summary for details.

@dgusakov
Copy link
Contributor

The proposal sounds good, though canceling consolidation altogether might harm its UX. I would rather add a fee that scales exponentially with the excess balance that results from the consolidation. This will add forgiveness for the small excess balances and prevent using consolidations as a withdrawal tool

@ralexstokes
Copy link
Member

@mkalinin please update discussions-to field

@vshvsh
Copy link

vshvsh commented Oct 30, 2025

My thinking is - if it's not breaking weak subjectivity parameters or other security concerns (I don't think it does), there's no strong enough reason to change it. Current situation makes sophisticated participant better off than less capitalized or skilled ones, which is bad, but the change will make some things break and require error-handling upgrades for people who do consolidations (esp costly in onchain protocols that adopt consolidations), which is also bad.

@avsetsin
Copy link

avsetsin commented Oct 30, 2025

I guess the root cause is that some part of the source validator’s balance effectively skips the normal exit queue process. And one way to tackle this directly is to treat that leftover balance (the portion that can’t fit on the target) as if it were exiting normally — by keeping it in the regular exit queue.

That can be done by increasing the withdrawable_epoch of the source validator and keeping the remaining balance on it. This will not be a fair pass through the exit queue, since the validator will not be active at that moment, but it will be a fair consumption of churn for this balance.

Smth like:

def process_pending_consolidations(state: BeaconState) -> None:
    next_epoch = Epoch(get_current_epoch(state) + 1)
    next_pending_consolidation = 0
    for pending_consolidation in state.pending_consolidations:
        source_validator = state.validators[pending_consolidation.source_index]

        if source_validator.slashed:
            next_pending_consolidation += 1
            continue
        if source_validator.withdrawable_epoch > next_epoch:
            break

        # Calculate the maximum stake that can be received by the target validator
        movable_balance_limit = max(0, MAX_EFFECTIVE_BALANCE_ELECTRA - state.balances[pending_consolidation.target_index])

        # Calculate the amount of balance that can actually be transferred
        movable_balance = min(state.balances[pending_consolidation.source_index], movable_balance_limit)

        # Transfer the movable balance to the target validator; any excess remains with the source
        decrease_balance(state, pending_consolidation.source_index, movable_balance)
        increase_balance(state, pending_consolidation.target_index, movable_balance)

        remaining_balance = state.balances[pending_consolidation.source_index]

        if remaining_balance > 0:
            # Consume the regular exit queue churn for the remaining balance
            exit_queue_epoch = compute_exit_epoch_and_update_churn(state, remaining_balance)

            # No need to add MIN_VALIDATOR_WITHDRAWABLE_DELAY since the validator has already passed that delay
            source_validator.withdrawable_epoch = exit_queue_epoch

        next_pending_consolidation += 1

    state.pending_consolidations = state.pending_consolidations[next_pending_consolidation:]

I guess this shouldn’t break any protocol invariants, increasing the withdrawable_epoch for an already exited validator is already a valid pattern (may occur when slashing a validator between exit and withdrawable epochs)

@mkalinin
Copy link
Contributor Author

mkalinin commented Oct 31, 2025

I guess the root cause is that some part of the source validator’s balance effectively skips the normal exit queue process. And one way to tackle this directly is to treat that leftover balance (the portion that can’t fit on the target) as if it were exiting normally — by keeping it in the regular exit queue.

This is interesting! The way to do it would be simply capping the balance that is moved such that there is no excess on the target, the rest will stay at the source’s balance and wait for the exit.

One potential issue: consolidation contract can be used to trigger full exits now when there is an imbalance between exit contract’s queue and the consolidation contract’s one.

@mkalinin
Copy link
Contributor Author

I guess the root cause is that some part of the source validator’s balance effectively skips the normal exit queue process. And one way to tackle this directly is to treat that leftover balance (the portion that can’t fit on the target) as if it were exiting normally — by keeping it in the regular exit queue.

That can be done by increasing the withdrawable_epoch of the source validator and keeping the remaining balance on it. This will not be a fair pass through the exit queue, since the validator will not be active at that moment, but it will be a fair consumption of churn for this balance.

Smth like:

def process_pending_consolidations(state: BeaconState) -> None:
    next_epoch = Epoch(get_current_epoch(state) + 1)
    next_pending_consolidation = 0
    for pending_consolidation in state.pending_consolidations:
        source_validator = state.validators[pending_consolidation.source_index]

        if source_validator.slashed:
            next_pending_consolidation += 1
            continue
        if source_validator.withdrawable_epoch > next_epoch:
            break

        # Calculate the maximum stake that can be received by the target validator
        movable_balance_limit = max(0, MAX_EFFECTIVE_BALANCE_ELECTRA - state.balances[pending_consolidation.target_index])

        # Calculate the amount of balance that can actually be transferred
        movable_balance = min(state.balances[pending_consolidation.source_index], movable_balance_limit)

        # Transfer the movable balance to the target validator; any excess remains with the source
        decrease_balance(state, pending_consolidation.source_index, movable_balance)
        increase_balance(state, pending_consolidation.target_index, movable_balance)

        remaining_balance = state.balances[pending_consolidation.source_index]

        if remaining_balance > 0:
            # Consume the regular exit queue churn for the remaining balance
            exit_queue_epoch = compute_exit_epoch_and_update_churn(state, remaining_balance)

            # No need to add MIN_VALIDATOR_WITHDRAWABLE_DELAY since the validator has already passed that delay
            source_validator.withdrawable_epoch = exit_queue_epoch

        next_pending_consolidation += 1

    state.pending_consolidations = state.pending_consolidations[next_pending_consolidation:]

I guess this shouldn’t break any protocol invariants, increasing the withdrawable_epoch for an already exited validator is already a valid pattern (may occur when slashing a validator between exit and withdrawable epochs)

The problem with this solution is the waste of the consolidation queue as when consolidation request is processed entire balance of the source is subtracted from the consolidation queue and then upon processing pending consolidation the remainder should also hit the exit queue. A solution to this problem would be in splitting the source balance between exit and consolidation queues upon processing consolidation request which is additional complexity and double semantics of the consolidation request which isn’t good from the design perspective.

@avsetsin
Copy link

The problem with this solution is the waste of the consolidation queue as when consolidation request is processed entire balance of the source is subtracted from the consolidation queue and then upon processing pending consolidation the remainder should also hit the exit queue.

Agree. I also noted that it might actually be important to fix the fact that exit_epoch is assigned based on the consolidation queue when the balance does not fit. Getting an earlier exit_epoch could theoretically be exploited — since it allows a validator to stop performing its duties sooner.

A solution to this problem would be in splitting the source balance between exit and consolidation queues upon processing consolidation request which is additional complexity and double semantics of the consolidation request which isn’t good from the design perspective.

Yeah, also thought about it and sketched this. I don't like this idea either, but since it's been mentioned...

def process_consolidation_request(
    state: BeaconState, consolidation_request: ConsolidationRequest
) -> None:
    # ...

    expected_target_validator_balance = min(
        get_max_effective_balance(target_validator),
        target_validator.effective_balance + get_pending_balance_to_consolidate(state, target_index)
    )

    expected_shortage_in_target_balance = get_max_effective_balance(target_validator) - expected_target_validator_balance
    expected_balance_to_consolidate = min(expected_shortage_in_target_balance, source_validator.effective_balance)
    expected_remaining_balance = source_validator.effective_balance - expected_balance_to_consolidate

    # Consume the consolidation queue churn
    source_validator.exit_epoch = compute_consolidation_epoch_and_update_churn(
        state, expected_balance_to_consolidate
    )

    if expected_remaining_balance > 0:
        # Consume the exit queue churn for the remaining balance
        exit_queue_epoch = compute_exit_epoch_and_update_churn(state, expected_remaining_balance)
        exit_consolidation_epoch = source_validator.exit_epoch

        # Use intermediate epoch between exit_consolidation_epoch and exit_queue_epoch 
        # depending on the proportion of balances in each queue
        #
        #      exit_consolidation_epoch                exit_queue_epoch
        #   ---|---------------------------------------|---->
        #                                ^
        #                                exit_epoch
        #
        #      exit_epoch - exit_consolidation_epoch      expected_remaining_balance
        #   ------------------------------------------- = --------------------------
        #   exit_queue_epoch - exit_consolidation_epoch       effective_balance
        #
        if exit_queue_epoch > exit_consolidation_epoch:
            source_validator.exit_epoch = (
                exit_consolidation_epoch
                + (exit_queue_epoch - exit_consolidation_epoch) 
                * expected_remaining_balance // effective_balance
            )

    source_validator.withdrawable_epoch = Epoch(
        source_validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY
    )
    state.pending_consolidations.append(
        PendingConsolidation(source_index=source_index, target_index=target_index)
    )

@github-actions github-actions bot removed the w-ci Waiting on CI to pass label Nov 1, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c-new Creates a brand new proposal e-consensus Waiting on editor consensus e-review Waiting on editor to review s-draft This EIP is a Draft t-core

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants