Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Constraints API v0 #49

Merged
merged 34 commits into from
Nov 29, 2024
Merged
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
22a13a8
feat: constraints-api-v0
thedevbirb Nov 6, 2024
f2f2fcf
docs(constraints-api): fix broken links
thedevbirb Nov 7, 2024
dc2919d
fix(constraints-api-v0): drop validator_delegations migration, curren…
thedevbirb Nov 8, 2024
40cda8e
feat(constraints-api): add startup configuration
thedevbirb Nov 8, 2024
38c8e8f
Merge pull request #22 from chainbound/lore/feat/constraints-api-config
merklefruit Nov 8, 2024
ffc5e5e
introduce max_block_value_to_verify in constraints config
gd-0 Nov 11, 2024
61d611f
address PR comments + other fixes
gd-0 Nov 11, 2024
b531d46
feat: import website from aestus upstream, solve conflicts
merklefruit Nov 11, 2024
b66e136
chore: re-added cargo.lock
merklefruit Nov 11, 2024
9d7e97f
chore: run fmt clippy checks
merklefruit Nov 11, 2024
f995522
chore: default run on unspecified ip
merklefruit Nov 11, 2024
30c5cb5
chore: rm aestus naming and links from web page
merklefruit Nov 11, 2024
8ba703c
chore: gattaca logo
merklefruit Nov 11, 2024
25d777e
chore: reverted changes to dockerfile + tls dependencies
merklefruit Nov 11, 2024
c864a9e
chore: addressed review
merklefruit Nov 11, 2024
9ee178a
chore: logo size
merklefruit Nov 11, 2024
ea15474
fix(tests): make them compile
thedevbirb Nov 12, 2024
ae59749
chore(api): should verify proofs logic
thedevbirb Nov 12, 2024
00b5bcb
Merge pull request #23 from gd-0/dont-enforce-constraints-if-block-va…
thedevbirb Nov 12, 2024
9003727
chore(website): fmt
thedevbirb Nov 12, 2024
0aee3e8
Merge pull request #25 from chainbound/feat/import-website
thedevbirb Nov 12, 2024
30c66ac
fix(db): retry creating pool handle
merklefruit Nov 12, 2024
7a0342f
fix(db): don't panic upon failing migrations once
merklefruit Nov 12, 2024
44b1823
fix(website): rm blob related fields from table
merklefruit Nov 12, 2024
f778527
Merge pull request #26 from chainbound/fix/retry-db-pool
thedevbirb Nov 12, 2024
b78427d
fix(ssz): restore SSZ backwards compatibility on SignedBuilderSubmiss…
thedevbirb Nov 15, 2024
cdb2795
Merge pull request #27 from chainbound/lore/fix/ssz
merklefruit Nov 18, 2024
ca4d329
fix(ssz): order of enum variants is important
thedevbirb Nov 26, 2024
dd97cb2
refactor(api/builder): submit_block(_with_proofs) now have the same impl
thedevbirb Nov 15, 2024
bd2b542
Merge pull request #28 from chainbound/lore/refactor/constraints-api
thedevbirb Nov 27, 2024
f81a92e
fix(constraints-api): don't read from db for checking proposer duties…
thedevbirb Nov 27, 2024
242607c
Merge pull request #30 from chainbound/lore/fix/db-read-constraints-api
thedevbirb Nov 28, 2024
5848eec
fix(constraints-api): review constraints saving logic
thedevbirb Nov 27, 2024
abf32f5
Merge pull request #29 from chainbound/lore/fix/submit-constraints-logic
thedevbirb Nov 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor(api/builder): submit_block(_with_proofs) now have the same impl
thedevbirb committed Nov 27, 2024
commit dd97cb20bb2b9e5baee12b1ebddeb9876fad4fb4
345 changes: 64 additions & 281 deletions crates/api/src/builder/api.rs
Original file line number Diff line number Diff line change
@@ -50,7 +50,9 @@ use helix_common::{
BidSubmission, BidTrace, SignedBidSubmission,
},
chain_info::ChainInfo,
proofs::{verify_multiproofs, InclusionProofs, SignedConstraints},
proofs::{
verify_multiproofs, InclusionProofs, SignedConstraints, SignedConstraintsWithProofData,
},
signing::RelaySigningContext,
simulator::BlockSimError,
versioned_payload::PayloadAndBlobs,
@@ -299,6 +301,7 @@ where
/// 6. Saves the bid to auctioneer and db.
///
/// Implements this API: <https://flashbots.github.io/relay-specs/#/Builder/submitBlock>
/// Implements this API: <https://docs.boltprotocol.xyz/technical-docs/api/relay#blocks_with_proofs>
pub async fn submit_block(
Extension(api): Extension<Arc<BuilderApi<A, DB, S, G>>>,
req: Request<Body>,
@@ -448,253 +451,36 @@ where
)
.await?;

// If cancellations are enabled, then abort now if there is a later submission
if is_cancellations_enabled {
if let Err(err) =
api.check_for_later_submissions(&payload, trace.receive, &request_id).await
{
warn!(request_id = %request_id, error = %err, "already processing later submission");
return Err(err)
}
}

// Save bid to auctioneer
match api
.save_bid_to_auctioneer(
&payload,
&mut trace,
is_cancellations_enabled,
floor_bid_value,
&request_id,
)
.await?
{
// If the bid was succesfully saved then we gossip the header and payload to all other
// relays.
Some((builder_bid, execution_payload)) => {
api.gossip_new_submission(
&payload,
execution_payload,
builder_bid,
is_cancellations_enabled,
trace.receive,
&request_id,
)
.await;
}
None => { /* Bid wasn't saved so no need to gossip as it will never be served */ }
}

// Log some final info
trace.request_finish = get_nanos_timestamp()?;
info!(
request_id = %request_id,
trace = ?trace,
request_duration_ns = trace.request_finish.saturating_sub(trace.receive),
"submit_block request finished"
);

let optimistic_version = if was_simulated_optimistically {
OptimisticVersion::V1
} else {
OptimisticVersion::NotOptimistic
};

// Save submission to db.
tokio::spawn(async move {
if let Err(err) = api
.db
.store_block_submission(payload, Arc::new(trace), optimistic_version as i16)
.await
{
error!(
error = %err,
"failed to store block submission",
)
}
});

Ok(StatusCode::OK)
}

/// Handles the submission of a new block with inclusion proofs.
///
/// This function extends the `submit_block` functionality to also handle inclusion proofs:
/// 1. Receives the request and decodes the payload into a `SignedBidSubmission` object.
/// 2. Validates the builder and checks against the next proposer duty.
/// 3. Verifies the signature of the payload.
/// 4. Fetches the constraints for the slot and verifies the inclusion proofs.
/// 5. Runs further validations against the auctioneer.
/// 6. Simulates the block to validate the payment.
/// 7. Saves the bid and inclusion proof to the auctioneer.
///
/// Implements this API: <https://docs.boltprotocol.xyz/technical-docs/api/relay#blocks_with_proofs>
pub async fn submit_block_with_proofs(
Extension(api): Extension<Arc<BuilderApi<A, DB, S, G>>>,
req: Request<Body>,
) -> Result<StatusCode, BuilderApiError> {
let request_id = Uuid::new_v4();
let mut trace = SubmissionTrace { receive: get_nanos_timestamp()?, ..Default::default() };
let (head_slot, next_duty) = api.curr_slot_info.read().await.clone();

info!(
request_id = %request_id,
event = "submit_block_with_proofs",
head_slot = head_slot,
timestamp_request_start = trace.receive,
);

// Decode the incoming request body into a payload with proofs
let (payload, is_cancellations_enabled) =
decode_payload(req, &mut trace, &request_id).await?;
let block_hash = payload.message().block_hash.clone();

// Verify that we have a validator connected for this slot
if next_duty.is_none() {
warn!(request_id = %request_id, "could not find slot duty");
return Err(BuilderApiError::ProposerDutyNotFound)
}
let next_duty = next_duty.unwrap();

debug!(
request_id = %request_id,
builder_pub_key = ?payload.builder_public_key(),
block_value = %payload.value(),
block_hash = ?block_hash,
"submit_block_with_proofs -- payload decoded",
);

// Verify the payload is for the current slot
if payload.slot() <= head_slot {
warn!(
request_id = %request_id,
"submission is for a past slot",
);
return Err(BuilderApiError::SubmissionForPastSlot {
current_slot: head_slot,
submission_slot: payload.slot(),
})
}

// Fetch the next payload attributes and validate basic information
let payload_attributes = api
.fetch_payload_attributes(payload.slot(), payload.parent_hash(), &request_id)
.await?;

// Handle duplicates.
if let Err(err) = api
.check_for_duplicate_block_hash(
&block_hash,
payload.slot(),
payload.parent_hash(),
payload.proposer_public_key(),
&request_id,
)
.await
{
match err {
BuilderApiError::DuplicateBlockHash { block_hash } => {
// We dont return the error here as we want to continue processing the request.
// This mitigates the risk of someone sending an invalid payload
// with a valid header, which would block subsequent submissions with the same
// header and valid payload.
debug!(
request_id = %request_id,
block_hash = ?block_hash,
builder_pub_key = ?payload.builder_public_key(),
"block hash already seen"
);
}
_ => return Err(err),
}
}

// Verify the payload value is above the floor bid
let floor_bid_value = api
.check_if_bid_is_below_floor(
payload.slot(),
payload.parent_hash(),
payload.proposer_public_key(),
payload.builder_public_key(),
payload.value(),
is_cancellations_enabled,
&request_id,
)
.await?;
trace.floor_bid_checks = get_nanos_timestamp()?;

// Fetch builder info
let builder_info = api.fetch_builder_info(payload.builder_public_key()).await;

// Handle trusted builders check
if !api.check_if_trusted_builder(&next_duty, &builder_info).await {
let proposer_trusted_builders = next_duty.entry.preferences.trusted_builders.unwrap();
warn!(
request_id = %request_id,
builder_pub_key = ?payload.builder_public_key(),
proposer_trusted_builders = ?proposer_trusted_builders,
"builder not in proposer trusted builders list",
);
return Err(BuilderApiError::BuilderNotInProposersTrustedList {
proposer_trusted_builders,
})
}

// Verify payload has not already been delivered
match api.auctioneer.get_last_slot_delivered().await {
Ok(Some(slot)) => {
if payload.slot() <= slot {
warn!(request_id = %request_id, "payload already delivered");
return Err(BuilderApiError::PayloadAlreadyDelivered)
// If constraints from the [Constraints API](https://docs.boltprotocol.xyz/technical-docs/api/relay#blocks_with_proofs)
// are available, verify inclusion proofs and save them to cache
//
// NOTE: this check must always be performed because otherwise a builder might trick
// the relay into accepting as best bid a block without invalid inclusion proofs when they
// are needed.
if let Some(constraints) = api.auctioneer.get_constraints(payload.slot()).await? {
let should_verify_and_save_proofs = api
.relay_config
.constraints_api_config
.max_block_value_to_verify_wei
.map_or(true, |max_block_value_to_verify| {
payload.value() <= max_block_value_to_verify
});
if should_verify_and_save_proofs {
if let Err(err) =
api.verify_and_save_inclusion_proofs(&payload, constraints, &request_id).await
{
warn!(request_id = %request_id, error = %err, "failed to verify and save inclusion proofs");
return Err(err)
}
}
Ok(None) => {}
Err(err) => {
error!(request_id = %request_id, error = %err, "failed to get last slot delivered");
}
}

// Sanity check the payload
if let Err(err) = sanity_check_block_submission(
&payload,
payload.bid_trace(),
&next_duty,
&payload_attributes,
&api.chain_info,
) {
warn!(request_id = %request_id, error = %err, "failed sanity check");
return Err(err)
}
trace.pre_checks = get_nanos_timestamp()?;

let (payload, was_simulated_optimistically) = api
.verify_submitted_block(
payload,
next_duty,
&builder_info,
&mut trace,
&request_id,
&payload_attributes,
)
.await?;

// Fetch constraints, and if available verify inclusion proofs and save them to cache
let should_verify_and_save_proofs = api
.relay_config
.constraints_api_config
.max_block_value_to_verify_wei
.map_or(true, |max_block_value_to_verify| payload.value() <= max_block_value_to_verify);
if should_verify_and_save_proofs {
if let Err(err) = api.verify_and_save_inclusion_proofs(&payload, &request_id).await {
warn!(request_id = %request_id, error = %err, "failed to verify and save inclusion proofs");
return Err(err)
} else {
info!(
request_id = %request_id,
block_value = %payload.value(),
"block value is greater than max value to verify, inclusion proof verification and saving is skipped",
);
}
} else {
info!(
request_id = %request_id,
block_value = %payload.value(),
"block value is greater than max value to verify, inclusion proof verification and saving is skipped",
);
info!(%request_id, "no constraints found for slot, proof verification is not needed");
}

// If cancellations are enabled, then abort now if there is a later submission
@@ -740,7 +526,7 @@ where
request_id = %request_id,
trace = ?trace,
request_duration_ns = trace.request_finish.saturating_sub(trace.receive),
"submit_block_with_proofs request finished"
"submit_block request finished"
);

let optimistic_version = if was_simulated_optimistically {
@@ -758,7 +544,7 @@ where
{
error!(
error = %err,
"failed to store block submission with proofs",
"failed to store block submission",
)
}
});
@@ -2183,43 +1969,40 @@ where
async fn verify_and_save_inclusion_proofs(
&self,
payload: &SignedBidSubmission,
constraints: Vec<SignedConstraintsWithProofData>,
request_id: &Uuid,
) -> Result<(), BuilderApiError> {
if let Some(constraints) = self.auctioneer.get_constraints(payload.slot()).await? {
let transactions_root: B256 = payload
.transactions()
.clone()
.hash_tree_root()?
.to_vec()
.as_slice()
.try_into()
.map_err(|error| {
error!(?error, "failed to convert root to hash32");
BuilderApiError::InternalError
})?;
let proofs = payload.proofs().ok_or(BuilderApiError::InclusionProofsNotFound)?;
let constraints_proofs: Vec<_> = constraints.iter().map(|c| &c.proof_data).collect();

verify_multiproofs(constraints_proofs.as_slice(), proofs, transactions_root).map_err(
|e| {
error!(error = %e, "failed to verify inclusion proofs");
BuilderApiError::InclusionProofVerificationFailed(e)
},
)?;
let transactions_root: B256 = payload
.transactions()
.clone()
.hash_tree_root()?
.to_vec()
.as_slice()
.try_into()
.map_err(|error| {
error!(?error, "failed to convert root to hash32");
BuilderApiError::InternalError
})?;
let proofs = payload.proofs().ok_or(BuilderApiError::InclusionProofsNotFound)?;
let constraints_proofs: Vec<_> = constraints.iter().map(|c| &c.proof_data).collect();

// Save inclusion proof to auctioneer.
self.save_inclusion_proof(
payload.slot(),
payload.proposer_public_key(),
payload.block_hash(),
proofs,
request_id,
)
.await?;
info!(%request_id, "inclusion proofs verified and saved to auctioneer");
} else {
info!(%request_id, "no constraints found for slot, proof verification is not needed");
};
verify_multiproofs(constraints_proofs.as_slice(), proofs, transactions_root).map_err(
|e| {
error!(error = %e, "failed to verify inclusion proofs");
BuilderApiError::InclusionProofVerificationFailed(e)
},
)?;

// Save inclusion proof to auctioneer.
self.save_inclusion_proof(
payload.slot(),
payload.proposer_public_key(),
payload.block_hash(),
proofs,
request_id,
)
.await?;
info!(%request_id, "inclusion proofs verified and saved to auctioneer");
Ok(())
}
}
Loading