Constraints API: Builder
- Original API docs: https://ethereum.github.io/builder-specs
- Updated API docs: https://chainbound.github.io/builder-specs
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).
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.
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.
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
(regex0x[a-fA-F0-9]+
)pubkey
:string
(regex0x[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.