Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A possible change to how validator balances are stored #685

Closed
vbuterin opened this issue Feb 25, 2019 · 6 comments
Closed

A possible change to how validator balances are stored #685

vbuterin opened this issue Feb 25, 2019 · 6 comments
Labels
general:enhancement New feature or request

Comments

@vbuterin
Copy link
Contributor

vbuterin commented Feb 25, 2019

Add to the validator registry a value rounded_balance, and reduce the uint64 in validator_balances to unit32, renaming the array validator_fractional_balances.

Redefine validator_balances[i] with the following function:

def get_balance(state: BeaconState, index: int) -> int:
    return (
        state.validator_registry[index].rounded_balance * 10**9 + 
        state.validator_fractional_balances[index]
    )

Replace validator_balances[i] = x (or += x) with the following function:

def set_balance(state: BeaconState, index: int, new_balance: int):
    validator = state.validator_registry[index]
    if (
        validator.rounded_balance * 10**9 > new_balance or
        validator.rounded_balance * 10**9 + (15 * 10**8) < new_balance
    ):
        validator.rounded_balance = new_balance // 10**9
    state.validator_fractional_balances[index] = (
        new_balance - validator.rounded_balance * 10**9
    )

Motivation

The goal is to split the balance into two components, one an integer number of ETH and the other a fractional part. This accomplishes three functions:

  • Reduces the per-epoch re-hashing cost of validator_balances by 50%
  • Maintains an "approximate balance" that can be used by light clients in the validator_registry, reducing the number of Merkle branches per validator they need to download from 3 to 2 (actually often from ~2.01 to ~1.01, because when fetching a committee the Merkle branches in active_index_roots are mostly shared), achieving a very significant decrease in light client bandwidth costs
  • Maintains an "approximate balance" that can be used in the fork choice rule (this does not technically need to be done in the state, but if it's done here it doesn't need to be done elsewhere...)

The "slack mechanism" (the rounded balance is adjusted down when the balance falls below x, but is adjusted up when it goes above x + 1.5) ensures that a full 0.5 ETH of balance adjustment is required to cause the rounded balance to change. This prevents "on-the-edge attacks" where an attacker maintains a large set of validators whose balances oscillate between x+0.000001 and x-0.000001 for some integer x, causing very frequent changes of the rounded balance and hence very frequent (i) relatively expensive validator registry re-hashings, and (ii) relatively expensive fork choice metadata adjustments.

@JustinDrake
Copy link
Collaborator

JustinDrake commented Feb 25, 2019

Minor suggestions:

  1. Stick with the Gwei type for consistency. (Also simplifies code.)
  2. Rename to validator.high_balance and state.low_balances.
  3. Minor code cleanups.
def get_balance(state: BeaconState, index: ValidatorIndex) -> Gwei:
    return state.validator_registry[index].high_balance + state.low_balances[index]
def set_balance(state: BeaconState, index: ValidatorIndex, balance: Gwei) -> None:
    validator = state.validator_registry[index]
    if validator.high_balance > balance or validator.high_balance + 15 * 10**8 < balance:
        validator.high_balance = balance - balance % 10**9
    state.low_balances[index] = balance - validator.high_balance

@CarlBeek
Copy link
Contributor

My understanding is that light clients and the fork choice rule will follow the high_balance. If so, I have two issues relating to this but both stem from the bias of using high_balance estimator.

  1. A floor function as the basis of the estimator, introduces a constant negative bias. Assuming the low low_balances are uniformly distributed across the range, then this is no issue, but it does encourage an attacker to keep all their low_balances as small as possible. (Thereby making the weight of their votes for the fork-choice more efficient than an honest participant's.

  2. The hysteresis adds further bias:

    1. Assuming validators move upwards and downwards with a symmetric probability distribution, on average, a validator's high_balance shifts when it crosses high_balance + 1.25. This positive bias partially counteracts the negative bias of the floor function, but the result is still net-negative.
    2. The bias is not symmetric between those whose balances are on average increasing and those who are decreasing. On average, those whose balances decrease have a positive bias from the hysteresis estimator whilst the opposite is true for those who behave honestly. That is to say, that the hysteresis component of this estimator favors those who are being leaked/slashed whilst decreasing the weight of votes from honest participants (assumed to have increasing balances).

This second issue can be addressed by shifting the hysteresis down by 0.25. (ie. x+0.75 and x+1.25) whilst still preserving the 0.5 ETH separation for the "on-edge attacks".

@vbuterin
Copy link
Contributor Author

That's doable though would require negative low balances which seems like it would add more complication.

In general this would be at most a ~1-3% benefit to the attacker so it's not close to a critical security issue; the fact that attackers with 16 ETH balances can be proposers as often as validators with 32 ETH may be more significant.

The reason neither of these issues are a big deal is that ultimately keeping your balance close to an integer, or pushing your balance down to 16 ETH, requires sacrificing revenue or deliberately incurring penalties, so in practice it's like slashing yourself by the amount by which you're gaining an unfair advantage.

@CarlBeek
Copy link
Contributor

I wholeheartedly agree that none of these issues propose a serious threat it was more a comment on how it well be cool if we could design around it.

I didn't think of the negative low balances, I agree it's not pretty.

@JustinDrake
Copy link
Collaborator

JustinDrake commented Feb 25, 2019

keeping your balance close to an integer [...] requires sacrificing revenue or deliberately incurring penalties

In theory an attacker could use deposits and transfers to make their balances close to integers prior to launching an attack. This may be a (small) argument for having ACTIVATION_EXIT_DELAY apply to transfers (deposits already have a delay thanks to EPOCHS_PER_ETH1_VOTING_PERIOD).

@vbuterin
Copy link
Contributor Author

vbuterin commented Feb 26, 2019

In theory an attacker could use deposits and transfers to make their balances close to integers prior to launching an attack

True, though that would only allow one deliberate change per deposit -> exit cycle, so minimum ~once per ~520 epochs. And a deposit/exit cycle would require two fork choice metadata updates and four branch updates anyway. Perhaps this is a rationale for a minimum deposit period equal to one custody period before exiting is allowed (didn't we have this in an earlier version anyway, to ensure shard proposal committee grinding resistance?), to bump that up to minimum once per ~2568 epochs. Would be a one-line change to implement.

vbuterin added a commit that referenced this issue Mar 7, 2019
@hwwhww hwwhww added the general:enhancement New feature or request label Mar 11, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
general:enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants