Skip to content

Commit

Permalink
add validations from API to spec (#76)
Browse files Browse the repository at this point in the history
* add validations from API to bellatrix spec

* fix: add missing slot key for bids dict access

* fix typo in verify_blinded_block_signature

* address some PR feedback

* remove specific validity conditions from API descriptions

* add code ticks around constant value

* fix pending validator criteria

* add some further changes to builder spec

* cleanup

* add note about slashing for accepting multiple bids

* add functions for bid procesing

* remove 'prepare_blinded_beacon_block' section

* address PR comments & fix spellcheck

* address round of PR feedback

* add 'bool' return type annotation for 'verify' functions
  • Loading branch information
jacobkaufmann authored Jul 6, 2023
1 parent 0b913da commit 34509da
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 24 deletions.
10 changes: 5 additions & 5 deletions apis/builder/blinded_blocks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ post:
summary: Submit a signed blinded block and get unblinded execution payload.
description: |
Submits a `SignedBlindedBeaconBlock` to the builder, binding the proposer
to the block. The builder is now able to unblind the transactions in
`ExecutionPayloadHeader` without the possibility of the validator modifying
them.
to the block.
If the builder is not able to unblind the corresponding
`ExecutionPayloadHeader`, it must error.
A success response (200) indicates that the signed blinded beacon block was
valid. If the signed blinded beacon block was invalid, then the builder
must return an error response (400) with a description of the validation
failure.
After Deneb, this endpoint will additionally accept `SignedBlindedBlobSidecars`, and return
with additional unblinded blobs in response.
Expand Down
17 changes: 4 additions & 13 deletions apis/builder/header.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,10 @@ get:
Requests a builder node to produce a valid execution payload header, which
can be integrated into a blinded beacon block and signed.
Builder must immediately return the header that is able to pay the
proposer's registered `fee_recipient` the most. If the builder is unaware
of `parent_hash`, it must error. If `pubkey` does not match the builder's
expected proposer `pubkey` for `slot`, it must error. If the builder has
not received a validator registration associated with `pubkey`, it must
error.
When possible, the builder must return a header with `gas_limit` equal to
proposer's registered value. If this isn't possible due to the large
difference between `parent.gas_limit` and the preferred `gas_limit`, the
builder must return a header that moves `gas_limit` the maximum amount
allowed under the rules of consensus (currently `parent.gas_limit +/-
parent.gas_limit / 1024`).
If the builder is unable to produce a valid execution payload header, then
the builder MUST return a 204 response. If the request is invalid, then the
builder MUST return an error response (400) with a description of the
validation failure.
tags:
- Builder
parameters:
Expand Down
11 changes: 6 additions & 5 deletions apis/builder/validators.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ post:
description: |
Registers a validator's preferred fee recipient and gas limit.
Builders should verify that `pubkey` corresponds to an active or pending
validator, and that `signature` is valid under `pubkey`. Otherwise, builder
must error. Requests with `timestamp` less than the previous successful
announcement must error. Requests with `timestamp` more than 10 seconds in
the future must error.
A success response (200) indicates that the registration was valid. If the
registration passes validation, then the builder MUST integrate the
registration into its state, such that future blocks built for the
validator conform to the preferences expressed in the registration. If the
registration is invalid, then the builder MUST return an error response
(400) with a description of the validation failure.
tags:
- Builder
requestBody:
Expand Down
198 changes: 197 additions & 1 deletion specs/bellatrix/builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@
| - | - |
| `DOMAIN_APPLICATION_BUILDER` | `DomainType('0x00000001')` |

### Time parameters

| Name | Value | Unit | Duration |
| - | - | - | - |
| `MAX_REGISTRATION_LOOKAHEAD` | `uint64(10)` | seconds | 10 seconds |

## Containers

Consider the following definitions supplementary to the definitions in
Expand Down Expand Up @@ -141,14 +147,204 @@ convert in-protocol messages to their SSZ representation to compute the signing
root and Builder API messages to the SSZ representations defined
[above](#containers).

## Validator registration processing

Validators must submit registrations before they can work with builders.

To assist in registration processing, we use the following functions from the [consensus specs][consensus-specs]:

* [`get_current_epoch`][get-current-epoch]
* [`is_active_validator`][is-active-validator]

### `is_pending_validator`

```python
def is_pending_validator(validator: Validator, epoch: Epoch) -> bool:
"""
Check if ``validator`` is pending in ``epoch``.
"""
return validator.activation_epoch > epoch
```

### `is_eligible_for_registration`

```python
def is_eligible_for_registration(state: BeaconState, validator: Validator) -> bool:
"""
Check if ``validator`` is active or pending.
"""
epoch = get_current_epoch(state)
return is_active_validator(validator, epoch) or is_pending_validator(validator, epoch)
```

### `verify_registration_signature`

```python
def verify_registration_signature(state: BeaconState, signed_registration: SignedValidatorRegistrationV1) -> bool:
pubkey = signed_registration.message.pubkey
domain = compute_domain(DOMAIN_APPLICATION_BUILDER)
signing_root = compute_signing_root(signed_registration.message, domain)
return bls.Verify(pubkey, signing_root, signed_registration.signature)
```

### `process_registration`

A `registration` is considered valid if the following function completes without raising any assertions:

```python
def process_registration(state: BeaconState,
registration: SignedValidatorRegistrationV1,
registrations: Dict[BLSPubkey, ValidatorRegistrationV1],
current_timestamp: uint64):
signature = registration.signature
registration = registration.message

# Verify BLS public key corresponds to a registered validator
validator_pubkeys = [v.pubkey for v in state.validators]
assert pubkey in validator_pubkeys

index = ValidatorIndex(validator_pubkeys.index(pubkey))
validator = state.validators[index]

# Verify validator registration elibility
assert is_eligible_for_registration(state, validator)

# Verify timestamp is not too far in the future
assert registration.timestamp <= current_timestamp + MAX_REGISTRATION_LOOKAHEAD

# Verify timestamp is not less than the timestamp of the previous registration (if it exists)
if registration.pubkey in registrations:
prev_registration = registrations[registration.pubkey]
assert registration.timestamp >= prev_registration.timestamp

# Verify registration signature
assert verify_registration_signature(state, registration)
```

## Building

The builder builds execution payloads for registered validators and submits them to an auction that happens each slot.
When a validator goes to propose, they select the winning `SignedBuilderBid` offered in that slot by constructing a `SignedBlindedBeaconBlock`.
The builder only reveals the full execution payload once they receive a valid `SignedBlindedBeaconBlock`.
The validator accepts a bid and commits to a specific `ExecutionPayload` with a `SignedBlindedBeaconBlock`.

### Bidding

To assist in bidding, we use the following functions from the [consensus specs][consensus-specs]:

* [`get_beacon_proposer_index`][get-beacon-proposer-index]
* [`hash_tree_root`][hash-tree-root]

Execution payloads are built for a specific `slot`, `parent_hash`, and `pubkey` tuple corresponding to a unique beacon block serving as the parent.

The builder validates requests for bids according to `is_eligible_for_bid(state, registrations, slot, parent_hash, pubkey)` where:

* `state` is the builder's consensus state transitioned to `slot`, including the application of any blocks before `slot`.
* `registrations` is the registry of validators [successfully registered](#process-registration) with the builder.

```python
def is_eligible_for_bid(state: BeaconState,
registrations: Dict[BLSPubkey, ValidatorRegistrationV1],
slot: Slot,
parent_hash: Hash32,
pubkey: BLSPubkey) -> bool:
# Verify slot
if slot != state.slot:
return False

# Verify BLS public key corresponds to a registered validator
if pubkey not in registrations:
return False

# Verify BLS public key corresponds to the proposer for the slot
proposer_index = get_beacon_proposer_index(state)
if pubkey != state.validators[proposer_index].pubkey:
return False

# Verify parent hash
return parent_hash == state.latest_execution_payload_header.block_hash
```

#### Constructing the `ExecutionPayloadHeader`

The builder MUST provide a bid for the valid execution payload that is able to pay the `fee_recipient` in the validator registration for the registered `pubkey` the most.
The builder MUST build an execution payload whose `gas_limit` is equal to the `gas_limit` of the latest registration for `pubkey`, or as close as is possible under the consensus rules.

#### Constructing the `BuilderBid`

```python
def get_bid(execution_payload: ExecutionPayload, value: uint256, pubkey: BLSPubkey) -> BuilderBid:
header = ExecutionPayloadHeader(
parent_hash=payload.parent_hash,
fee_recipient=payload.fee_recipient,
state_root=payload.state_root,
receipts_root=payload.receipts_root,
logs_bloom=payload.logs_bloom,
prev_randao=payload.prev_randao,
block_number=payload.block_number,
gas_limit=payload.gas_limit,
gas_used=payload.gas_used,
timestamp=payload.timestamp,
extra_data=payload.extra_data,
base_fee_per_gas=payload.base_fee_per_gas,
block_hash=payload.block_hash,
transactions_root=hash_tree_root(payload.transactions),
)
return BuilderBid(header=header, value=value, pubkey=pubkey)
```

#### Packaging into a `SignedBuilderBid`

The builder packages `bid` into a `SignedBuilderBid`, denoted `signed_bid`, with `signed_bid=SignedBuilderBid(bid=bid, signature=signature)` where `signature` is obtained from:

```python
def get_bid_signature(state: BeaconState, bid: BuilderBid, privkey: int) -> BLSSignature:
domain = compute_domain(DOMAIN_APPLICATION_BUILDER)
signing_root = compute_signing_root(bid, domain)
return bls.Sign(privkey, signing_root)
```

### Revealing the `ExecutionPayload`

#### Blinded block processing

To assist in blinded block processing, we use the following functions from the [consensus specs][consensus-specs]:

* [`get_beacon_proposer_index`][get-beacon-proposer-index]
* [`get_current_epoch`][get-current-epoch]
* [`compute_epoch_at_slot`][compute-epoch-at-slot]
* [`get_domain`][get-domain]

A proposer selects a bid by constructing a valid `SignedBlindedBeaconBlock`.
The proposer MUST accept at most one bid for a given `slot`.
Otherwise, the builder can produce a [`ProposerSlashing`][proposer-slashing].

The builder must ensure the `SignedBlindedBeaconBlock` is valid according to the rules of consensus and also that the signature is correct for the expected proposer using `verify_blinded_block_signature`:

##### `verify_blinded_block_signature`

```python
def verify_blinded_block_signature(state: BeaconState, signed_block: SignedBlindedBeaconBlock) -> bool:
proposer = state.validators[get_beacon_proposer_index(state)]
epoch = get_current_epoch(state)
signing_root = compute_signing_root(signed_block.message, get_domain(state, DOMAIN_BEACON_PROPOSER, epoch))
return bls.Verify(proposer.pubkey, signing_root, signed_block.signature)
```

## Endpoints

Builder API endpoints are defined in `builder-oapi.yaml` in the root of the
repository. A rendered version can be viewed at
https://ethereum.github.io/builder-specs/.


[consensus-specs]: https://github.com/ethereum/consensus-specs
[bls]: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#bls-signatures
[compute-root]: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#compute_signing_root
[compute-domain]: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#compute_domain
[get-current-epoch]: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#get_current_epoch
[is-active-validator]: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#is_active_validator
[hash-tree-root]: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#hash_tree_root
[get-domain]: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#get_domain
[compute-epoch-at-slot]: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#compute_epoch_at_slot
[get-beacon-proposer-index]: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#get_beacon_proposer_index
[proposer-slashing]: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#proposerslashing
25 changes: 25 additions & 0 deletions specs/bellatrix/validator.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,31 @@ the following actions:
to assemble a `SignedBeaconBlock` following the rest of the proposal process outlined in the
[Bellatrix specs][bellatrix-specs].

##### Bid processing

Bids received from step (1) above can be validated with `process_bid` below, where `state` corresponds to the state for the proposal without applying the block (currently under construction) and `fee_recipient` corresponds to the validator's most recently registered fee recipient address:

```python
def verify_bid_signature(state: BeaconState, signed_bid: SignedBuilderBid) -> bool:
pubkey = signed_bid.message.pubkey
domain = compute_domain(DOMAIN_APPLICATION_BUILDER)
signing_root = compute_signing_root(signed_registration.message, domain)
return bls.Verify(pubkey, signing_root, signed_bid.signature)
```

A `bid` is considered valid if the following function completes without raising any assertions:

```python
def process_bid(state: BeaconState, bid: SignedBuilderBid, fee_recipient: ExecutionAddress):
# Verify execution payload header
header = bid.message.header
assert header.parent_hash == state.latest_execution_payload_header.block_hash
assert header.fee_recipient == fee_recipient

# Verify bid signature
verify_bid_signature(state, bid)
```

#### Relation to local block building

The external builder network offers a service for proposers that may from time to time fail to produce a timely block.
Expand Down
1 change: 1 addition & 0 deletions wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ apis
bellatrix
bls
bn
bool
capella
cli
dev
Expand Down

0 comments on commit 34509da

Please sign in to comment.