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

Routine for processing deposits #1640

Merged
merged 9 commits into from
Dec 21, 2018
Merged

Routine for processing deposits #1640

merged 9 commits into from
Dec 21, 2018

Conversation

hwwhww
Copy link
Contributor

@hwwhww hwwhww commented Dec 18, 2018

What was wrong?

Implement routine for processing deposits

How was it fixed?

  1. Add BeaconState.update_validator(validator_index, validator)
  2. Fix DepositInput
  3. Fix get_new_validator_registry_delta_chain_tip
  4. Add eth/beacon/deposits_helpers.py <-- might be put to somewhere under state machine
    • Implement def min_empty_validator_index as spec
    • Impelment def process_deposit as spec

Question to the reviewers:

  1. The state transition function will update state: BeaconState RLP/SSZ object frequently. Currently, I'm using changeset() for each snippet, but I'm not sure if that's the best practice.
  2. Also add an instance function BeaconState.update_validator(validator_index, validator) for updating BeaconState.validator_registry: Sequence[ValidatorRecord] - immutably. Do you think it's okay for readability or any other concerns?

Cute Animal Picture

shark

Copy link
Contributor

@djrtwo djrtwo left a comment

Choose a reason for hiding this comment

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

great! comments below

eth/beacon/deposit_helpers.py Outdated Show resolved Hide resolved
eth/beacon/deposit_helpers.py Outdated Show resolved Hide resolved
eth/beacon/deposit_helpers.py Outdated Show resolved Hide resolved
eth/beacon/deposit_helpers.py Outdated Show resolved Hide resolved
eth/beacon/deposit_helpers.py Show resolved Hide resolved
validator_registry=(),
)

privkey_1 = privkeys[0]
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe should do a loop

for index in range(3):
    privkey = privkeys[index]
    pubkey = pubkeys[index]
    ....
    result_state, validator_index = process_deposit()
    ...
    check_results()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh but I want to make an empty slot between the tests. ("# Force the first validator exited")

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I see! Maybe update comment

# Add the third validator.
# Should overwrite previously exited validator

or something. Would be good to make it more explicit than just the assert len(result_state.validator_registry) == 2

@djrtwo
Copy link
Contributor

djrtwo commented Dec 18, 2018

(2) seems okay to me.
Not sure about (1). We're going to be updating the shit out of state in the transition function.

current_slot
):
return index
return None
Copy link
Member

Choose a reason for hiding this comment

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

We tend to try and stay away from return value checking. I'd convert this to raise an IndexError or a custom exception (probably the better option) here to ensure that call-sites cannot accidentally treat the return value incorrectly.

"BLS signature verification error"
)

return True
Copy link
Member

Choose a reason for hiding this comment

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

Our convention for validation functions is to have them return None (implicitly with no return statement).

# Update validator's balance and state
with validator.build_changeset() as validator_changeset:
validator_changeset.balance = validator.balance + deposit
validator = validator_changeset.commit()
Copy link
Member

Choose a reason for hiding this comment

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

This can be simplified to: validator = validator.copy(balance=validator.balance + deposit). The changeset API is better purposed for when you need to do multiple updates, potentially with some conditional logic.

Copy link
Contributor Author

@hwwhww hwwhww Dec 19, 2018

Choose a reason for hiding this comment

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

Oh, you're right.

And I just benchmarked between using copy() and changeset() - I thought using copy would be slower since it's deepcopy, but, it turns out using copy is 30-40% faster(!) (with a simple RLP data with 13 fields)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

p.s. I haven't tried a hugh RLP data like BeaconState. But the performance for ValidatorRecord should be better with using copy().

@hwwhww
Copy link
Contributor Author

hwwhww commented Dec 19, 2018

Changelog

  1. Rebase for resolving conflicts
  2. Add eth/beacon/exceptions.py - MinEmptyValidatorIndexNotFound
  3. Refactors
    1. Update eth.utils.bls
      1. Currently, inside the the pairing functions, there are some assert ... thing that will be fixed in Refactoring the Codebase py_ecc#35. Currently I add something to make sure the verify function returns bool.
    2. Add eth.beacon.deposit_helpers.add_pending_validator
    3. Add ValidatorRecord.get_pending_validator() classmethod for generating new pending validator
  4. Resolve PR feedback

validator_registry=(),
)

privkey_1 = privkeys[0]
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I see! Maybe update comment

# Add the third validator.
# Should overwrite previously exited validator

or something. Would be good to make it more explicit than just the assert len(result_state.validator_registry) == 2


validator_pubkeys = tuple([v.pubkey for v in state.validator_registry])
if pubkey not in validator_pubkeys:
validator = ValidatorRecord.get_pending_validator(
Copy link
Contributor

@djrtwo djrtwo Dec 19, 2018

Choose a reason for hiding this comment

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

Would this be clearer as create_pending_validator rather than get_?
get implies to me that it is trying to retrieve something that at least might already exist.

Not sure what the codebase naming conventions are in general. Will defer to @pipermerriam

current_slot: int,
zero_balance_validator_ttl: int) -> int:
for index, validator in enumerate(validators):
if (
Copy link
Member

Choose a reason for hiding this comment

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

small formatting nitpick. I'd capture the contidional in a variable and then use it in a single line if

is_empty = (validator.balance == 0 and ...)
if is_empty:
    return index

except MinEmptyValidatorIndexNotFound:
index = None

if index is None:
Copy link
Member

Choose a reason for hiding this comment

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

I think this would all be a little better readability if you had all of this in the try/except/else

try:
    index = ...
except MinEmptyValidatorIndexNotFound:
    ...  # logic from the `if index is None`
else:
    ...  # other logic.

return state, index

randao_commitment,
)

validator_pubkeys = tuple([v.pubkey for v in state.validator_registry])
Copy link
Member

Choose a reason for hiding this comment

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

The inner [...] brackets can be removed as tuple(generator) works just fine.

index = validator_pubkeys.index(pubkey)
validator = state.validator_registry[index]

if validator.withdrawal_credentials != validator.withdrawal_credentials:
Copy link
Member

Choose a reason for hiding this comment

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

This conditional looks broken. Seems like you are comparing the same value with itself. Is one of these supposed to be just withdrawal_credentials.

Maybe add a test that covers this code path since this code path probably isn't being exercised in the tests right now?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, should be checking that the existing validator with the pubkey pubkey has the same withdrawal_credentials that were passed in with the deposit

validator: ValidatorRecord) -> 'BeaconState':
validator_registry = list(self.validator_registry)
validator_registry[validator_index] = validator
self = self.copy(
Copy link
Member

Choose a reason for hiding this comment

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

I'm 👎 on overriding self in a method context. Seems like an antipattern and easily able to lead to confusion. Maybe just inline the copy in the return statement or call the variable something like updated_state

eth/utils/bls.py Outdated
pairing(
FQP_point_to_FQ2_point(hash_to_G2(message, domain)),
neg(decompress_G1(pubkey)),
False
Copy link
Member

Choose a reason for hiding this comment

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

I find positional booleans to be confusing since they give no context on what is being False'd.. Maybe convert to either a keyword argument, or just use a comment to denote what argument this is for if performance is an issue.

eth/utils/bls.py Outdated
for m_pubs in set(messages):
# aggregate the pubs
group_pub = Z1
for i in range(len_msgs):
Copy link
Member

Choose a reason for hiding this comment

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

This looks like you could do for pubkey, message in zip(pubkeys, messages): which removes the indexing.

Alternatively, I think the following is a functional equivalent implementation.

groups = (
    pairing(hash_to_G2(m_pubs, domain), reduce(add, (decompress_G1(pubkey), Z1)), False)
    for (pubkey, message) 
    in zip(pubkeys, messages) if message == m_pubs
    for m_pubs in set(messages)
)
final_exponentiation = final_exponentiate(
    reduce(
        operator.mul, 
        groups,
        FQ12([1] + [0] * 11),
    ) * pairing(decompress_G2(signature), neg(G1), False)
)
return final_exponentiation == FQ12.one()

Also, there are a few constants in there that could be lifted out such as:

  • FQ12.one()
  • neg(G1)

The above is just hand-written code without being tested so you should verify if you choose to switch. I think the above code would be more performant... but that's just intuition.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the feedback! 👍 That's looks great!

I'm inclined to leave this refactoring to #1592 because the current spec is still a draft, we may seek for review from academic cryptographers to help review in 1-2 months; in the meantime, the Python codes are the reference for the other implementers.

@hwwhww hwwhww merged commit d82b10a into ethereum:master Dec 21, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants