Skip to main content

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. Over TCP, an operator MAY also accept merging data v2, a compact form of the merging data. 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.

HeaderRequiredValueIf absent
Content-TypeNoapplication/octet-stream for SSZThe operator decodes the body as JSON
Content-EncodingNogzip or zstdThe operator reads the body uncompressed
x-api-keyNoAPI keyThe submission is accepted on its signature. An operator MAY require a key for encodings it offers
x-merge-typeNomergeable or append_onlyThe submission does not take part in MPBC. Parsing is case-insensitive, and an unparseable value is treated the same way
x-sequenceNou32See Slot state

Responses​

StatusMeaning
200The 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
400The submission did not decode, failed validation, or arrived out of sequence
401The API key is absent or not recognised
408Block simulation timed out
413The decompressed body exceeded the operator's size limit
500The 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​

Merging data comes in two versions. The v1 BlockMergingData is accepted over HTTP and TCP. The v2 BlockMergingDataV2 is a compact form accepted over TCP only, selected by the MERGING_V2 flag. An operator converts v1 into v2 on arrival, so both versions are merged identically.

A builder MUST only submit blocks and transactions that comply with the proposer's preferences (e.g. filtering). Operators validate all blocks against these preferences.

Merging data v1​

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. A bundle's txs MUST be consecutive ascending block indices. An operator MUST preserve flag bits it does not recognise.

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.

SelectorCarriesNotes
0TransactionOrder
1BundleOrder with the flags field omittedDeprecated. An operator MUST accept it and read it as no flags set. A builder SHOULD NOT emit it
2BundleOrder

Merging data v2​

BlockMergingDataV2 describes the same orders as v1 in a fixed-width form that takes a fraction of the bytes. Bundles are defined as ranges of consecutive block transactions, and individual transactions are defined as bundles of length one.

pub struct BlockMergingDataV2 {
/// 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 orders: Vec<OrderV2>,
/// Per-transaction codes, only for orders whose transactions differ.
pub tx_codes: Vec<OrderTxCodes>,
}

pub struct OrderV2 {
/// Block index of the first transaction.
pub start: u16,
/// Number of transactions. At least one.
pub len: u8,
/// LATEST_ONLY = 1 << 0, ALL_REVERT = 1 << 1
pub flags: u8,
}

pub struct OrderTxCodes {
/// Index into `orders`.
pub order: u16,
/// One 2-bit code per transaction in the bundle, packed four per byte
/// from the lowest bits up. 0 must succeed, 1 may revert, 2 may be dropped.
pub codes: Vec<u8>,
}

LATEST_ONLY keeps its meaning from Slot state. ALL_REVERT allows every transaction in the bundle to revert, and replaces can_revert. A bundle in which only some transactions may revert or be dropped instead carries an OrderTxCodes entry, which assigns each of its transactions one of three codes: must succeed, may revert, or may be dropped. Such a bundle MUST NOT set ALL_REVERT. A bundle with neither is only merged if every transaction succeeds.

An operator skips a bundle that is empty, extends past the end of the block, sets a flag it does not recognise, sets ALL_REVERT and also carries codes, or carries a codes entry of the wrong length or with an undefined code.

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.

OutcomeWhat it means
RejectedThe operator responds with an error status. The bid does not enter the auction.
Block forfeits MPBCThe bid enters the auction. The block contributes no orders and cannot serve as a base.
Order skippedThe bid enters the auction and the block merges. One order is not applied.
Order revokedA 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, and for each mergeable transaction, 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,
}

bitflags! {
pub struct BidSubmissionFlags: u8 {
/// The body is a dehydrated submission.
const IS_DEHYDRATED = 1 << 0;
/// The body is zstd compressed.
const ZSTD_COMPRESSED = 1 << 2;
/// The merging data is `BlockMergingDataV2`.
const MERGING_V2 = 1 << 5;
}
}

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.

MERGING_V2 selects merging data v2. It requires merge type mergeable, and an operator rejects any other merge type with InvalidRequest.

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:

HeaderPurpose
x-api-keyIdentifies the proposer with a UUID, sent on the websocket handshake. Required; operators admit streams on this key.
x-timeout-msHow long the client gives the operator to answer. Required. Missing, unparseable or zero returns 401.
date-millisecondsWhen 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:

FieldMeaning
slot, block_numberThe slot and block
original_block_hash, original_valueThe base block hash and what it paid the proposer
block_hash, proposer_valueThe MPBC block hash and what it paid the proposer.
total_merged_valueValue MPBC added.
base_builder_revenue, relay_revenueThe base builder's and the operator's revenue.
original_tx_count, merged_tx_countTransaction counts before and after MPBC.
original_blob_count, merged_blob_countBlob counts before and after MPBC.
original_gas_used, merged_gas_usedGas used before and after MPBC.
builder_inclusionsKeyed 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.

MPBC transactions​

GET /relay/v1/data/merged_txs?slot=<slot>&tx_hash=<hash>

Returns how the operator handled one transaction for the block it delivered in the slot:

FieldMeaning
slot, tx_hashThe slot and transaction queried.
block_hashThe MPBC block the transaction was merged into.
base_block_hashThe base block of the delivered block, against which the transaction was considered.
mergedWhether the operator merged the transaction into the delivered block.
reasonIf the transaction was not merged, the reason for why this is the case.
ReasonMeaning
zero_paymentThe transaction or bundle does not increase the base builder's coinbase balance on simulation.
out_of_spaceThe block's gas or blob budget was full when the transaction was considered.
replacedThe builder's later submission omitted the transaction, cancelling it.
invalidThe transaction failed simulation against the block's state.
duplicateThe transaction was already in the base block.

Only transactions made available for merging are in scope, including those later excluded. Data exists only for the block the operator delivered in the slot, and operators retain it for at least 7 days. Both parameters are required. A transaction that was not made available for merging, or a slot in which the operator delivered no block, returns 404.