Skip to main content

Constraints API: Builder

Overview

The Builder API is the way for proposers to communicate with builders in the PBS pipeline. The constraints-API adds the following new responsibilities:

  • Proposers should be able to submit constraints through the builder API
  • Proposers should be able to delegate constraint submission rights to another entity
  • Proposers should be able to get bids with proofs of constraint validity

Endpoints

constraints namespace

This namespace defines endpoints that should be called by either the validator, or an entity that the validator has delegated constraint submission rights to.

/constraints/v1/builder/constraints

Endpoint for submitting a batch of signed constraints.

  • Method: POST
  • Response: Empty
  • Headers:
    • Content-Type: application/json
  • Body: JSON object of type List[SignedConstraints, MAX_CONSTRAINTS_PER_SLOT]

Schema

# A signed "bundle" of constraints.
class SignedConstraints(Container):
message: ConstraintsMessage
signature: BLSSignature

# A "bundle" of constraints for a specific slot.
class ConstraintsMessage(Container):
pubkey: BLSPubkey,
slot: uint64
top: boolean,
transactions: List[Bytes, MAX_CONSTRAINTS_PER_SLOT]

Description

In the SignedConstraints object, the signature field represents the BLS signature over the signing root of the ConstraintsMessage object. The signature must come from either the validator signing key, or a delegated key (see below).

info

The message digest is computed like this:

fn digest(messsage: ConstraintsMessage) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(messsage.pubkey.to_vec());
hasher.update(messsage.slot.to_le_bytes());
hasher.update((messsage.top as u8).to_le_bytes());

for tx in &messsage.transactions {
hasher.update(tx.hash());
}

hasher.finalize().into()
}

Explanation of other fields:

  • pubkey: the BLS public key of the validator submitting the constraints.
  • slot: the target slot for which these constraints are valid.
  • top: boolean value indicating whether these constraints are only valid on the top of the block. Per slot, only 1 top-of-block bundle is valid.
  • transactions: the list of EIP-2718 RLP-encoded transactions. In case of a type 3 transaction, must contain the blob data.
Example body
// Example: 2 inclusion constraints where the transactions can be placed anywhere in the block

[
{
"message": {
"pubkey": "0xa695ad325dfc7e1191fbc9f186f58eff42a634029731b18380ff89bf42c464a42cb8ca55b200f051f57f1e1893c68759",
"slot": 32,
"top": false,
"transactions": [
"0x02f86c870c72dd9d5e883e4d0183408f2382520894d2e2adf7177b7a8afddbc12d1634cf23ea1a71020180c001a08556dcfea479b34675db3fe08e29486fe719c2b22f6b0c1741ecbbdce4575cc6a01cd48009ccafd6b9f1290bbe2ceea268f94101d1d322c787018423ebcbc87ab4",
"0x02f86c870c72dd9d5e883e4d0183408f2382520894d2e2adf7177b7a8afddbc12d1634cf23ea1a71020180c001a08556dcfea479b34675db3fe08e29486fe719c2b22f6b0c1741ecbbdce4575cc6a01cd48009ccafd6b9f1290bbe2ceea268f94101d1d322c787018423ebcbc87ab4"
]
},
"signature": "0xae5aa93391a256eebef79fe452951ae196b3b3ac9046e45cd63713a57ad0548ddb56430477cb3d70287710984fc4bc4e091da1d5594de25c2caeca4872b35c12587ef168b0c878cde4025d66d4195cd875df7e2c4d7ba2b9fe2010b0cf5caccc"
},
{
"message": {
"pubkey": "0xa695ad325dfc7e1191fbc9f186f58eff42a634029731b18380ff89bf42c464a42cb8ca55b200f051f57f1e1893c68759",
"slot": 33,
"top": false,
"transactions": [
"0x02f86c870c72dd9d5e883e4d0183408f2382520894d2e2adf7177b7a8afddbc12d1634cf23ea1a71020180c001a08556dcfea479b34675db3fe08e29486fe719c2b22f6b0c1741ecbbdce4575cc6a01cd48009ccafd6b9f1290bbe2ceea268f94101d1d322c787018423ebcbc87ab4",
"0x02f86c870c72dd9d5e883e4d0183408f2382520894d2e2adf7177b7a8afddbc12d1634cf23ea1a71020180c001a08556dcfea479b34675db3fe08e29486fe719c2b22f6b0c1741ecbbdce4575cc6a01cd48009ccafd6b9f1290bbe2ceea268f94101d1d322c787018423ebcbc87ab4"
]
},
"signature": "0x9822741e08975fffdd16ba39967f86780b6b11c5235bde3f4d68c7ef8612ae5d1e71cd40431baa74e699e4f94e3090300c00bef720e8b982e12e7e9eb9cc78ab75ed99e412c99ea4ea89cc887ab7849b0fc9e299183f977965c0607b5a178793"
}
]

// Example: a bundle of 2 transactions executed at the top of the block

[
{
"message": {
"pubkey": "0xa695ad325dfc7e1191fbc9f186f58eff42a634029731b18380ff89bf42c464a42cb8ca55b200f051f57f1e1893c68759",
"slot": 33,
"top": true,
"transactions": [
"0x02f86c870c72dd9d5e883e4d0183408f2382520894d2e2adf7177b7a8afddbc12d1634cf23ea1a71020180c001a08556dcfea479b34675db3fe08e29486fe719c2b22f6b0c1741ecbbdce4575cc6a01cd48009ccafd6b9f1290bbe2ceea268f94101d1d322c787018423ebcbc87ab4",
"0x02f86c870c72dd9d5e883e4d0183408f2382520894d2e2adf7177b7a8afddbc12d1634cf23ea1a71020180c001a08556dcfea479b34675db3fe08e29486fe719c2b22f6b0c1741ecbbdce4575cc6a01cd48009ccafd6b9f1290bbe2ceea268f94101d1d322c787018423ebcbc87ab4"
]
},
"signature": "0x9241909209aa8f5d7128452d478922f7d2f54040eeaa0e998cd395f102c577c171523073aabe3290caeaed2389e412ae03021bf3ef29a836ad3e8dde7cc799fa95695ae980246b5714dd75f6f06427a8c5e911db4295c6f8975bbe716704d19b"
}
]

// Example: a bundle of 2 transactions that must be executed atomically, but have no constraints
// on the position in the block

[
{
"message": {
"pubkey": "0xa695ad325dfc7e1191fbc9f186f58eff42a634029731b18380ff89bf42c464a42cb8ca55b200f051f57f1e1893c68759",
"slot": 32,
"top": false,
"transactions": [
"0x02f86c870c72dd9d5e883e4d0183408f2382520894d2e2adf7177b7a8afddbc12d1634cf23ea1a71020180c001a08556dcfea479b34675db3fe08e29486fe719c2b22f6b0c1741ecbbdce4575cc6a01cd48009ccafd6b9f1290bbe2ceea268f94101d1d322c787018423ebcbc87ab4",
"0x02f86c870c72dd9d5e883e4d0183408f2382520894d2e2adf7177b7a8afddbc12d1634cf23ea1a71020180c001a08556dcfea479b34675db3fe08e29486fe719c2b22f6b0c1741ecbbdce4575cc6a01cd48009ccafd6b9f1290bbe2ceea268f94101d1d322c787018423ebcbc87ab4"
]
},
"signature": "0xae5aa93391a256eebef79fe452951ae196b3b3ac9046e45cd63713a57ad0548ddb56430477cb3d70287710984fc4bc4e091da1d5594de25c2caeca4872b35c12587ef168b0c878cde4025d66d4195cd875df7e2c4d7ba2b9fe2010b0cf5caccc"
}
]

/constraints/v1/builder/delegate

Endpoint for delegating constraint submission rights to another BLS key. If this is called multiple times, it will not override any previous delegations, but add new ones.

  • Method: POST
  • Response: Empty
  • Headers:
    • Content-Type: application/json
  • Body: JSON object of type SignedDelegation[]

Schema

# A signed delegation
class SignedDelegation(Container):
message: Delegation
signature: BLSSignature

# A delegation from a proposer to a BLS public key
class Delegation(Container):
action: uint8, # must be 0 for all delegations
validator_pubkey: BLSPubkey,
delegatee_pubkey: BLSPubkey

Description

The signature field is the BLS signature from the specified validator_pubkey over the signing root of the Delegation object.

info

The message digest is computed like this:

fn digest(message: Delegation) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update([message.action]);
hasher.update(message.validator_pubkey.to_vec());
hasher.update(message.delegatee_pubkey.to_vec());

hasher.finalize().into()
}
Example body
[
{
"message": {
"action": 0,
"validator_pubkey": "0xa695ad325dfc7e1191fbc9f186f58eff42a634029731b18380ff89bf42c464a42cb8ca55b200f051f57f1e1893c68759",
"delegatee_pubkey": "0x8db2e6dd9fe48cb14b2d0d5427b639c6aa0c7bf25cf132f27ad5ae5a2dd2523626d26171d5189869cf83228b29ef3919"
},
"signature": "0xb62c235dd275859c1bda06b657f9a6058dc4c7d27322e16c743abbe942caa54cf00cc5d206db1924a30e1cc91e44db6b0091a405926e470063313b1022f8982e32476934de79ace0deb5b9332d5347aa9f4a8b9d0ed2222af0144eefb6aed145"
}
]

/constraints/v1/builder/revoke

Endpoint for revoking constraint submission rights from another BLS key.

  • Method: POST
  • Response: Empty
  • Headers: Content-Type: application/json
  • Body: JSON object of type SignedRevocation[]

Schema

# A signed revocation
class SignedRevocation(Container):
message: Revocation,
signature: BLSSignature

# A revocation from a proposer for a BLS public key
class Revocation(Container):
action: uint8, # must be 1 for all revocations
validator_pubkey: BLSPubkey,
delegatee_pubkey: BLSPubkey

Description

The signature field is the BLS signature from the specified validator_pubkey over the signing root of the Revocation object.

info

The message digest is computed like this:

fn digest(message: Revocation) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update([message.action]);
hasher.update(message.validator_pubkey.to_vec());
hasher.update(message.delegatee_pubkey.to_vec());

hasher.finalize().into()
}
Example body
[
{
"message": {
"action": 1,
"validator_pubkey": "0xa695ad325dfc7e1191fbc9f186f58eff42a634029731b18380ff89bf42c464a42cb8ca55b200f051f57f1e1893c68759",
"delegatee_pubkey": "0x8db2e6dd9fe48cb14b2d0d5427b639c6aa0c7bf25cf132f27ad5ae5a2dd2523626d26171d5189869cf83228b29ef3919"
},
"signature": "0xb62c235dd275859c1bda06b657f9a6058dc4c7d27322e16c743abbe942caa54cf00cc5d206db1924a30e1cc91e44db6b0091a405926e470063313b1022f8982e32476934de79ace0deb5b9332d5347aa9f4a8b9d0ed2222af0144eefb6aed145"
}
]

eth namespace

We also add an endpoint to the existing eth namespace for dealing with proofs when a proposer requests a bid with header from the builder.

/eth/v1/builder/header_with_proofs/{slot}/{parent_hash}/{pubkey}

Endpoint for requesting a builder bid with constraint proofs.

  • Method: GET
  • Response: VersionedSignedBuilderBidWithProofs
  • Parameters:
    • slot: string (regex [0-9]+)
    • parent_hash: string (regex 0x[a-fA-F0-9]+)
    • pubkey: string (regex 0x[a-fA-F0-9]+)
  • Body: Empty

Schema

class VersionedSignedBuilderBidWithProofs:
... # All regular fields from VersionedSignedBuilderBid, additionally
proofs: InclusionProofs

# An SSZ Merkle Multiproof for proving inclusion against the transactions_root
class InclusionProofs(Container):
transaction_hashes: List[Bytes32, MAX_CONSTRAINTS_PER_SLOT]
generalized_indexes: List[uint64, MAX_CONSTRAINTS_PER_SLOT]
merkle_hashes: List[List[Bytes32], MAX_CONSTRAINTS_PER_SLOT]

Description

VersionedSignedBuilderBid is from the original specs. VersionedSignedBuilderBidWithProofs just adds a field for proofs of inclusion. Note that InclusionProofs is a Merkle multiproof, as defined in the consensus specs.

When serializing, the proofs field must be present in data, at the same level of signature and message. See the example below.

Example response
{
"version": "deneb",
"data": {
"message": {
"header": {
"parent_hash": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
"fee_recipient": "0xabcf8e0d4e9587369b2301d0790347320302cc09",
"state_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
"receipts_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
"logs_bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"prev_randao": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
"block_number": "1",
"gas_limit": "1",
"gas_used": "1",
"timestamp": "1",
"extra_data": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
"base_fee_per_gas": "1",
"blob_gas_used": "1",
"excess_blob_gas": "1",
"block_hash": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
"transactions_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
"withdrawals_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"
},
"blob_kzg_commitments": [
"0xa94170080872584e54a1cf092d845703b13907f2e6b3b1c0ad573b910530499e3bcd48c6378846b80d2bfa58c81cf3d5"
],
"value": "1",
"pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a"
},
"proofs": {
"transaction_hashes": ["0x1234...", "0x456..."],
"generalized_indexes": [4, 5],
"merkle_hashes": ["0x5097...", "0x932587..."]
},
"signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505"
}
}

Note on Signatures

Various messages in the constraints-API must be correctly signed. The signing procedure involves signing over the actual message digest and a signing domain. We explain how to calculate this signing domain below:

// The constraints-API domain type
const CONSTRAINTS_DOMAIN_TYPE: [u8; 4] = [109, 109, 111, 67]

#[derive(Debug, TreeHash)]
struct ForkData {
fork_version: [u8; 4],
genesis_validators_root: [u8; 32],
}

let mut domain = [0u8; 32];
domain[..4].copy_from_slice(&CONSTRAINTS_DOMAIN_TYPE);

// This will depend on the chain configuration
let fork_version = chain.genesis_fork_version();

// Empty genesis validators root!
let fd = ForkData { fork_version, genesis_validators_root: [0; 32] };

let fork_data_root = fd.tree_hash_root();

domain[4..].copy_from_slice(&fork_data_root[..28]);

// This will be your final signing domain!
domain

This signing domain must then be further used to sign any message digests like so:

#[derive(Default, Debug, TreeHash)]
struct SigningData {
object_root: [u8; 32],
signing_domain: [u8; 32],
}

// The root (digest) of the object to sign
let signing_data = SigningData { object_root, signing_domain };
let root = signing_data.tree_hash_root()

// The `root` above is what needs to be signed in order to produce a valid signature!

We include this part so that infrastructure builders can produce and verify valid constraint signatures.