ability to reuse free passes from the storage for new gateways

This commit is contained in:
Jędrzej Stuczyński
2024-03-11 17:38:15 +00:00
parent 9b10871efb
commit 0ac8098dad
10 changed files with 206 additions and 40 deletions
+10 -6
View File
@@ -49,6 +49,7 @@ impl<C, St: Storage> BandwidthController<C, St> {
/// It marks any retrieved intermediate credentials as expired.
pub async fn get_next_usable_credential(
&self,
gateway_id: &str,
) -> Result<RetrievedCredential, BandwidthControllerError>
where
<St as Storage>::StorageError: Send + Sync + 'static,
@@ -56,7 +57,7 @@ impl<C, St: Storage> BandwidthController<C, St> {
loop {
let Some(maybe_next) = self
.storage
.get_next_unspent_credential()
.get_next_unspent_credential(gateway_id)
.await
.map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))?
else {
@@ -114,12 +115,13 @@ impl<C, St: Storage> BandwidthController<C, St> {
pub async fn prepare_bandwidth_credential(
&self,
gateway_id: &str,
) -> Result<PreparedCredential, BandwidthControllerError>
where
C: DkgQueryClient + Sync + Send,
<St as Storage>::StorageError: Send + Sync + 'static,
{
let retrieved_credential = self.get_next_usable_credential().await?;
let retrieved_credential = self.get_next_usable_credential(gateway_id).await?;
let epoch_id = retrieved_credential.credential.epoch_id();
let credential_id = retrieved_credential.credential_id;
@@ -137,14 +139,16 @@ impl<C, St: Storage> BandwidthController<C, St> {
})
}
pub async fn consume_credential(&self, id: i64) -> Result<(), BandwidthControllerError>
pub async fn consume_credential(
&self,
id: i64,
gateway_id: &str,
) -> Result<(), BandwidthControllerError>
where
<St as Storage>::StorageError: Send + Sync + 'static,
{
// JS: shouldn't we send some contract/validator/gateway message here to actually, you know,
// consume it?
self.storage
.consume_coconut_credential(id)
.consume_coconut_credential(id, gateway_id)
.await
.map_err(|err| BandwidthControllerError::CredentialStorageError(Box::new(err)))
}
@@ -626,11 +626,13 @@ impl<C, St> GatewayClient<C, St> {
});
}
let gateway_id = self.gateway_identity().to_base58_string();
let prepared_credential = self
.bandwidth_controller
.as_ref()
.unwrap()
.prepare_bandwidth_credential()
.prepare_bandwidth_credential(&gateway_id)
.await?;
self.claim_coconut_bandwidth(prepared_credential.data)
@@ -638,7 +640,7 @@ impl<C, St> GatewayClient<C, St> {
self.bandwidth_controller
.as_ref()
.unwrap()
.consume_credential(prepared_credential.credential_id)
.consume_credential(prepared_credential.credential_id, &gateway_id)
.await?;
Ok(())
@@ -0,0 +1,31 @@
/*
* Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
DROP TABLE coconut_credentials;
CREATE TABLE coconut_credentials
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
-- introduce a way for us to introduce breaking changes in serialization
serialization_revision INTEGER NOT NULL,
-- the best we can do without enums
credential_type TEXT CHECK ( credential_type IN ('BandwidthVoucher', 'FreeBandwidthPass') ) NOT NULL,
credential_data BLOB NOT NULL,
epoch_id INTEGER NOT NULL,
-- this field is only really applicable to free passes
expired BOOLEAN NOT NULL
);
-- for bandwidth vouchers there's going to be only a single entry; for freepasses there can be as many as there are gateways
CREATE TABLE credential_usage
(
credential_id INTEGER NOT NULL REFERENCES coconut_credentials (id),
gateway_id_bs58 TEXT NOT NULL,
-- no matter credential type, we can't spend the same credential with the same gateway multiple times
UNIQUE (credential_id, gateway_id_bs58)
)
@@ -0,0 +1,7 @@
/*
* Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
-- we never actually dropped that table...
DROP TABLE erc20_credentials;
@@ -1,7 +1,7 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::StoredIssuedCredential;
use crate::models::{CredentialUsage, StoredIssuedCredential};
use std::sync::Arc;
use tokio::sync::RwLock;
@@ -12,7 +12,8 @@ pub struct CoconutCredentialManager {
#[derive(Default)]
struct CoconutCredentialManagerInner {
data: Vec<StoredIssuedCredential>,
credentials: Vec<StoredIssuedCredential>,
credential_usage: Vec<CredentialUsage>,
_next_id: i64,
}
@@ -41,25 +42,67 @@ impl CoconutCredentialManager {
) {
let mut inner = self.inner.write().await;
let id = inner.next_id();
inner.data.push(StoredIssuedCredential {
inner.credentials.push(StoredIssuedCredential {
id,
serialization_revision,
credential_data: credential_data.to_vec(),
credential_type,
epoch_id,
consumed: false,
expired: false,
})
}
/// Tries to retrieve one of the stored, unused credentials.
pub async fn get_next_unspent_credential(&self) -> Option<StoredIssuedCredential> {
let creds = self.inner.read().await;
creds
.data
async fn bandwidth_voucher_spent(&self, id: i64) -> bool {
self.inner
.read()
.await
.credential_usage
.iter()
.find(|c| !c.consumed && !c.expired)
.cloned()
.any(|c| c.credential_id == id)
}
async fn freepass_spent(&self, id: i64, gateway_id: &str) -> bool {
self.inner
.read()
.await
.credential_usage
.iter()
.any(|c| c.credential_id == id && c.gateway_id_bs58 == gateway_id)
}
/// Tries to retrieve one of the stored, unused credentials.
pub async fn get_next_unspect_bandwidth_voucher(&self) -> Option<StoredIssuedCredential> {
let guard = self.inner.read().await;
for credential in guard
.credentials
.iter()
.filter(|c| c.credential_type == "BandwidthVoucher")
{
if !self.bandwidth_voucher_spent(credential.id).await {
return Some(credential.clone());
}
}
None
}
pub async fn get_next_unspect_freepass(
&self,
gateway_id: &str,
) -> Option<StoredIssuedCredential> {
let guard = self.inner.read().await;
for credential in guard
.credentials
.iter()
.filter(|c| c.credential_type == "FreeBandwidthPass")
{
if credential.expired {
continue;
}
if !self.freepass_spent(credential.id, gateway_id).await {
return Some(credential.clone());
}
}
None
}
/// Consumes in the database the specified credential.
@@ -67,11 +110,12 @@ impl CoconutCredentialManager {
/// # Arguments
///
/// * `id`: Database id.
pub async fn consume_coconut_credential(&self, id: i64) {
let mut creds = self.inner.write().await;
if let Some(cred) = creds.data.get_mut(id as usize) {
cred.consumed = true;
}
pub async fn consume_coconut_credential(&self, id: i64, gateway_id: &str) {
let mut guard = self.inner.write().await;
guard.credential_usage.push(CredentialUsage {
credential_id: id,
gateway_id_bs58: gateway_id.to_string(),
});
}
/// Marks the specified credential as expired
@@ -81,7 +125,7 @@ impl CoconutCredentialManager {
/// * `id`: Id of the credential to mark as expired.
pub async fn mark_expired(&self, id: i64) {
let mut creds = self.inner.write().await;
if let Some(cred) = creds.data.get_mut(id as usize) {
if let Some(cred) = creds.credentials.get_mut(id as usize) {
cred.expired = true;
}
}
@@ -27,19 +27,52 @@ impl CoconutCredentialManager {
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO coconut_credentials(serialization_revision, credential_type, credential_data, epoch_id, consumed, expired)
VALUES (?, ?, ?, ?, false, false)
INSERT INTO coconut_credentials(serialization_revision, credential_type, credential_data, epoch_id, expired)
VALUES (?, ?, ?, ?, false)
"#,
serialization_revision, credential_type, credential_data, epoch_id
).execute(&self.connection_pool).await?;
Ok(())
}
pub async fn get_next_unspent_credential(
pub async fn get_next_unspect_freepass(
&self,
gateway_id: &str,
) -> Result<Option<StoredIssuedCredential>, sqlx::Error> {
// get a credential of freepass type that doesn't appear in `credential_usage` for the provided gateway_id
sqlx::query_as(
r#"
SELECT *
FROM coconut_credentials
WHERE coconut_credentials.credential_type == "FreeBandwidthPass"
AND NOT EXISTS (SELECT 1
FROM credential_usage
WHERE credential_usage.credential_id = coconut_credential.id
AND credential_usage.gateway_id_bs58 == ?)
ORDER BY coconut_credentials.id
LIMIT 1
"#,
)
.bind(gateway_id)
.fetch_optional(&self.connection_pool)
.await
}
pub async fn get_next_unspect_bandwidth_voucher(
&self,
) -> Result<Option<StoredIssuedCredential>, sqlx::Error> {
// get a credential of bandwidth voucher type that doesn't appear in `credential_usage` for any gateway_id
sqlx::query_as(
"SELECT * FROM coconut_credentials WHERE NOT consumed AND NOT expired LIMIT 1",
r#"
SELECT *
FROM coconut_credentials
WHERE coconut_credentials.credential_type == "BandwidthVoucher"
AND NOT EXISTS (SELECT 1
FROM credential_usage
WHERE credential_usage.credential_id = coconut_credential.id)
ORDER BY coconut_credentials.id
LIMIT 1
"#,
)
.fetch_optional(&self.connection_pool)
.await
@@ -50,10 +83,16 @@ impl CoconutCredentialManager {
/// # Arguments
///
/// * `id`: Database id.
pub async fn consume_coconut_credential(&self, id: i64) -> Result<(), sqlx::Error> {
/// * `gateway_id`: id of the gateway that received the credential
pub async fn consume_coconut_credential(
&self,
id: i64,
gateway_id: &str,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"UPDATE coconut_credentials SET consumed = TRUE WHERE id = ?",
id
"INSERT INTO credential_usage (credential_id, gateway_id_bs58) VALUES (?, ?)",
id,
gateway_id
)
.execute(&self.connection_pool)
.await?;
@@ -44,16 +44,30 @@ impl Storage for EphemeralStorage {
async fn get_next_unspent_credential(
&self,
gateway_id: &str,
) -> Result<Option<StoredIssuedCredential>, Self::StorageError> {
// first try to get a free pass if available, otherwise fallback to bandwidth voucher
let maybe_freepass = self
.coconut_credential_manager
.get_next_unspect_freepass(gateway_id)
.await;
if maybe_freepass.is_some() {
return Ok(maybe_freepass);
}
Ok(self
.coconut_credential_manager
.get_next_unspent_credential()
.get_next_unspect_bandwidth_voucher()
.await)
}
async fn consume_coconut_credential(&self, id: i64) -> Result<(), StorageError> {
async fn consume_coconut_credential(
&self,
id: i64,
gateway_id: &str,
) -> Result<(), StorageError> {
self.coconut_credential_manager
.consume_coconut_credential(id)
.consume_coconut_credential(id, gateway_id)
.await;
Ok(())
+6 -1
View File
@@ -26,7 +26,6 @@ pub struct StoredIssuedCredential {
pub credential_type: String,
pub epoch_id: u32,
pub consumed: bool,
pub expired: bool,
}
@@ -37,3 +36,9 @@ pub struct StorableIssuedCredential<'a> {
pub epoch_id: u32,
}
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
pub struct CredentialUsage {
pub credential_id: i64,
pub gateway_id_bs58: String,
}
@@ -76,16 +76,30 @@ impl Storage for PersistentStorage {
async fn get_next_unspent_credential(
&self,
gateway_id: &str,
) -> Result<Option<StoredIssuedCredential>, Self::StorageError> {
// first try to get a free pass if available, otherwise fallback to bandwidth voucher
let maybe_freepass = self
.coconut_credential_manager
.get_next_unspect_freepass(gateway_id)
.await?;
if maybe_freepass.is_some() {
return Ok(maybe_freepass);
}
Ok(self
.coconut_credential_manager
.get_next_unspent_credential()
.get_next_unspect_bandwidth_voucher()
.await?)
}
async fn consume_coconut_credential(&self, id: i64) -> Result<(), StorageError> {
async fn consume_coconut_credential(
&self,
id: i64,
gateway_id: &str,
) -> Result<(), StorageError> {
self.coconut_credential_manager
.consume_coconut_credential(id)
.consume_coconut_credential(id, gateway_id)
.await?;
Ok(())
+7 -1
View File
@@ -18,6 +18,7 @@ pub trait Storage: Send + Sync {
/// that is also not marked as expired
async fn get_next_unspent_credential(
&self,
gateway_id: &str,
) -> Result<Option<StoredIssuedCredential>, Self::StorageError>;
/// Marks as consumed in the database the specified credential.
@@ -25,7 +26,12 @@ pub trait Storage: Send + Sync {
/// # Arguments
///
/// * `id`: Id of the credential to be consumed.
async fn consume_coconut_credential(&self, id: i64) -> Result<(), Self::StorageError>;
/// * `gateway_id`: id of the gateway that received the credential.
async fn consume_coconut_credential(
&self,
id: i64,
gateway_id: &str,
) -> Result<(), Self::StorageError>;
/// Marks the specified credential as expired
///