Specifications
MPBC defines four interfaces that allow builders to submit blocks and merging preferences to an operator, operators to share information between themselves, proposers to receive bids, and any stakeholder to query an operator for merging data. MPBC does not prescribe a specific implementation beyond these interfaces, and operators are encouraged to run performant and uncorrelated proprietary implementations.
Builder to operator
A builder submits blocks to an operator and marks which of its blocks and transactions may take part in MPBC.
An operator MUST implement the HTTP submission endpoint, the merge types, the merging data schema, the order semantics, and the response model. A builder that implements this section MUST be able to submit to any operator without per-operator code.
An operator MAY offer the TCP transport. If it does, the transport MUST behave as described under TCP transport. An operator MAY accept encodings beyond those required here.
Limits, ports, and timeouts are operator-local. A builder MUST NOT hardcode them. An operator MUST publish the limits it enforces.
Submitting a block
Endpoint
POST /relay/v1/builder/blocks
Headers
The table lists the default behavior if any header is absent.
| Header | Required | Value | If absent |
|---|---|---|---|
Content-Type | No | application/octet-stream for SSZ | The operator decodes the body as JSON |
Content-Encoding | No | gzip or zstd | The operator reads the body uncompressed |
x-api-key | No | API key | The submission is accepted on its signature. An operator MAY require a key for encodings it offers |
x-merge-type | No | mergeable or append_only | The submission does not take part in MPBC. Parsing is case-insensitive, and an unparseable value is treated the same way |
x-sequence | No | u32 | See Slot state |
Responses
| Status | Meaning |
|---|---|
200 | The operator accepted the submission into its auction, with an empty response body. Acceptance does not mean the block was merged, and merging outcomes appear through Verification instead |
400 | The submission did not decode, failed validation, or arrived out of sequence |
401 | The API key is absent or not recognised |
408 | Block simulation timed out |
413 | The decompressed body exceeded the operator's size limit |
500 | The operator failed internally, or exceeded its own processing deadline |
Body
A submission is the block a builder is bidding with, in the same shape the relay builder API already uses.
pub struct SignedBidSubmission {
pub message: BidTrace,
pub execution_payload: ExecutionPayload,
pub blobs_bundle: BlobsBundle,
pub execution_requests: ExecutionRequests,
pub signature: BlsSignatureBytes,
}
MPBC adds one field. A submission that takes part in MPBC carries its merging data alongside the block.
pub struct SignedBidSubmissionWithMergingData {
pub submission: SignedBidSubmission,
pub merging_data: BlockMergingData,
}
These are the two levels of configuration. The x-merge-type header applies to the whole submission, and merging_data.merge_orders selects transactions within it.
Merge types
Every submission has a merge type, which is none unless the builder sets another. It decides what an operator may do with the block: use it as a base, take orders out of it, both, or neither.
Opting in is per submission. Registering reserves makes a builder eligible for MPBC but a block takes part only when its submission sets mergeable or append_only.
pub enum MergeType {
None = 0, // this submission does not take part in MPBC
Mergeable = 1, // the body carries merging data: base eligibility, payout address, orders
AppendOnly = 2, // the block is a merge base and contributes no orders
}
The merge type header only applies to its current submission. A later mergeable submission does not make earlier ones mergeable, and a later none does not withdraw blocks already sent.
The block in an append_only submission is eligible as a merge base, and its payout address is the block's fee recipient.
A submission whose merge type is mergeable MUST carry merging data. A submission whose merge type is none or append_only MUST NOT carry it. An operator that receives a mismatched pair MUST respond 400.
Merging data
pub struct BlockMergingData {
/// Whether this block may be used as a merge base.
pub allow_appending: bool,
/// The address credited for orders merged out of this block.
pub builder_address: Address,
/// What may be merged out of it. May be empty.
pub merge_orders: Vec<Order>,
}
pub enum Order {
Tx(TransactionOrder),
Bundle(BundleOrder),
}
pub struct TransactionOrder {
/// Index into the submitted block's transactions.
pub index: usize,
/// Whether the transaction may revert.
pub can_revert: bool,
}
pub struct BundleOrder {
/// Block transaction indices, in bundle order. At least one.
pub txs: TxIndices,
/// Indices into `txs` that may revert.
pub reverting_txs: TxIndices,
/// Indices into `txs` that may be omitted, but not revert.
pub dropping_txs: TxIndices,
pub flags: MergeOrderFlags,
}
bitflags! {
pub struct MergeOrderFlags: u64 {
/// The order is eligible only while it is in the builder's latest
/// submission, allowing cancels.
const LATEST_ONLY = 1 << 0;
}
}
Note that reverting_txs and dropping_txs index into txs, not into the block. Every index MUST be in range for the array it refers to. An operator MUST preserve flag bits it does not recognise.
Encodings
Over JSON, MergeOrderFlags is a string of flag names rather than a number. LATEST_ONLY is the only name defined, and "" sets no flags. An integer such as 1 is invalid, and an operator rejects the submission.
{ "txs": [45, 46, 47], "reverting_txs": [2], "dropping_txs": [], "flags": "LATEST_ONLY" }
Over SSZ, Order is a union. A builder emits selector 0 for a transaction and selector 2 for a bundle.
| Selector | Carries | Notes |
|---|---|---|
0 | TransactionOrder | |
1 | BundleOrder with the flags field omitted | Deprecated. An operator MUST accept it and read it as no flags set. A builder SHOULD NOT emit it |
2 | BundleOrder |
Slot state
An operator keeps state for a builder across the submissions it sends for one slot.
LATEST_ONLY is how a builder cancels an order. An order carrying the flag stays eligible only while the builder keeps including that order in later submissions. The first submission that omits the order cancels it, and an operator will not merge it after that. A submission that carries no orders at all cancels every LATEST_ONLY order the builder has standing for the slot.
x-sequence orders a builder's submissions against each other. An operator MUST reject a submission whose sequence number is not greater than the highest it has seen from that builder for the slot. When either of two submissions carries no sequence number, an operator orders them by arrival instead, so a builder that omits the header on any submission loses control of ordering for the slot.
Resubmitting a block hash does not amend the orders an operator already holds for it. A builder that wants a different order set submits a different block.
Outcomes
An order or a submission can fail in four ways.
| Outcome | What it means |
|---|---|
| Rejected | The operator responds with an error status. The bid does not enter the auction. |
| Block forfeits MPBC | The bid enters the auction. The block contributes no orders and cannot serve as a base. |
| Order skipped | The bid enters the auction and the block merges. One order is not applied. |
| Order revoked | A previously eligible LATEST_ONLY order is withdrawn by a later submission. |
An operator MUST NOT let a malformed order remove a submission's own bid from the auction. An operator SHOULD report the outcome for each merged block through the Verification interface.
TCP transport
An operator MAY accept submissions over a TCP connection instead of HTTP. Where it does, the connection carries the same submissions with a different framing, and the operator MUST apply the same rules to them.
Each message sits inside a frame of a little-endian u32 length and a little-endian u64 send timestamp. The first message on a connection registers the builder, and every message after it is a submission.
/// First message on a new TCP connection.
pub struct RegistrationMsg {
pub api_key: [u8; 16],
pub builder_pubkey: BlsPublicKey,
}
/// `[4B big-endian seq_number][1B merge_type][1B flags]`
pub struct BidSubmissionHeader {
pub sequence_number: u32,
pub merge_type: MergeType,
pub flags: BidSubmissionFlags,
}
Every field is positional, so all three are always present. sequence_number carries what x-sequence carries over HTTP, and a builder that has no ordering to express sends 0. merge_type carries the same values as x-merge-type, and an operator MUST reject a byte it does not recognise rather than defaulting it. flags selects the body's encoding, and an operator MUST preserve bits it does not recognise.
Registration binds the connection to one builder key. An operator MAY skip signature verification on a registered connection, so a builder MUST treat the API key as the credential that authorises submissions on it.
TCP bodies are SSZ. An operator MUST accept uncompressed bodies and MAY accept zstd.
An operator responds to each submission with one frame.
pub struct BidSubmissionResponse {
pub sequence_number: u32,
pub request_id: [u8; 16],
pub status: Status,
pub error_msg: Vec<u8>,
}
pub enum Status {
Okay,
InvalidRequest,
InternalError,
}
An operator returns InternalError for its own failures and InvalidRequest for everything else, including a submission that arrives out of sequence.
Operator to operator
Operators use this interface to exchange execution payloads and information on builder demotions, and will be able to exchange proposer constraints.
Transport
Operators broadcast messages over libp2p gossipsub on QUIC, with messages signed by the sending operator's key. Each operator is configured with the other operators' public keys and addresses and connects to them directly.
Demotions and collateral
Operators exchange builder demotions and re-promotions, so a builder that one operator restricts is restricted at all of them. Joint enforcement is what allows a builder to share collateral across operators, increasing capital efficiency.
pub enum OperatorMessage {
Demotion(Demotion),
Promotion(Promotion),
Collateral(BuilderCollateral),
Payload(Payload),
}
pub struct Demotion {
pub ts_ms: u64,
pub slot: u64,
pub builder_pubkey: BlsPublicKeyBytes,
pub block_hash: B256,
pub reason_msg: Vec<u8>,
}
When a peer subscribes to the topic, each operator republishes its current demotion and promotion state, and its collateral messages, to the topic. Every subscriber receives the replay, not just the new peer.
BuilderCollateral carries an operator_group. Operator instances in the same group report the same collateral rather than separate amounts, so a receiver deduplicates by group instead of summing.
Proposer to operator
Proposers ask for headers over the relay interface. MPBC operators can additionally stream bids to improve delivery:
GET /eth/v1/builder/header_stream/{slot}/{parent_hash}/{pubkey}
The connection is handled over a websocket. Frames are binary: a kind byte, a fork byte, then the SSZ-encoded bid. Proposers connect using the following request headers:
| Header | Purpose |
|---|---|
x-api-key | Identifies the proposer with a UUID, sent on the websocket handshake. Required; operators admit streams on this key. |
x-timeout-ms | How long the client gives the operator to answer. Required. Missing, unparseable or zero returns 401. |
date-milliseconds | When the client started the request. |
The operator streams in a window that ends at the client's deadline, calculable from x-timeout-ms and date-milliseconds. The window is 300ms by default, capped at the 3200ms header-request cutoff. The socket upgrades immediately and stays silent until the window opens. Inside the window the operator pushes an update at most every 5ms and suppresses identical consecutive bids.
Verification
Operators publish MPBC metadata after the slot has elapsed. This delay is necessary to avoid leaking information which could adversely impact originators.
MPBC blocks
GET /relay/v1/data/merged_blocks?slot=<slot>
Returns one entry per MPBC block the operator built for the slot:
| Field | Meaning |
|---|---|
slot, block_number | The slot and block |
original_block_hash, original_value | The base block hash and what it paid the proposer |
block_hash, proposer_value | The MPBC block hash and what it paid the proposer. |
total_merged_value | Value MPBC added. |
base_builder_revenue, relay_revenue | The base builder's and the operator's revenue. |
original_tx_count, merged_tx_count | Transaction counts before and after MPBC. |
original_blob_count, merged_blob_count | Blob counts before and after MPBC. |
original_gas_used, merged_gas_used | Gas used before and after MPBC. |
builder_inclusions | Keyed by each contributing builder's builder_address. contribution is the value merged in, revenue is that net of payout gas, txs are the transaction hashes. |
total_merged_value, base_builder_revenue, relay_revenue, original_gas_used and merged_gas_used may be null on older blocks. Every integer is a quoted string except the two gas fields, which are bare JSON numbers.
Payout gas is the gas the operator spends executing the transaction that pays contributing builders.
A slot that did not benefit from merging returns an empty array.