ability to reuse free passes from the storage for new gateways
This commit is contained in:
@@ -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(())
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user