Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
46 changes: 46 additions & 0 deletions src/api/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,19 @@ use crate::api::method::get_queue_elements::{
use crate::api::method::get_queue_info::{
get_queue_info, GetQueueInfoRequest, GetQueueInfoResponse,
};
use crate::api::method::get_queue_leaf_indices::{
get_queue_leaf_indices, GetQueueLeafIndicesRequest, GetQueueLeafIndicesResponse,
};
use crate::api::method::get_validity_proof::{
get_validity_proof, get_validity_proof_v2, GetValidityProofRequest,
GetValidityProofRequestDocumentation, GetValidityProofRequestV2, GetValidityProofResponse,
GetValidityProofResponseV2,
};
use crate::api::method::interface::{
get_account_interface, get_multiple_account_interfaces, GetAccountInterfaceRequest,
GetAccountInterfaceResponse, GetMultipleAccountInterfacesRequest,
GetMultipleAccountInterfacesResponse,
};
use crate::api::method::utils::{
AccountBalanceResponse, GetLatestSignaturesRequest, GetNonPaginatedSignaturesResponse,
GetNonPaginatedSignaturesResponseWithError, GetPaginatedSignaturesResponse, HashRequest,
Expand Down Expand Up @@ -283,6 +291,13 @@ impl PhotonApi {
get_queue_info(self.db_conn.as_ref(), request).await
}

pub async fn get_queue_leaf_indices(
&self,
request: GetQueueLeafIndicesRequest,
) -> Result<GetQueueLeafIndicesResponse, PhotonApiError> {
get_queue_leaf_indices(self.db_conn.as_ref(), request).await
}

pub async fn get_compressed_accounts_by_owner(
&self,
request: GetCompressedAccountsByOwnerRequest,
Expand Down Expand Up @@ -402,13 +417,33 @@ impl PhotonApi {
get_latest_non_voting_signatures(self.db_conn.as_ref(), request).await
}

// Interface endpoints - race hot (on-chain) and cold (compressed) lookups
pub async fn get_account_interface(
&self,
request: GetAccountInterfaceRequest,
) -> Result<GetAccountInterfaceResponse, PhotonApiError> {
get_account_interface(&self.db_conn, &self.rpc_client, request).await
}

pub async fn get_multiple_account_interfaces(
&self,
request: GetMultipleAccountInterfacesRequest,
) -> Result<GetMultipleAccountInterfacesResponse, PhotonApiError> {
get_multiple_account_interfaces(&self.db_conn, &self.rpc_client, request).await
}

pub fn method_api_specs() -> Vec<OpenApiSpec> {
vec![
OpenApiSpec {
name: "getQueueElements".to_string(),
request: Some(GetQueueElementsRequest::schema().1),
response: GetQueueElementsResponse::schema().1,
},
OpenApiSpec {
name: "getQueueLeafIndices".to_string(),
request: Some(GetQueueLeafIndicesRequest::schema().1),
response: GetQueueLeafIndicesResponse::schema().1,
},
OpenApiSpec {
name: "getQueueInfo".to_string(),
request: Some(GetQueueInfoRequest::schema().1),
Expand Down Expand Up @@ -591,6 +626,17 @@ impl PhotonApi {
request: None,
response: UnsignedInteger::schema().1,
},
// Interface endpoints
OpenApiSpec {
name: "getAccountInterface".to_string(),
request: Some(GetAccountInterfaceRequest::schema().1),
response: GetAccountInterfaceResponse::schema().1,
},
OpenApiSpec {
name: "getMultipleAccountInterfaces".to_string(),
request: Some(GetMultipleAccountInterfacesRequest::schema().1),
response: GetMultipleAccountInterfacesResponse::schema().1,
},
]
}
}
101 changes: 101 additions & 0 deletions src/api/method/get_queue_leaf_indices.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use crate::api::error::PhotonApiError;
use crate::common::typedefs::context::Context;
use crate::common::typedefs::hash::Hash;
use crate::dao::generated::accounts;
use sea_orm::{
ColumnTrait, Condition, DatabaseConnection, EntityTrait, FromQueryResult, QueryFilter,
QueryOrder, QuerySelect,
};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

const MAX_QUEUE_ELEMENTS: u16 = 30_000;

/// Parameters for requesting input queue leaf indices.
/// Returns (hash, queue_index, leaf_index) for nullifier queue items.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct GetQueueLeafIndicesRequest {
pub tree: Hash,
pub limit: u16,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_index: Option<u64>,
}

/// A lightweight queue leaf index entry
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct QueueLeafIndex {
pub hash: Hash,
pub queue_index: u64,
pub leaf_index: u64,
}

/// Response containing queue leaf indices
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct GetQueueLeafIndicesResponse {
pub context: Context,
pub value: Vec<QueueLeafIndex>,
}

#[derive(FromQueryResult, Debug)]
struct QueueLeafIndexModel {
hash: Vec<u8>,
nullifier_queue_index: i64,
leaf_index: i64,
}

pub async fn get_queue_leaf_indices(
conn: &DatabaseConnection,
request: GetQueueLeafIndicesRequest,
) -> Result<GetQueueLeafIndicesResponse, PhotonApiError> {
if request.limit > MAX_QUEUE_ELEMENTS {
return Err(PhotonApiError::ValidationError(format!(
"Too many queue elements requested {}. Maximum allowed: {}",
request.limit, MAX_QUEUE_ELEMENTS
)));
}

let context = Context::extract(conn).await?;

let mut query_condition = Condition::all()
.add(accounts::Column::Tree.eq(request.tree.to_vec()))
.add(accounts::Column::NullifierQueueIndex.is_not_null())
.add(accounts::Column::NullifiedInTree.eq(false))
.add(accounts::Column::Spent.eq(true));

if let Some(start_queue_index) = request.start_index {
query_condition = query_condition
.add(accounts::Column::NullifierQueueIndex.gte(start_queue_index as i64));
}

let queue_elements: Vec<QueueLeafIndexModel> = accounts::Entity::find()
.filter(query_condition)
.order_by_asc(accounts::Column::NullifierQueueIndex)
.limit(request.limit as u64)
.into_model::<QueueLeafIndexModel>()
.all(conn)
.await
.map_err(|e| {
PhotonApiError::UnexpectedError(format!("DB error fetching queue leaf indices: {}", e))
})?;

let value = queue_elements
.into_iter()
.map(|e| {
Ok(QueueLeafIndex {
hash: Hash::new(e.hash.as_slice()).map_err(|err| {
PhotonApiError::UnexpectedError(format!(
"Invalid hash for queue element at queue_index {}: {}",
e.nullifier_queue_index, err
))
})?,
queue_index: e.nullifier_queue_index as u64,
leaf_index: e.leaf_index as u64,
})
})
.collect::<Result<Vec<QueueLeafIndex>, PhotonApiError>>()?;

Ok(GetQueueLeafIndicesResponse { context, value })
}
8 changes: 4 additions & 4 deletions src/api/method/get_validity_proof/prover/gnark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::api::error::PhotonApiError;
use crate::api::method::get_validity_proof::prover::structs::{CompressedProof, ProofABC};
use ark_serialize::{CanonicalDeserialize, CanonicalSerialize, Compress, Validate};
use solana_bn254::compression::prelude::{
alt_bn128_g1_compress, alt_bn128_g2_compress, convert_endianness,
alt_bn128_g1_compress_be, alt_bn128_g2_compress_be, convert_endianness,
};
use std::ops::Neg;

Expand Down Expand Up @@ -31,11 +31,11 @@ pub fn negate_g1(g1_be: &[u8; 64]) -> Result<[u8; 64], PhotonApiError> {
}

pub fn compress_proof(proof: &ProofABC) -> Result<CompressedProof, PhotonApiError> {
let proof_a = alt_bn128_g1_compress(&proof.a)
let proof_a = alt_bn128_g1_compress_be(&proof.a)
.map_err(|_| PhotonApiError::UnexpectedError("Failed to compress G1 proof".to_string()))?;
let proof_b = alt_bn128_g2_compress(&proof.b)
let proof_b = alt_bn128_g2_compress_be(&proof.b)
.map_err(|_| PhotonApiError::UnexpectedError("Failed to compress G2 proof".to_string()))?;
let proof_c = alt_bn128_g1_compress(&proof.c)
let proof_c = alt_bn128_g1_compress_be(&proof.c)
.map_err(|_| PhotonApiError::UnexpectedError("Failed to compress G1 proof".to_string()))?;

Ok(CompressedProof {
Expand Down
23 changes: 23 additions & 0 deletions src/api/method/interface/get_account_interface.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use sea_orm::DatabaseConnection;
use solana_client::nonblocking::rpc_client::RpcClient;

use crate::api::error::PhotonApiError;
use crate::common::typedefs::context::Context;

use super::racing::race_hot_cold;
use super::types::{GetAccountInterfaceRequest, GetAccountInterfaceResponse};

/// Get account data from either on-chain or compressed sources.
/// Races both lookups and returns the result with the higher slot.
pub async fn get_account_interface(
conn: &DatabaseConnection,
rpc_client: &RpcClient,
request: GetAccountInterfaceRequest,
) -> Result<GetAccountInterfaceResponse, PhotonApiError> {
let context = Context::extract(conn).await?;
let commitment = request.commitment.unwrap_or_default();

let value = race_hot_cold(rpc_client, conn, &request.address, None, commitment).await?;

Ok(GetAccountInterfaceResponse { context, value })
}
115 changes: 115 additions & 0 deletions src/api/method/interface/get_multiple_account_interfaces.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use sea_orm::DatabaseConnection;
use solana_client::nonblocking::rpc_client::RpcClient;
use tokio::sync::Semaphore;

use crate::api::error::PhotonApiError;
use crate::common::typedefs::context::Context;
use crate::common::typedefs::serializable_pubkey::SerializablePubkey;

use super::racing::{get_distinct_owners_with_addresses, race_hot_cold};
use super::types::{
AccountInterface, GetMultipleAccountInterfacesRequest, GetMultipleAccountInterfacesResponse,
MAX_BATCH_SIZE,
};

/// Maximum concurrent hot+cold lookups per batch request.
const MAX_CONCURRENT_LOOKUPS: usize = 20;

/// Get multiple account data from either on-chain or compressed sources.
/// Returns one unified AccountInterface shape for every input pubkey.
pub async fn get_multiple_account_interfaces(
conn: &DatabaseConnection,
rpc_client: &RpcClient,
request: GetMultipleAccountInterfacesRequest,
) -> Result<GetMultipleAccountInterfacesResponse, PhotonApiError> {
if request.addresses.len() > MAX_BATCH_SIZE {
return Err(PhotonApiError::ValidationError(format!(
"Batch size {} exceeds maximum of {}",
request.addresses.len(),
MAX_BATCH_SIZE
)));
}

if request.addresses.is_empty() {
return Err(PhotonApiError::ValidationError(
"At least one address must be provided".to_string(),
));
}

let context = Context::extract(conn).await?;
let commitment = request.commitment.unwrap_or_default();

let distinct_owners = get_distinct_owners_with_addresses(conn)
.await
.map_err(PhotonApiError::DatabaseError)?;

let semaphore = Semaphore::new(MAX_CONCURRENT_LOOKUPS);
let futures: Vec<_> = request
.addresses
.iter()
.map(|address| async {
let _permit = semaphore.acquire().await.unwrap();
race_hot_cold(
rpc_client,
conn,
address,
Some(&distinct_owners),
commitment,
)
.await
})
.collect();

let results = futures::future::join_all(futures).await;

let value = collect_batch_results(&request.addresses, results)?;

Ok(GetMultipleAccountInterfacesResponse { context, value })
}

fn collect_batch_results(
addresses: &[SerializablePubkey],
results: Vec<Result<Option<AccountInterface>, PhotonApiError>>,
) -> Result<Vec<Option<AccountInterface>>, PhotonApiError> {
let mut value = Vec::with_capacity(results.len());
for (i, result) in results.into_iter().enumerate() {
match result {
// Includes Ok(None): account not found is returned as None.
Ok(account) => value.push(account),
// Only actual lookup failures abort the entire batch call.
Err(e) => {
log::error!(
"Failed to fetch interface for address {:?} (index {}): {:?}",
addresses.get(i),
i,
e
);
return Err(e);
}
}
}
Ok(value)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn collect_batch_results_keeps_none_for_not_found_accounts() {
let addresses = vec![SerializablePubkey::default(), SerializablePubkey::default()];
let results = vec![Ok(None), Ok(None)];

let value = collect_batch_results(&addresses, results).expect("expected success");
assert_eq!(value, vec![None, None]);
}

#[test]
fn collect_batch_results_returns_error_for_actual_failure() {
let addresses = vec![SerializablePubkey::default()];
let results = vec![Err(PhotonApiError::UnexpectedError("boom".to_string()))];

let err = collect_batch_results(&addresses, results).expect_err("expected error");
assert_eq!(err, PhotonApiError::UnexpectedError("boom".to_string()));
}
}
8 changes: 8 additions & 0 deletions src/api/method/interface/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
pub mod get_account_interface;
pub mod get_multiple_account_interfaces;
pub mod racing;
pub mod types;

pub use get_account_interface::get_account_interface;
pub use get_multiple_account_interfaces::get_multiple_account_interfaces;
pub use types::*;
Loading
Loading