Here are the Prysm validator features that I've been working on last week:
The end to end tests are an important part of the Prysm test collateral since they make many systems work together wihout any mocking and simulate how the validator and beacon node should work for about 10 epochs. Therefore, adding a version of those tests where the REST API is tested instead of the gRPC one seemed like a no-brainer. In addition to onboarding the tests to the E2E collateral, this change also merged the REST API build flavor with the gRPC one, which means that we don't need 2 build flavors anymore just to enable or disable the REST API. Everything is controlled through the --enable-beacon-rest-api
command-line flag.
DomainData
endpointThe DomainData
endpoint is a relatively simple one since it only requires interacting with the /eth/v1/beacon/genesis
endpoint, which had already been implemented during the WaitForChainStart work. After retrieving the genesis validator root, everything - including computing the signature domain - is done offline. The change can be found here.
GetAttestationData
endpointThe goal of the GetAttestationData
endpoint is to retrieve the following information from the attestations included into a slot:
There was a small mixup while I was working on this PR, which is why it was closed with 0 files changed. Unfortunately, someone else rebased this PR on top of their own PR, which got merged first and therefore absorbed all of my changes. It's not a big deal besides messing up with the history (the other PR basically has 2 new features in it instead of a single one) and associating the commits with the wrong author during a git blame.
ProposeBeaconBlock
endpointThis change was definitely the biggest one so far, although it was still not very complex. The complexity here was more related to the limitations of Go version 1.19 than anything else. At the time of writing, there are currently 5 types of blocks that Prysm supports:
Although all block are slightly different and mostly a superset of the previous version, most of their fields are identical. But because of these small differences, they all have different structs that make it hard to build the blocks in an elegant polymorphic way. For example, here is what the body of a Phase0 block looks like:
and here is what the body of an Altair block looks like:
So although they are almost identical, if we were to refactor Prysm, BeaconBlockBodyJson
could easily become a part of BeaconBlockBodyAltairJson
, which would look like this:
But since this is not the case, we need to duplicate the logic of assigning each field to the corresponding gRPC value for each type of block, even though most of the fields are identical.
Another problem is that, even though the gRPC structs and the beacon API JSON structs are literally identical, there are no elegant ways to translate one into the other. For example, compare the structs between the beacon API version and the gRPC version of a Phase0 block:
If we ignore the protobuf metadata at the top of the gRPC struct, they are identical and only their types differ. In theory, the conversion should also be fairly mechanical:
hex
tag, convert it using the hexutil.Decode
functionBut Golang doesn't yet have an elegant way to do this conversion a single time at compile time when dealing with very similar types. It has generics, but they are not powerful enough yet to be able to call common fields. It lacks a compile-type alternative to templates in C++. Even though reflection seems like a fairly elegant solution, I decided against it for a few reasons:
Overall, reflection is great when adding middlewares that connect existing APIs together, but when adding new features it feels cleaner and safer to go with the compile-time alternative of just assigning each struct field manually, although it may look less elegant at first.