Try   HackMD

EPF Cohort 3 Dev Update 5 (pavignol)

Here are the Prysm validator features that I’ve been working on last week:

Added a REST API implementation for Validator's GetBeaconBlock

During my previous update, I talked about how implementing ProposeBeaconBlock was a big change (although not very complex) because generics in Go are not as powerful yet as the equivalent in other languages (e.g. templates in C++). But little did I know that GetBeaconBlock was going to be worse.

GetBeaconBlock is the endpoint that validators query when they want the beacon node to produce a valid, non-signed block and send it to them. The validator then signs the block and calls ProposeBeaconBlock to broadcast the signed block to the beacon network. Both endpoints are very similar at first glance since they both deal with similar structures, but the additional complexity in GetBeaconBlock is caused by the need to validate every single field that the beacon node returns.

Every single integer and hex-encoded string needs to be validated before it gets converted to an integer or bytes to make sure that we don't panic and can gracefully fail when interacting with a buggy or non-compliant beacon node. ProposeBeaconBlock didn't have this problem since it's a POST endpoint that doesn't return any data, so the only data that we deal with is data that we already verified is valid internally.

So, in addition to the same complexity of converting between JSON and protobuf structs that we had to deal with ProposeBeaconBlock, the implementation of GetBeaconBlock is full of statements that look like the following:

blockProposerIndex, err := strconv.ParseUint(block.ProposerIndex, 10, 64)
if err != nil {
    return nil, errors.Wrapf(err, "failed to parse proposer index `%s`", block.ProposerIndex)
}

parentRoot, err := hexutil.Decode(block.ParentRoot)
if err != nil {
    return nil, errors.Wrapf(err, "failed to decode parent root `%s`", block.ParentRoot)
}

This code can look repetitive and is never fun to write, but it is necessary in order to have a robust validator implementation that doesn't randomly crash and that can output helpful error messages to the user when it fails.

But the implementation is not the only place where GetBeaconBlock is more complex than ProposeBeaconBlock; tests are also more complex for the same reason. Whereas ProposeBeaconBlock doesn't need to test for any potential failures after sending the POST request, GetBeaconBlock needs to make sure that every potential bad output doesn't cause a panic and outputs the expected error message.

Luckily, table-driven tests take some of the pain away. My approach here was to have a function that generates a valid output, and then each test case would change a single field at a time by putting an invalid value and making sure that we get the expected error message.

If you're interested to see what this change looks like, you can look at the PR.

Added a REST API implementation for Validator's PrepareBeaconProposer and ProposeExit

PrepareBeaconProposer and ProposeExit are the complete opposite of GetBeaconBlock and ProposeBeaconBlock. Indeed, those APIs deal with a very small amount of data and the mapping between the beacon API and the protobuf structs is pretty much 1 to 1. Also, there's absolutely no validation aside from handling a bad HTTP response, so the tests are also very straightforward. I was able to implement both endpoints and their tests within an hour, including running the E2E tests. There's not a lot more to say about those endpoints, but feel free to check out the PrepareBeaconProposer PR and the ProposeExit PR.