Files
nym/nym-api/src/ecash/state/mod.rs
T
Jędrzej Stuczyński 65175fee09 merge #5512 again after reverting due to incorrect rebase (#5520)
* setup workspace global lints to prevent needless panics

* removed sources of panic in nym-crypto, nym-node and nym-api

* adjusted test code
2025-02-26 10:52:09 +00:00

951 lines
35 KiB
Rust

// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::ecash::client::Client as LocalClient;
use crate::ecash::comm::APICommunicationChannel;
use crate::ecash::deposit::validate_deposit;
use crate::ecash::error::{EcashError, RedemptionError, Result};
use crate::ecash::helpers::{IssuedCoinIndicesSignatures, IssuedExpirationDateSignatures};
use crate::ecash::keys::KeyPair;
use crate::ecash::state::auxiliary::AuxiliaryEcashState;
use crate::ecash::state::cleaner::EcashBackgroundStateCleaner;
use crate::ecash::state::global::GlobalEcachState;
use crate::ecash::state::helpers::{ensure_sane_expiration_date, query_all_threshold_apis};
use crate::ecash::state::local::{DailyMerkleTree, LocalEcashState};
use crate::ecash::storage::models::{SerialNumberWrapper, TicketProvider};
use crate::ecash::storage::EcashStorageExt;
use crate::support::config::Config;
use crate::support::storage::NymApiStorage;
use cosmwasm_std::{from_binary, CosmosMsg, WasmMsg};
use cw3::Status;
use nym_api_requests::ecash::models::{
BatchRedeemTicketsBody, IssuedTicketbooksChallengeRequest,
IssuedTicketbooksChallengeResponseBody, IssuedTicketbooksForResponseBody,
};
use nym_api_requests::ecash::BlindSignRequestBody;
use nym_coconut_dkg_common::types::EpochId;
use nym_compact_ecash::scheme::coin_indices_signatures::{
aggregate_annotated_indices_signatures, sign_coin_indices, CoinIndexSignatureShare,
};
use nym_compact_ecash::scheme::expiration_date_signatures::{
aggregate_annotated_expiration_signatures, ExpirationDateSignatureShare,
};
use nym_compact_ecash::{
scheme::expiration_date_signatures::sign_expiration_date, BlindedSignature, Bytable,
SecretKeyAuth, VerificationKeyAuth,
};
use nym_credentials::ecash::utils::EcashTime;
use nym_credentials::{aggregate_verification_keys, CredentialSpendingData};
use nym_crypto::asymmetric::identity;
use nym_ecash_contract_common::deposit::{Deposit, DepositId};
use nym_ecash_contract_common::msg::ExecuteMsg;
use nym_ecash_contract_common::redeem_credential::BATCH_REDEMPTION_PROPOSAL_TITLE;
use nym_ecash_time::{ecash_default_expiration_date, ecash_today_date};
use nym_task::TaskClient;
use nym_ticketbooks_merkle::{IssuedTicketbook, IssuedTicketbooksFullMerkleProof, MerkleLeaf};
use nym_validator_client::nyxd::AccountId;
use nym_validator_client::EcashApiClient;
use rand::{thread_rng, RngCore};
use std::collections::HashMap;
use std::ops::Deref;
use time::{Date, OffsetDateTime};
use tokio::sync::{RwLockReadGuard, RwLockWriteGuard};
use tokio::task::JoinHandle;
use tracing::{debug, error, info, warn};
pub(crate) mod auxiliary;
mod cleaner;
pub(crate) mod global;
mod helpers;
pub(crate) mod local;
pub struct EcashStateConfig {
pub(crate) issued_ticketbooks_retention_period_days: u32,
}
impl EcashStateConfig {
pub(crate) fn ticketbook_retention_cutoff(&self) -> Date {
ecash_today_date()
- time::Duration::days(self.issued_ticketbooks_retention_period_days as i64)
}
}
impl EcashStateConfig {
pub(crate) fn new(global_config: &Config) -> Self {
EcashStateConfig {
issued_ticketbooks_retention_period_days: global_config
.ecash_signer
.debug
.issued_ticketbooks_retention_period_days,
}
}
}
#[derive(Default)]
pub(crate) enum BackgroundCleanerState {
WaitingStartup(EcashBackgroundStateCleaner),
Running {
_handle: JoinHandle<()>,
},
// an ephemeral state so that we could swap between the other two
#[default]
Invalid,
}
pub struct EcashState {
// additional global config parameters
pub(crate) config: EcashStateConfig,
pub(crate) background_cleaner_state: BackgroundCleanerState,
// state global to the system, like aggregated keys, addresses, etc.
pub(crate) global: GlobalEcachState,
// state local to the api instance, like partial signatures, keys, etc.
pub(crate) local: LocalEcashState,
// auxiliary data used for resolving requests like clients, storage, etc.
pub(crate) aux: AuxiliaryEcashState,
}
impl EcashState {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new<C, D>(
global_config: &Config,
contract_address: AccountId,
client: C,
identity_keypair: identity::KeyPair,
key_pair: KeyPair,
comm_channel: D,
storage: NymApiStorage,
task_client: TaskClient,
) -> Self
where
C: LocalClient + Send + Sync + 'static,
D: APICommunicationChannel + Send + Sync + 'static,
{
Self {
config: EcashStateConfig::new(global_config),
background_cleaner_state: BackgroundCleanerState::WaitingStartup(
EcashBackgroundStateCleaner::new(global_config, storage.clone(), task_client),
),
global: GlobalEcachState::new(contract_address),
local: LocalEcashState::new(
key_pair,
identity_keypair,
!global_config.ecash_signer.enabled,
),
aux: AuxiliaryEcashState::new(client, comm_channel, storage),
}
}
// whilst we normally don't want to panic, this one would only occur at startup,
// if some logical invariants got broken (which have to be fixed in code anyway)
#[allow(clippy::panic)]
pub(crate) fn spawn_background_cleaner(&mut self) {
match std::mem::take(&mut self.background_cleaner_state) {
BackgroundCleanerState::WaitingStartup(cleaner) => {
self.background_cleaner_state = BackgroundCleanerState::Running {
_handle: cleaner.start(),
}
}
_ => panic!("attempted to spawn background cleaner more than once"),
}
}
/// 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 Ok(address) = self.aux.client.address().await else {
return Ok(false);
};
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
}
#[allow(dead_code)]
pub(crate) async fn current_master_verification_key(
&self,
) -> Result<RwLockReadGuard<VerificationKeyAuth>> {
self.master_verification_key(None).await
}
pub(crate) async fn master_verification_key(
&self,
epoch_id: Option<EpochId>,
) -> Result<RwLockReadGuard<VerificationKeyAuth>> {
let epoch_id = match epoch_id {
Some(id) => id,
None => self.aux.current_epoch().await?,
};
self.global
.master_verification_key
.get_or_init(epoch_id, || async {
// 1. check the storage
if let Some(stored) = self
.aux
.storage
.get_master_verification_key(epoch_id)
.await?
{
return Ok(stored);
}
// 2. perform actual aggregation
let all_apis = self.aux.comm_channel.ecash_clients(epoch_id).await?;
let threshold = self.aux.comm_channel.ecash_threshold(epoch_id).await?;
if all_apis.len() < threshold as usize {
return Err(EcashError::InsufficientNumberOfShares {
threshold,
shares: all_apis.len(),
});
}
let master_key = aggregate_verification_keys(&all_apis)?;
// 3. save the key in the storage for when we reboot
self.aux
.storage
.insert_master_verification_key(epoch_id, &master_key)
.await?;
Ok(master_key)
})
.await
}
pub(crate) async fn master_coin_index_signatures(
&self,
epoch_id: Option<EpochId>,
) -> Result<RwLockReadGuard<IssuedCoinIndicesSignatures>> {
let epoch_id = match epoch_id {
Some(id) => id,
None => self.aux.current_epoch().await?,
};
self.global
.coin_index_signatures
.get_or_init(epoch_id, || async {
// 1. check the storage
if let Some(master_sigs) = self
.aux
.storage
.get_master_coin_index_signatures(epoch_id)
.await?
{
return Ok(IssuedCoinIndicesSignatures {
epoch_id,
signatures: master_sigs,
});
}
info!(
"attempting to establish master coin index signatures for epoch {epoch_id}..."
);
// 2. go around APIs and attempt to aggregate the data
let master_vk = self.master_verification_key(Some(epoch_id)).await?;
let all_apis = self.aux.comm_channel.ecash_clients(epoch_id).await?;
let threshold = self.aux.comm_channel.ecash_threshold(epoch_id).await?;
// let mut shares = Mutex::new(Vec::with_capacity(all_apis.len()));
let cosmos_address = self.aux.client.address().await.ok();
let get_partial_signatures = |api: EcashApiClient| async {
// move the api into the closure
let api = api;
let node_index = api.node_id;
let partial_vk = api.verification_key;
// check if we're attempting to query ourselves, in that case just get local signature
// rather than making the http query
let partial = if Some(api.cosmos_address) == cosmos_address {
self.partial_coin_index_signatures(Some(epoch_id))
.await?
.signatures
.clone()
} else {
api.api_client
.partial_coin_indices_signatures(Some(epoch_id))
.await?
.signatures
};
Ok(CoinIndexSignatureShare {
index: node_index,
key: partial_vk,
signatures: partial,
})
};
let shares =
query_all_threshold_apis(all_apis, threshold, get_partial_signatures).await?;
let aggregated = aggregate_annotated_indices_signatures(
nym_credentials_interface::ecash_parameters(),
&master_vk,
&shares,
)?;
// 3. save the signatures in the storage for when we reboot
self.aux
.storage
.insert_master_coin_index_signatures(epoch_id, &aggregated)
.await?;
Ok(IssuedCoinIndicesSignatures {
epoch_id,
signatures: aggregated,
})
})
.await
}
pub(crate) async fn partial_coin_index_signatures(
&self,
epoch_id: Option<EpochId>,
) -> Result<RwLockReadGuard<IssuedCoinIndicesSignatures>> {
let epoch_id = match epoch_id {
Some(id) => id,
None => self.aux.current_epoch().await?,
};
self.local
.partial_coin_index_signatures
.get_or_init(epoch_id, || async {
// 1. check the storage
if let Some(partial_sigs) = self
.aux
.storage
.get_partial_coin_index_signatures(epoch_id)
.await?
{
return Ok(IssuedCoinIndicesSignatures {
epoch_id,
signatures: partial_sigs,
});
}
// 2. perform actual issuance
let signing_keys = self.local.ecash_keypair.keys().await?;
if signing_keys.issued_for_epoch != epoch_id {
// TODO: this should get handled at some point,
// because if it was a past epoch we **do** have those keys.
// they're just archived
error!("received partial coin index signature request for an invalid epoch ({epoch_id}). our key was derived for epoch {}", signing_keys.issued_for_epoch);
return Err(EcashError::InvalidSigningKeyEpoch {
requested: epoch_id,
available: signing_keys.issued_for_epoch,
})
}
let master_vk = self.master_verification_key(Some(epoch_id)).await?;
let signatures = sign_coin_indices(
nym_compact_ecash::ecash_parameters(),
&master_vk,
signing_keys.keys.secret_key(),
)?;
// 3. save the signatures in the storage for when we reboot
self.aux.storage.insert_partial_coin_index_signatures(epoch_id, &signatures).await?;
Ok(IssuedCoinIndicesSignatures {
epoch_id,
signatures,
})
})
.await
}
pub(crate) async fn master_expiration_date_signatures(
&self,
expiration_date: Date,
) -> Result<RwLockReadGuard<IssuedExpirationDateSignatures>> {
self.global
.expiration_date_signatures
.get_or_init(expiration_date, || async {
// 1. sanity check to see if the expiration_date is not nonsense
ensure_sane_expiration_date(expiration_date)?;
// 2. check the storage
if let Some(master_sigs) = self
.aux
.storage
.get_master_expiration_date_signatures(expiration_date)
.await?
{
return Ok(master_sigs);
}
// 3. go around APIs and attempt to aggregate the data
let epoch_id = self.aux.comm_channel.current_epoch().await?;
let master_vk = self.master_verification_key(Some(epoch_id)).await?;
let all_apis = self.aux.comm_channel.ecash_clients(epoch_id).await?;
let threshold = self.aux.comm_channel.ecash_threshold(epoch_id).await?;
let cosmos_address = self.aux.client.address().await.ok();
let get_partial_signatures = |api: EcashApiClient| async {
// move the api into the closure
let api = api;
let node_index = api.node_id;
let partial_vk = api.verification_key;
// check if we're attempting to query ourselves, in that case just get local signature
// rather than making the http query
let partial = if Some(api.cosmos_address) == cosmos_address {
self.partial_expiration_date_signatures(expiration_date)
.await?
.signatures
.clone()
} else {
api.api_client
.partial_expiration_date_signatures(Some(expiration_date))
.await?
.signatures
};
Ok(ExpirationDateSignatureShare {
index: node_index,
key: partial_vk,
signatures: partial,
})
};
let shares =
query_all_threshold_apis(all_apis, threshold, get_partial_signatures).await?;
let aggregated = aggregate_annotated_expiration_signatures(
&master_vk,
expiration_date.ecash_unix_timestamp(),
&shares,
)?;
let issued = IssuedExpirationDateSignatures {
epoch_id,
signatures: aggregated,
};
// 4. save the signatures in the storage for when we reboot
self.aux
.storage
.insert_master_expiration_date_signatures(expiration_date, &issued)
.await?;
Ok(issued)
})
.await
}
pub(crate) async fn partial_expiration_date_signatures(
&self,
expiration_date: Date,
) -> Result<RwLockReadGuard<IssuedExpirationDateSignatures>> {
self.local
.partial_expiration_date_signatures
.get_or_init(expiration_date, || async {
// 1. sanity check to see if the expiration_date is not nonsense
ensure_sane_expiration_date(expiration_date)?;
// 2. check the storage
if let Some(partial_sigs) = self
.aux
.storage
.get_partial_expiration_date_signatures(expiration_date)
.await?
{
return Ok(partial_sigs);
}
// 3. perform actual issuance
let signing_keys = self.local.ecash_keypair.keys().await?;
let signatures = sign_expiration_date(
signing_keys.keys.secret_key(),
expiration_date.ecash_unix_timestamp(),
)?;
let issued = IssuedExpirationDateSignatures {
epoch_id: signing_keys.issued_for_epoch,
signatures,
};
// 4. save the signatures in the storage for when we reboot
self.aux
.storage
.insert_partial_expiration_date_signatures(expiration_date, &issued)
.await?;
Ok(issued)
})
.await
}
pub(crate) async fn ensure_dkg_not_in_progress(&self) -> Result<()> {
if self.aux.comm_channel.dkg_in_progress().await? {
return Err(EcashError::DkgInProgress);
}
Ok(())
}
/// Check if this nym-api has already issued a credential for the provided deposit id.
/// If so, return it.
pub async fn already_issued(&self, deposit_id: DepositId) -> Result<Option<BlindedSignature>> {
Ok(self
.aux
.storage
.get_issued_partial_signature(deposit_id)
.await?)
}
pub async fn get_deposit(&self, deposit_id: DepositId) -> Result<Deposit> {
self.aux
.client
.get_deposit(deposit_id)
.await?
.deposit
.ok_or(EcashError::NonExistentDeposit { deposit_id })
}
pub async fn validate_request(
&self,
request: &BlindSignRequestBody,
deposit: Deposit,
) -> Result<()> {
validate_deposit(request, deposit).await
}
pub(crate) async fn validate_redemption_proposal(
&self,
request: &BatchRedeemTicketsBody,
) -> std::result::Result<(), RedemptionError> {
let proposal_id = request.proposal_id;
// retrieve the proposal itself
let mut proposal = self
.aux
.client
.get_proposal(proposal_id)
.await
.map_err(|_| RedemptionError::ProposalRetrievalFailure { proposal_id })?;
if proposal.title != BATCH_REDEMPTION_PROPOSAL_TITLE {
return Err(RedemptionError::InvalidProposalTitle {
proposal_id,
received: proposal.title,
});
}
// make sure you can still vote on it
match proposal.status {
Status::Pending => return Err(RedemptionError::StillPending { proposal_id }),
Status::Open => {}
Status::Rejected => return Err(RedemptionError::AlreadyRejected { proposal_id }),
// TODO: need to double check with the multisig whether it wouldn't always be thrown on threshold
// i.e. whether after the 2+/3 vote, the remaining 1-/3 would return this error
Status::Passed => return Err(RedemptionError::AlreadyPassed { proposal_id }),
Status::Executed => return Err(RedemptionError::AlreadyExecuted { proposal_id }),
}
let encoded_digest = bs58::encode(&request.digest).into_string();
// check if the description matches the expected digest
if encoded_digest != proposal.description {
return Err(RedemptionError::InvalidProposalDescription {
proposal_id,
received: proposal.description,
expected: encoded_digest,
});
}
// check if it was actually created by the ecash contract
if proposal.proposer != self.global.contract_address.as_ref() {
return Err(RedemptionError::InvalidProposer {
proposal_id,
received: proposal.proposer.into_string(),
expected: self.global.contract_address.clone(),
});
}
// check if contains exactly the content we expect,
// i.e. single `RedeemTickets` message with no funds, etc.
if proposal.msgs.len() != 1 {
return Err(RedemptionError::TooManyMessages { proposal_id });
}
// SAFETY: we just checked we have exactly one message
#[allow(clippy::unwrap_used)]
let msg = proposal.msgs.pop().unwrap();
let CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr,
msg,
funds,
}) = msg
else {
return Err(RedemptionError::InvalidMessage { proposal_id });
};
if !funds.is_empty() {
return Err(RedemptionError::InvalidMessage { proposal_id });
}
if contract_addr != self.global.contract_address.as_ref() {
return Err(RedemptionError::InvalidContract { proposal_id });
}
let Ok(ExecuteMsg::RedeemTickets { n, gw }) = from_binary(&msg) else {
return Err(RedemptionError::InvalidMessage { proposal_id });
};
if gw != request.gateway_cosmos_addr.as_ref() {
return Err(RedemptionError::InvalidRedemptionTarget {
proposal_id,
proposed: gw,
received: request.gateway_cosmos_addr.to_string(),
});
}
if n as usize != request.included_serial_numbers.len() {
return Err(RedemptionError::InvalidRedemptionTicketCount {
proposal_id,
proposed: n,
received: request.included_serial_numbers.len() as u16,
});
}
Ok(())
}
pub(crate) async fn accept_proposal(&self, proposal_id: u64) -> Result<()> {
//SW NOTE: What to do if this fails
if let Err(err) = self.aux.client.vote_proposal(proposal_id, true, None).await {
debug!("failed to vote on proposal {proposal_id}: {err}");
}
Ok(())
}
// pub(crate) async fn blacklist(&self, public_key: String) {
// let client = self.aux.client.clone();
// tokio::spawn(async move {
// //SW TODO error handling with one log at the end
// let response = client.propose_for_blacklist(public_key.clone()).await?;
// let proposal_id = find_proposal_id(&response.logs)?;
//
// let proposal = client.get_proposal(proposal_id).await?;
// if proposal.status == Status::Open {
// if public_key != proposal.description {
// return Err(EcashError::IncorrectProposal {
// reason: String::from("incorrect publickey in description"),
// });
// }
// let ret = client.vote_proposal(proposal_id, true, None).await;
//
// accepted_vote_err(ret)?;
//
// if let Ok(proposal) = client.get_proposal(proposal_id).await {
// if proposal.status == Status::Passed {
// client.execute_proposal(proposal_id).await?
// }
// }
// }
// Ok(())
// });
// }
pub(crate) async fn persist_issued(
&self,
current_epoch: EpochId,
issued: &IssuedTicketbook,
merkle_leaf: MerkleLeaf,
) -> Result<()> {
// note: we have a UNIQUE constraint on the deposit_id column of the credential
// and so if the api is processing request for the same deposit at the same time,
// only one of them will be successfully inserted to the database
self.aux
.storage
.store_issued_ticketbook(
issued.deposit_id,
current_epoch as u32,
&issued.blinded_partial_credential,
&issued.joined_encoded_private_attributes_commitments,
issued.expiration_date,
issued.ticketbook_type,
merkle_leaf,
)
.await?;
Ok(())
}
async fn get_updated_merkle_read(
&self,
expiration_date: Date,
) -> Result<RwLockReadGuard<DailyMerkleTree>> {
let write_guard = self.get_updated_full_write(expiration_date).await?;
// SAFETY: the entry was either not empty or we just inserted data in there, whilst never dropping the lock
// thus it MUST exist
#[allow(clippy::unwrap_used)]
Ok(RwLockWriteGuard::downgrade_map(write_guard, |map| {
map.get(&expiration_date).unwrap()
}))
}
async fn get_updated_full_write(
&self,
expiration_date: Date,
) -> Result<RwLockWriteGuard<HashMap<Date, DailyMerkleTree>>> {
let mut write_guard = self.local.issued_merkle_trees.write().await;
// double check if it's still empty in case another task has already grabbed the write lock and performed the update
let still_empty = write_guard.get(&expiration_date).is_none();
if still_empty {
// the order actually does not matter since we're building the tree back from scratch
let issued_hashes = self.aux.storage.get_issued_hashes(expiration_date).await?;
write_guard.insert(expiration_date, DailyMerkleTree::new(issued_hashes));
}
Ok(write_guard)
}
pub async fn store_issued_ticketbook(
&self,
request_body: BlindSignRequestBody,
blinded_signature: &BlindedSignature,
) -> Result<()> {
let current_epoch = self.aux.current_epoch().await?;
let expiration = request_body.expiration_date;
let deposit_id = request_body.deposit_id;
let joined_encoded_private_attributes_commitments = request_body.encode_join_commitments();
let issued = IssuedTicketbook {
deposit_id: request_body.deposit_id,
epoch_id: current_epoch,
blinded_partial_credential: blinded_signature.to_byte_vec(),
joined_encoded_private_attributes_commitments,
expiration_date: request_body.expiration_date,
ticketbook_type: request_body.ticketbook_type,
};
let mut map = self.get_updated_full_write(expiration).await?;
// SAFETY: get_updated_full_write inserted relevant entry to the map, and we never dropped the lock
#[allow(clippy::unwrap_used)]
let merkle_entry = map.get_mut(&expiration).unwrap();
// insert the ticketbook into the merkle tree
let inserted_leaf = merkle_entry.insert(&issued);
// note: there's a primary key constraint on the deposit_id
// and so if the api is processing request for the same deposit at the same time,
// only one of them will be successfully inserted to the database
if let Err(err) = self
.persist_issued(current_epoch, &issued, inserted_leaf)
.await
{
// if we failed to insert it into the db, rollback the tree. there was most likely clash on the deposit
warn!("failed to persist ticketbook corresponding to deposit {deposit_id}: {err}");
merkle_entry.rollback(deposit_id);
return Err(err);
}
// if we managed to insert it into db, check if we might want to purge the tree history,
// since we will no longer have to roll it back
merkle_entry.maybe_rebuild();
// toss a coin to check if we should clean memory of old merkle trees
if thread_rng().next_u32() % 10000 == 0 {
let mut values_to_clean = Vec::new();
let cutoff = self.config.ticketbook_retention_cutoff();
info!("attempting to remove old issued ticketbooks. the cutoff is set to {cutoff}");
for date in map.keys() {
if date < &cutoff {
values_to_clean.push(*date)
}
}
for date in values_to_clean {
// remove the in-memory merkle tree
map.remove(&date);
}
}
Ok(())
}
async fn get_merkle_proof(
&self,
expiration_date: Date,
deposits: &[DepositId],
) -> Result<IssuedTicketbooksFullMerkleProof> {
// check if the entry for this expiration date is empty. if so, it might imply we have crashed/shutdown
// and not have the full data in memory
if self.local.is_merkle_empty(expiration_date).await {
let entry = self.get_updated_merkle_read(expiration_date).await?;
return entry.proof(deposits);
}
// I can imagine this could happen under very rare edge case when the function is called just as the retention period expired
let guard = self.local.issued_merkle_trees.read().await;
let Some(entry) = guard.get(&expiration_date) else {
warn!("it seems our merkle tree has just expired!");
return Err(EcashError::ExpirationDateTooEarly);
};
entry.proof(deposits)
}
pub async fn get_issued_ticketbooks(
&self,
challenge: IssuedTicketbooksChallengeRequest,
) -> Result<IssuedTicketbooksChallengeResponseBody> {
if challenge.expiration_date < self.config.ticketbook_retention_cutoff() {
return Err(EcashError::ExpirationDateTooEarly);
}
if challenge.expiration_date > ecash_default_expiration_date() {
// we wouldn't have issued any credentials for that expiration date so no point
// in attempting to construct an ultimately empty response
return Err(EcashError::ExpirationDateTooLate);
}
let merkle_proof = self
.get_merkle_proof(challenge.expiration_date, &challenge.deposits)
.await?;
let partial_ticketbooks = self
.aux
.storage
.get_issued_ticketbooks(challenge.deposits)
.await?;
let partial_ticketbooks = partial_ticketbooks
.into_iter()
.map(|t| (t.deposit_id, t))
.collect();
Ok(IssuedTicketbooksChallengeResponseBody {
expiration_date: challenge.expiration_date,
partial_ticketbooks,
merkle_proof,
})
}
pub async fn get_issued_ticketbooks_deposits_on(
&self,
expiration: Date,
) -> Result<IssuedTicketbooksForResponseBody> {
if expiration < self.config.ticketbook_retention_cutoff() {
return Err(EcashError::ExpirationDateTooEarly);
}
// check if the entry for this expiration date is empty. if so, it might imply we have crashed/shutdown
// and not have the full data in memory
if self.local.is_merkle_empty(expiration).await {
let entry = self.get_updated_merkle_read(expiration).await?;
return Ok(IssuedTicketbooksForResponseBody {
expiration_date: expiration,
deposits: entry.deposits(),
merkle_root: entry.merkle_root(),
});
}
// I can imagine this could happen under very rare edge case when the function is called just as the retention period expired
let guard = self.local.issued_merkle_trees.read().await;
let Some(entry) = guard.get(&expiration) else {
warn!("it seems our merkle tree has just expired!");
return Err(EcashError::ExpirationDateTooEarly);
};
Ok(IssuedTicketbooksForResponseBody {
expiration_date: expiration,
deposits: entry.deposits(),
merkle_root: entry.merkle_root(),
})
}
/// Returns a boolean to indicate whether the ticket has actually been inserted
pub async fn store_verified_ticket(
&self,
ticket_data: &CredentialSpendingData,
gateway_addr: &AccountId,
) -> Result<bool> {
self.aux
.storage
.store_verified_ticket(ticket_data, gateway_addr)
.await
.map_err(Into::into)
}
pub async fn get_ticket_provider(
&self,
gateway_address: &str,
) -> Result<Option<TicketProvider>> {
self.aux
.storage
.get_ticket_provider(gateway_address)
.await
.map_err(Into::into)
}
pub async fn get_redeemable_tickets(
&self,
provider_info: &TicketProvider,
) -> Result<Vec<SerialNumberWrapper>> {
let since = provider_info
.last_batch_verification
.unwrap_or(OffsetDateTime::UNIX_EPOCH);
self.aux
.storage
.get_verified_tickets_since(provider_info.id, since)
.await
.map_err(Into::into)
}
pub async fn update_last_batch_verification(&self, provider: &TicketProvider) -> Result<()> {
Ok(self
.aux
.storage
.update_last_batch_verification(provider.id, OffsetDateTime::now_utc())
.await?)
}
pub async fn get_ticket_data_by_serial_number(
&self,
serial_number: &[u8],
) -> Result<Option<CredentialSpendingData>> {
self.aux
.storage
.get_credential_data(serial_number)
.await
.map_err(Into::into)
}
}