Skip to content

Commit

Permalink
Merge pull request #4980 from nymtech/feature/nym-api-always-expose-g…
Browse files Browse the repository at this point in the history
…lobal-ecash-data

enable global ecash routes even if api is not a signer
  • Loading branch information
jstuczyn authored Oct 17, 2024
2 parents 6446e43 + ed2fbc5 commit 72c54e0
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 122 deletions.
15 changes: 12 additions & 3 deletions nym-api/src/ecash/api_routes/issued.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,16 @@ struct EpochParam {
),
path = "/v1/ecash/epoch-credentials/{epoch}",
responses(
(status = 200, body = EpochCredentialsResponse)
(status = 200, body = EpochCredentialsResponse),
(status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"),
)
)]
async fn epoch_credentials(
Path(EpochParam { epoch }): Path<EpochParam>,
state: Arc<EcashState>,
) -> AxumResult<Json<EpochCredentialsResponse>> {
state.ensure_signer().await?;

let issued = state.aux.storage.get_epoch_credentials(epoch).await?;

let response = if let Some(issued) = issued {
Expand Down Expand Up @@ -92,13 +95,16 @@ struct IdParam {
),
path = "/v1/ecash/issued-credential/{id}",
responses(
(status = 200, body = IssuedCredentialResponse)
(status = 200, body = IssuedCredentialResponse),
(status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"),
)
)]
async fn issued_credential(
Path(IdParam { id }): Path<IdParam>,
state: Arc<EcashState>,
) -> AxumResult<Json<IssuedCredentialResponse>> {
state.ensure_signer().await?;

let issued = state.aux.storage.get_issued_credential(id).await?;

let credential = if let Some(issued) = issued {
Expand All @@ -116,13 +122,16 @@ async fn issued_credential(
request_body = CredentialsRequestBody,
path = "/v1/ecash/issued-credentials",
responses(
(status = 200, body = IssuedCredentialsResponse)
(status = 200, body = IssuedCredentialsResponse),
(status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"),
)
)]
async fn issued_credentials(
Json(params): Json<CredentialsRequestBody>,
state: Arc<EcashState>,
) -> AxumResult<Json<IssuedCredentialsResponse>> {
state.ensure_signer().await?;

if params.pagination.is_some() && !params.credential_ids.is_empty() {
return Err(EcashError::InvalidQueryArguments.into());
}
Expand Down
16 changes: 13 additions & 3 deletions nym-api/src/ecash/api_routes/partial_signing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,17 @@ pub(crate) fn partial_signing_routes(ecash_state: Arc<EcashState>) -> Router<App
request_body = BlindSignRequestBody,
path = "/v1/ecash/blind-sign",
responses(
(status = 200, body = BlindedSignatureResponse)
(status = 200, body = BlindedSignatureResponse),
(status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"),
)
)]
async fn post_blind_sign(
Json(blind_sign_request_body): Json<BlindSignRequestBody>,
state: Arc<EcashState>,
) -> AxumResult<Json<BlindedSignatureResponse>> {
state.ensure_signer().await?;

debug!("Received blind sign request");
trace!("body: {:?}", blind_sign_request_body);

Expand Down Expand Up @@ -125,13 +129,16 @@ struct ExpirationDateParam {
),
path = "/v1/ecash/partial-expiration-date-signatures/{expiration_date}",
responses(
(status = 200, body = PartialExpirationDateSignatureResponse)
(status = 200, body = PartialExpirationDateSignatureResponse),
(status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"),
)
)]
async fn partial_expiration_date_signatures(
Path(ExpirationDateParam { expiration_date }): Path<ExpirationDateParam>,
state: Arc<EcashState>,
) -> AxumResult<Json<PartialExpirationDateSignatureResponse>> {
state.ensure_signer().await?;

let expiration_date = match expiration_date {
None => cred_exp_date().ecash_date(),
Some(raw) => Date::parse(&raw, &rfc_3339_date())
Expand Down Expand Up @@ -160,13 +167,16 @@ async fn partial_expiration_date_signatures(
),
path = "/v1/ecash/partial-coin-indices-signatures/{epoch_id}",
responses(
(status = 200, body = PartialExpirationDateSignatureResponse)
(status = 200, body = PartialExpirationDateSignatureResponse),
(status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"),
)
)]
async fn partial_coin_indices_signatures(
Path(EpochIdParam { epoch_id }): Path<EpochIdParam>,
state: Arc<EcashState>,
) -> AxumResult<Json<PartialCoinIndicesSignatureResponse>> {
state.ensure_signer().await?;

// see if we're not in the middle of new dkg
state.ensure_dkg_not_in_progress().await?;

Expand Down
21 changes: 12 additions & 9 deletions nym-api/src/ecash/api_routes/spending.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

use crate::ecash::error::EcashError;
use crate::ecash::state::EcashState;
use crate::node_status_api::models::AxumResult;
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
use crate::support::http::state::AppState;
use axum::{Json, Router};
use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY;
Expand Down Expand Up @@ -61,14 +61,17 @@ fn reject_ticket(
request_body = VerifyEcashTicketBody,
path = "/v1/ecash/verify-ecash-ticket",
responses(
(status = 200, body = EcashTicketVerificationResponse)
(status = 200, body = EcashTicketVerificationResponse),
(status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"),
)
)]
async fn verify_ticket(
// TODO in the future: make it send binary data rather than json
Json(verify_ticket_body): Json<VerifyEcashTicketBody>,
state: Arc<EcashState>,
) -> AxumResult<Json<EcashTicketVerificationResponse>> {
state.ensure_signer().await?;

let credential_data = &verify_ticket_body.credential;
let gateway_cosmos_addr = &verify_ticket_body.gateway_cosmos_addr;

Expand Down Expand Up @@ -161,14 +164,17 @@ async fn verify_ticket(
request_body = BatchRedeemTicketsBody,
path = "/v1/ecash/batch-redeem-ecash-tickets",
responses(
(status = 200, body = EcashBatchTicketRedemptionResponse)
(status = 200, body = EcashBatchTicketRedemptionResponse),
(status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"),
)
)]
async fn batch_redeem_tickets(
// TODO in the future: make it send binary data rather than json
Json(batch_redeem_credentials_body): Json<BatchRedeemTicketsBody>,
state: Arc<EcashState>,
) -> AxumResult<Json<EcashBatchTicketRedemptionResponse>> {
state.ensure_signer().await?;

// 1. see if that gateway has even submitted any tickets
let Some(provider_info) = state
.get_ticket_provider(batch_redeem_credentials_body.gateway_cosmos_addr.as_ref())
Expand Down Expand Up @@ -233,14 +239,11 @@ async fn batch_redeem_tickets(
get,
path = "/v1/ecash/double-spending-filter-v1",
responses(
(status = 200, body = SpentCredentialsResponse)
(status = 500, body = ErrorResponse, description = "bloomfilters got disabled"),
)
)]
async fn double_spending_filter_v1(
state: Arc<EcashState>,
_state: Arc<EcashState>,
) -> AxumResult<Json<SpentCredentialsResponse>> {
let spent_credentials_export = state.get_bloomfilter_bytes().await;
Ok(Json(SpentCredentialsResponse::new(
spent_credentials_export,
)))
AxumResult::Err(AxumErrorResponse::internal_msg("permanently restricted"))
}
3 changes: 3 additions & 0 deletions nym-api/src/ecash/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ pub enum EcashError {
#[error(transparent)]
IOError(#[from] std::io::Error),

#[error("this operation couldn't be completed as this nym-api is not an active ecash signer")]
NotASigner,

#[error("the address of the bandwidth contract hasn't been set")]
MissingBandwidthContractAddress,

Expand Down
61 changes: 9 additions & 52 deletions nym-api/src/ecash/state/local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@ use crate::ecash::keys::KeyPair;
use nym_config::defaults::BloomfilterParameters;
use nym_crypto::asymmetric::identity;
use nym_ecash_double_spending::DoubleSpendingFilter;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use time::{Date, OffsetDateTime};
use time::Date;
use tokio::sync::RwLock;
use tracing::debug;

pub(crate) struct TicketDoubleSpendingFilter {
built_on: Date,
Expand Down Expand Up @@ -71,86 +69,45 @@ impl TicketDoubleSpendingFilter {
self.today_filter.dump_bitmap()
}

pub(crate) fn export_global_bitmap(&self) -> Vec<u8> {
self.global_filter.dump_bitmap()
}

pub(crate) fn advance_day(&mut self, date: Date, new_global: DoubleSpendingFilter) {
self.built_on = date;
self.global_filter = new_global;
self.today_filter.reset();
}
}

pub(crate) struct ExportedDoubleSpendingFilterData {
pub(crate) last_exported_at: OffsetDateTime,
pub(crate) bytes: Vec<u8>,
}

#[derive(Clone)]
pub(crate) struct ExportedDoubleSpendingFilter {
pub(crate) being_exported: Arc<AtomicBool>,
pub(crate) data: Arc<RwLock<ExportedDoubleSpendingFilterData>>,
}

pub(crate) struct LocalEcashState {
pub(crate) ecash_keypair: KeyPair,
pub(crate) identity_keypair: identity::KeyPair,

pub(crate) explicitly_disabled: bool,

/// Specifies whether this api is a signer in given epoch
pub(crate) active_signer: CachedImmutableEpochItem<bool>,

pub(crate) partial_coin_index_signatures: CachedImmutableEpochItem<IssuedCoinIndicesSignatures>,
pub(crate) partial_expiration_date_signatures:
CachedImmutableItems<Date, IssuedExpirationDateSignatures>,

// the actual, up to date, bloomfilter
pub(crate) double_spending_filter: Arc<RwLock<TicketDoubleSpendingFilter>>,

// the cached byte representation of the bloomfilter to be used by the clients
pub(crate) exported_double_spending_filter: ExportedDoubleSpendingFilter,
}

impl LocalEcashState {
pub(crate) fn new(
ecash_keypair: KeyPair,
identity_keypair: identity::KeyPair,
double_spending_filter: TicketDoubleSpendingFilter,
explicitly_disabled: bool,
) -> Self {
LocalEcashState {
ecash_keypair,
identity_keypair,
explicitly_disabled,
active_signer: Default::default(),
partial_coin_index_signatures: Default::default(),
partial_expiration_date_signatures: Default::default(),
exported_double_spending_filter: ExportedDoubleSpendingFilter {
being_exported: Arc::new(Default::default()),
data: Arc::new(RwLock::new(ExportedDoubleSpendingFilterData {
last_exported_at: OffsetDateTime::now_utc(),
bytes: double_spending_filter.export_global_bitmap(),
})),
},
double_spending_filter: Arc::new(RwLock::new(double_spending_filter)),
}
}

pub(crate) fn maybe_background_update_exported_bloomfilter(&self) {
// make sure another query hasn't already spawned an exporting task
let Ok(should_export) = self
.exported_double_spending_filter
.being_exported
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
else {
return;
};

let filter = self.double_spending_filter.clone();
let exported = self.exported_double_spending_filter.clone();

if should_export {
tokio::spawn(async move {
debug!("exporting bloomfilter bitmap");
let new = filter.read().await.export_global_bitmap();
let mut exported_guard = exported.data.write().await;
exported_guard.last_exported_at = OffsetDateTime::now_utc();
exported_guard.bytes = new;
});
}
}
}
51 changes: 36 additions & 15 deletions nym-api/src/ecash/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ use nym_ecash_double_spending::DoubleSpendingFilter;
use nym_ecash_time::cred_exp_date;
use nym_validator_client::nyxd::AccountId;
use nym_validator_client::EcashApiClient;
use std::ops::Deref;
use time::ext::NumericalDuration;
use time::{Date, Duration, OffsetDateTime};
use time::{Date, OffsetDateTime};
use tokio::sync::RwLockReadGuard;
use tracing::{debug, error, info, warn};

Expand Down Expand Up @@ -74,6 +75,7 @@ impl EcashState {
key_pair: KeyPair,
comm_channel: D,
storage: NymApiStorage,
signer_disabled: bool,
) -> Result<Self>
where
C: LocalClient + Send + Sync + 'static,
Expand All @@ -83,11 +85,43 @@ impl EcashState {

Ok(Self {
global: GlobalEcachState::new(contract_address),
local: LocalEcashState::new(key_pair, identity_keypair, double_spending_filter),
local: LocalEcashState::new(
key_pair,
identity_keypair,
double_spending_filter,
signer_disabled,
),
aux: AuxiliaryEcashState::new(client, comm_channel, storage),
})
}

/// Ensures that this nym-api is one of ecash signers for the current epoch
pub(crate) async fn ensure_signer(&self) -> Result<()> {
if self.local.explicitly_disabled {
return Err(EcashError::NotASigner);
}

let epoch_id = self.aux.current_epoch().await?;

let is_epoch_signer = self
.local
.active_signer
.get_or_init(epoch_id, || async {
let address = self.aux.client.address().await;
let ecash_signers = self.aux.comm_channel.ecash_clients(epoch_id).await?;

// check if any ecash signers for this epoch has the same cosmos address as this api
Ok(ecash_signers.iter().any(|c| c.cosmos_address == address))
})
.await?;

if !is_epoch_signer.deref() {
return Err(EcashError::NotASigner);
}

Ok(())
}

pub(crate) async fn ecash_signing_key(&self) -> Result<RwLockReadGuard<SecretKeyAuth>> {
self.local.ecash_keypair.signing_key().await
}
Expand Down Expand Up @@ -840,17 +874,4 @@ impl EcashState {

res
}

pub async fn get_bloomfilter_bytes(&self) -> Vec<u8> {
let guard = self.local.exported_double_spending_filter.data.read().await;

let bytes = guard.bytes.clone();

// see if it's been > 5min since last export (that value is arbitrary)
if guard.last_exported_at + Duration::minutes(5) < OffsetDateTime::now_utc() {
self.local.maybe_background_update_exported_bloomfilter();
}

bytes
}
}
4 changes: 2 additions & 2 deletions nym-api/src/ecash/tests/issued_credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ async fn epoch_credentials() {
assert_eq!(parsed_response.total_issued, 2);
assert_eq!(parsed_response.first_epoch_credential_id, Some(1));

test_fixture.set_epoch(2);
test_fixture.set_epoch(2).await;

let response = test_fixture.axum.get(&route_epoch2).await;
assert_eq!(response.status_code(), StatusCode::OK);
Expand Down Expand Up @@ -115,7 +115,7 @@ async fn issued_credential() {

let cred1 = test_fixture.issue_credential(request1.clone()).await;

test_fixture.set_epoch(3);
test_fixture.set_epoch(3).await;
let cred2 = test_fixture.issue_credential(request2.clone()).await;

let response = test_fixture.axum.get(&route(1)).await;
Expand Down
Loading

0 comments on commit 72c54e0

Please sign in to comment.