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(builder-api): delegations getter fn #5

Merged
merged 4 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
53 changes: 47 additions & 6 deletions crates/api/src/builder/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use helix_common::{
BidSubmission, BidTrace, SignedBidSubmission,
}, chain_info::ChainInfo, proofs::{self, verify_multiproofs, ConstraintsWithProofData, InclusionProofs, SignedConstraints, SignedConstraintsWithProofData}, signing::RelaySigningContext, simulator::BlockSimError, versioned_payload::PayloadAndBlobs, BuilderInfo, GossipedHeaderTrace, GossipedPayloadTrace, HeaderSubmissionTrace, SignedBuilderBid, SubmissionTrace
};
use helix_database::DatabaseService;
use helix_database::{error::DatabaseError, DatabaseService};
use helix_datastore::{types::SaveBidAndUpdateTopBidResponse, Auctioneer};
use helix_housekeeper::{ChainUpdate, PayloadAttributesUpdate, SlotUpdate};
use helix_utils::{get_payload_attributes_key, has_reached_fork, try_decode_into};
Expand Down Expand Up @@ -157,6 +157,8 @@ where
}
}

/// This endpoint returns a list of signed constraints for a given `slot`.
///
/// Implements this API: <https://chainbound.github.io/bolt-docs/api/relay#constraints>
pub async fn constraints(
Extension(api): Extension<Arc<BuilderApi<A, DB, S, G>>>,
Expand All @@ -177,10 +179,10 @@ where
.map(|data| data.signed_constraints)
.collect::<Vec<SignedConstraints>>();

Ok(Json(constraints))
Ok(Json(constraints).into_response())
}
Ok(None) => {
Ok(Json(vec![])) // Return an empty vector if no constraints are found
Ok(StatusCode::NO_CONTENT.into_response())
merklefruit marked this conversation as resolved.
Show resolved Hide resolved
}
Err(err) => {
warn!(error=%err, "Failed to get constraints");
Expand All @@ -189,6 +191,8 @@ where
}
}

/// This endpoint returns a stream of signed constraints for a given `slot`.
///
/// Implements this API: <https://chainbound.github.io/bolt-docs/api/relay#constraints-stream>
pub async fn constraints_stream(
Extension(api): Extension<Arc<BuilderApi<A, DB, S, G>>>,
Expand Down Expand Up @@ -216,6 +220,43 @@ where
Sse::new(filtered).keep_alive(KeepAlive::default())
}

/// This endpoint returns the active delegations for the validator scheduled to propose
/// at the provided `slot`. The delegations are returned as a list of BLS pubkeys.
///
/// Implements this API: <https://chainbound.github.io/bolt-docs/api/relay#delegations>
pub async fn delegations(
Extension(api): Extension<Arc<BuilderApi<A, DB, S, G>>>,
Query(slot): Query<SlotQuery>,
) -> Result<impl IntoResponse, BuilderApiError> {
let slot = slot.slot;
let duty_bytes = api.proposer_duties_response.read().await.clone();
let proposer_duties: Vec<BuilderGetValidatorsResponse> = serde_json::from_slice(&duty_bytes.unwrap()).unwrap();

let duty = proposer_duties
.iter()
.find(|duty| duty.slot == slot)
.ok_or(BuilderApiError::ProposerDutyNotFound)?;

let pubkey = duty.entry.message.public_key.clone();

match api.db.get_validator_delegations(pubkey).await {
Ok(delegations) => Ok(Json(delegations).into_response()),

Err(err) => {
match err {
DatabaseError::ValidatorDelegationNotFound => {
warn!("No delegations found for validator");
Ok(StatusCode::NO_CONTENT.into_response())
}
_ => {
warn!(error=%err, "Failed to get delegations");
Err(BuilderApiError::DatabaseError(err))
}
}
}
}
}

/// Handles the submission of a new block by performing various checks and verifications
/// before saving the submission to the auctioneer.
///
Expand Down Expand Up @@ -440,10 +481,10 @@ where
/// 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 and handles inclusion proofs.
/// 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 proofs to the auctioneer and db.
/// 7. Saves the bid and inclusion proof to the auctioneer.
///
/// Implements this API: <https://chainbound.github.io/bolt-docs/api/relay#blocks_with_proofs>
pub async fn submit_block_with_proofs(
Expand Down Expand Up @@ -637,7 +678,7 @@ where
None => { /* Bid wasn't saved so no need to gossip as it will never be served */ }
}

// Save inclusion proofs to auctioneer.
// Save inclusion proof to auctioneer.
if let Err(err) = api.save_inclusion_proof(
payload.slot(),
payload.proposer_public_key(),
Expand Down
7 changes: 7 additions & 0 deletions crates/api/src/builder/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use ethereum_consensus::{
ssz::{self, prelude::*},
};
use helix_common::simulator::BlockSimError;
use helix_database::error::DatabaseError;
use helix_datastore::error::AuctioneerError;

#[derive(Debug, thiserror::Error)]
Expand Down Expand Up @@ -107,6 +108,9 @@ pub enum BuilderApiError {
#[error("datastore error: {0}")]
AuctioneerError(#[from] AuctioneerError),

#[error("database error: {0}")]
DatabaseError(#[from] DatabaseError),

#[error("incorrect prev_randao - got: {got:?}, expected: {expected:?}")]
PrevRandaoMismatch { got: Bytes32, expected: Bytes32 },

Expand Down Expand Up @@ -234,6 +238,9 @@ impl IntoResponse for BuilderApiError {
BuilderApiError::AuctioneerError(err) => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("Auctioneer error: {err}")).into_response()
},
BuilderApiError::DatabaseError(err) => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("Database error: {err}")).into_response()
},
BuilderApiError::FeeRecipientMismatch { got, expected } => {
(StatusCode::BAD_REQUEST, format!("Fee recipient mismatch. got: {got:?}, expected: {expected:?}")).into_response()
},
Expand Down
48 changes: 47 additions & 1 deletion crates/api/src/constraints/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ mod tests {
const SUBMISSION_SLOT: u64 = HEAD_SLOT + 1;
const SUBMISSION_TIMESTAMP: u64 = 1606824419;
const VALIDATOR_INDEX: usize = 1;
const PUB_KEY: &str = "0x84e975405f8691ad7118527ee9ee4ed2e4e8bae973f6e29aa9ca9ee4aea83605ae3536d22acc9aa1af0545064eacf82e";
const PUB_KEY: &str = "0xa695ad325dfc7e1191fbc9f186f58eff42a634029731b18380ff89bf42c464a42cb8ca55b200f051f57f1e1893c68759";

// +++ HELPER FUNCTIONS +++

Expand Down Expand Up @@ -365,6 +365,52 @@ mod tests {
let _ = tx.send(());
}

#[tokio::test]
#[serial]
async fn test_get_delegations() {
tracing_subscriber::fmt::init();

// Start the server
let (tx, http_config, _constraints_api, _builder_api, mut slot_update_receiver) = start_api_server().await;

let slot_update_sender = slot_update_receiver.recv().await.unwrap();
send_dummy_slot_update(slot_update_sender.clone(), None, None, None).await;

let test_delegation: SignedDelegation = serde_json::from_str(_get_signed_delegation()).unwrap();

let req_url = format!("{}{}", http_config.base_url(), Route::DelegateSubmissionRights.path());
let req_payload = serde_json::to_vec(&test_delegation).unwrap();

// Send JSON encoded request
let resp = send_request(&req_url, Encoding::Json, req_payload).await;
assert_eq!(resp.status(), reqwest::StatusCode::OK);

// Get delegations
let slot = 33;

let req_url = format!(
"{}{}",
http_config.base_url(),
Route::GetBuilderDelegations.path()
);

let resp = reqwest::Client::new()
.get(req_url)
.query(&[("slot", slot)])
.header("accept", "application/json")
.send()
.await
.unwrap();

// Ensure the response is OK
assert_eq!(resp.status(), reqwest::StatusCode::OK);

let body: Vec<BlsPublicKey> = serde_json::from_str(&resp.text().await.unwrap()).unwrap();
assert_eq!(test_delegation.message.delegatee_pubkey, body.first().unwrap().clone());

let _ = tx.send(());
}

#[tokio::test]
#[serial]
async fn test_revoke_submission_rights_ok() {
Expand Down
13 changes: 12 additions & 1 deletion crates/api/src/proposer/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,17 @@ where
}
}

/// Retrieves the best bid header (with inclusion proof) for the specified slot, parent hash, and public key.
///
/// This function accepts a slot number, parent hash and public_key.
/// 1. Validates that the request's slot is not older than the head slot.
/// 2. Validates the request timestamp to ensure it's not too late.
/// 3. Fetches the best bid for the given parameters from the auctioneer.
/// 4. Fetches the inclusion proof for the best bid.
///
/// The function returns a JSON response containing the best bid and inclusion proofs if found.
///
/// Implements this API: <https://chainbound.github.io/bolt-docs/api/builder#get_header_with_proofs>
pub async fn get_header_with_proofs(
Extension(proposer_api): Extension<Arc<ProposerApi<A, DB, M, G>>>,
Path(GetHeaderParams { slot, parent_hash, public_key }): Path<GetHeaderParams>,
Expand Down Expand Up @@ -1146,7 +1157,7 @@ where
}
}

/// Fetches the inclusion proof for a given slot, public key, and block hash.
/// This function fetches the inclusion proof for a given slot, public key, and block hash.
async fn get_inclusion_proof(
&self,
slot: u64,
Expand Down
3 changes: 3 additions & 0 deletions crates/api/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ pub fn build_router(
Route::GetBuilderConstraintsStream => {
router = router.route(&route.path(), get(BuilderApiProd::constraints_stream));
}
Route::GetBuilderDelegations => {
router = router.route(&route.path(), get(BuilderApiProd::delegations));
}
Route::Status => {
router = router.route(&route.path(), get(ProposerApiProd::status));
}
Expand Down
4 changes: 4 additions & 0 deletions crates/api/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,10 @@ pub fn constraints_api_app() -> (
&Route::GetBuilderConstraintsStream.path(),
get(BuilderApi::<MockAuctioneer, MockDatabaseService, MockSimulator, MockGossiper>::constraints_stream),
)
.route(
&Route::GetBuilderDelegations.path(),
get(BuilderApi::<MockAuctioneer, MockDatabaseService, MockSimulator, MockGossiper>::delegations),
)
.route(
&Route::SubmitBuilderConstraints.path(),
post(ConstraintsApi::<MockAuctioneer, MockDatabaseService>::submit_constraints),
Expand Down
1 change: 1 addition & 0 deletions crates/common/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub(crate) const PATH_SUBMIT_HEADER: &str = "/headers";
pub(crate) const PATH_GET_TOP_BID: &str = "/top_bid";
pub(crate) const PATH_BUILDER_CONSTRAINTS: &str = "/constraints";
pub(crate) const PATH_BUILDER_CONSTRAINTS_STREAM: &str = "/constraints_stream";
pub(crate) const PATH_BUILDER_DELEGATIONS: &str = "/delegations";

pub(crate) const PATH_PROPOSER_API: &str = "/eth/v1/builder";

Expand Down
2 changes: 2 additions & 0 deletions crates/common/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ pub enum Route {
GetBuilderConstraints,
/// Reference: <https://chainbound.github.io/bolt-docs/api/relay#constraints_stream>
GetBuilderConstraintsStream,
GetBuilderDelegations,
/// Reference: <https://chainbound.github.io/bolt-docs/api/relay#blocks_with_proofs>
SubmitBlockWithProofs,
}
Expand All @@ -290,6 +291,7 @@ impl Route {
Route::RevokeSubmissionRights => format!("{PATH_CONSTRAINTS_API}{PATH_REVOKE_SUBMISSION_RIGHTS}"),
Route::GetBuilderConstraints => format!("{PATH_BUILDER_API}{PATH_BUILDER_CONSTRAINTS}"),
Route::GetBuilderConstraintsStream => format!("{PATH_BUILDER_API}{PATH_BUILDER_CONSTRAINTS_STREAM}"),
Route::GetBuilderDelegations => format!("{PATH_BUILDER_API}{PATH_BUILDER_DELEGATIONS}"),
Route::All => panic!("All is not a real route"),
Route::BuilderApi => panic!("BuilderApi is not a real route"),
Route::ProposerApi => panic!("ProposerApi is not a real route"),
Expand Down
3 changes: 3 additions & 0 deletions crates/database/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ pub enum DatabaseError {
#[error("Block submission not found")]
RowParsingError(#[from] Box<dyn std::error::Error + Sync + Send>),

#[error("Validator delegation not found")]
ValidatorDelegationNotFound,

#[error("General error")]
GeneralError,
}
28 changes: 25 additions & 3 deletions crates/database/src/mock_database_service.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::{
collections::HashSet,
collections::{HashMap, HashSet},
sync::{Arc, Mutex},
};

Expand All @@ -25,24 +25,46 @@ use crate::{
pub struct MockDatabaseService {
known_validators: Arc<Mutex<Vec<ValidatorSummary>>>,
proposer_duties: Arc<Mutex<Vec<BuilderGetValidatorsResponseEntry>>>,
validator_delegations: Arc<Mutex<HashMap<BlsPublicKey, Vec<BlsPublicKey>>>>,
}

impl MockDatabaseService {
pub fn new(
known_validators: Arc<Mutex<Vec<ValidatorSummary>>>,
proposer_duties: Arc<Mutex<Vec<BuilderGetValidatorsResponseEntry>>>,
) -> Self {
Self { known_validators, proposer_duties }
Self {
known_validators,
proposer_duties,
validator_delegations: Arc::new(Mutex::new(HashMap::new())),
}
}
}

#[async_trait]
impl DatabaseService for MockDatabaseService {
async fn get_validator_delegations(
&self,
pub_key: BlsPublicKey,
) -> Result<Vec<BlsPublicKey>, DatabaseError> {
let validator_delegations = self.validator_delegations.lock().unwrap();
let delegations = validator_delegations.get(&pub_key);
match delegations {
Some(delegations) => Ok(delegations.clone()),
None => Ok(vec![]),
}
}

async fn save_validator_delegation(
&self,
signed_delegation: SignedDelegation,
) -> Result<(), DatabaseError> {
println!("received delegation: {:?}", signed_delegation);
// Add to the hashmap mapping
let mut validator_delegations = self.validator_delegations.lock().unwrap();
let delegatee_pub_key = signed_delegation.message.delegatee_pubkey;
let validator_pub_key = signed_delegation.message.validator_pubkey;
let delegations = validator_delegations.entry(validator_pub_key).or_insert_with(Vec::new);
delegations.push(delegatee_pub_key);
Ok(())
}
async fn revoke_validator_delegation(
Expand Down
8 changes: 8 additions & 0 deletions crates/database/src/postgres/postgres_db_row_parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ impl FromRow for GetPayloadTrace {
}
}

impl FromRow for BlsPublicKey {
fn from_row(row: &tokio_postgres::Row) -> Result<Self, DatabaseError> {
Ok(parse_bytes_to_pubkey(
row.get::<&str, &[u8]>("validator_delegations"),
)?)
}
}

impl<
const BYTES_PER_LOGS_BLOOM: usize,
const MAX_EXTRA_DATA_BYTES: usize,
Expand Down
23 changes: 23 additions & 0 deletions crates/database/src/postgres/postgres_db_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,29 @@ impl Default for PostgresDatabaseService {

#[async_trait]
impl DatabaseService for PostgresDatabaseService {
async fn get_validator_delegations(
&self,
validator_pubkey: BlsPublicKey, // Passing the validator pubkey as a byte slice
) -> Result<Vec<BlsPublicKey>, DatabaseError> {
match self
.pool
.get()
.await?
.query(
"
SELECT delegatee_pubkey
FROM validator_delegations
WHERE validator_pubkey = $1
",
&[&(validator_pubkey.as_ref())],
)
.await?
{
rows if rows.is_empty() => Err(DatabaseError::ValidatorDelegationNotFound),
rows => parse_rows(rows),
}
}

async fn save_validator_delegation(
&self,
signed_delegation: SignedDelegation,
Expand Down
5 changes: 5 additions & 0 deletions crates/database/src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ use crate::{

#[async_trait]
pub trait DatabaseService: Send + Sync + Clone {
async fn get_validator_delegations(
&self,
pub_key: BlsPublicKey,
) -> Result<Vec<BlsPublicKey>, DatabaseError>;

async fn save_validator_delegation(
&self,
signed_delegation: SignedDelegation,
Expand Down