Skip to content

Commit

Permalink
chore(node): refactor pricing metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
maqi committed Mar 27, 2024
1 parent 0f14fe5 commit 5d638bd
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 55 deletions.
24 changes: 9 additions & 15 deletions sn_networking/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use sn_protocol::{
storage::{RecordHeader, RecordKind, RecordType},
NetworkAddress, PrettyPrintRecordKey,
};
use sn_transfers::NanoTokens;
use sn_transfers::{NanoTokens, QuotingMetrics};
use std::{
collections::{BTreeMap, HashMap},
fmt::Debug,
Expand Down Expand Up @@ -120,7 +120,7 @@ pub enum SwarmCmd {
/// GetLocalStoreCost for this node
GetLocalStoreCost {
key: RecordKey,
sender: oneshot::Sender<NanoTokens>,
sender: oneshot::Sender<(NanoTokens, QuotingMetrics)>,
},
/// Notify the node received a payment.
PaymentReceived,
Expand Down Expand Up @@ -336,19 +336,13 @@ impl SwarmDriver {
}
SwarmCmd::GetLocalStoreCost { key, sender } => {
cmd_string = "GetLocalStoreCost";
let record_exists = self
.swarm
.behaviour_mut()
.kademlia
.store_mut()
.contains(&key);
let cost = if record_exists {
NanoTokens::zero()
} else {
self.swarm.behaviour_mut().kademlia.store_mut().store_cost()
};

let _res = sender.send(cost);
let _res = sender.send(
self.swarm
.behaviour_mut()
.kademlia
.store_mut()
.store_cost(&key),
);
}
SwarmCmd::PaymentReceived => {
cmd_string = "PaymentReceived";
Expand Down
7 changes: 5 additions & 2 deletions sn_networking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ use sn_protocol::{
storage::{RecordType, RetryStrategy},
NetworkAddress, PrettyPrintKBucketKey, PrettyPrintRecordKey,
};
use sn_transfers::{MainPubkey, NanoTokens, PaymentQuote};
use sn_transfers::{MainPubkey, NanoTokens, PaymentQuote, QuotingMetrics};
use std::{
collections::{BTreeMap, BTreeSet, HashMap},
path::PathBuf,
Expand Down Expand Up @@ -515,7 +515,10 @@ impl Network {
}

/// Get the cost of storing the next record from the network
pub async fn get_local_storecost(&self, key: RecordKey) -> Result<NanoTokens> {
pub async fn get_local_storecost(
&self,
key: RecordKey,
) -> Result<(NanoTokens, QuotingMetrics)> {
let (sender, receiver) = oneshot::channel();
self.send_swarm_cmd(SwarmCmd::GetLocalStoreCost { key, sender });

Expand Down
77 changes: 52 additions & 25 deletions sn_networking/src/record_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#![allow(clippy::mutable_key_type)] // for the Bytes in NetworkAddress

use crate::target_arch::{spawn, Instant};
use crate::CLOSE_GROUP_SIZE;
use crate::{cmd::SwarmCmd, event::NetworkEvent, send_swarm_cmd};
use aes_gcm_siv::{
aead::{Aead, KeyInit, OsRng},
Expand All @@ -28,7 +29,7 @@ use sn_protocol::{
storage::{RecordHeader, RecordKind, RecordType},
NetworkAddress, PrettyPrintRecordKey,
};
use sn_transfers::NanoTokens;
use sn_transfers::{NanoTokens, QuotingMetrics, TOTAL_SUPPLY};
use std::{
borrow::Cow,
collections::{HashMap, HashSet},
Expand Down Expand Up @@ -66,6 +67,8 @@ pub struct NodeRecordStore {
/// Encyption cipher for the records, randomly generated at node startup
/// Plus a 4 byte nonce starter
encryption_details: (Aes256GcmSiv, [u8; 4]),
/// Time that this record_store got started
timestamp: Instant,
}

/// Configuration for a `DiskBackedRecordStore`.
Expand Down Expand Up @@ -194,6 +197,7 @@ impl NodeRecordStore {
record_count_metric: None,
received_payment_count: 0,
encryption_details,
timestamp: Instant::now(),
}
}

Expand Down Expand Up @@ -423,18 +427,28 @@ impl NodeRecordStore {

/// Calculate the cost to store data for our current store state
#[allow(clippy::mutable_key_type)]
pub(crate) fn store_cost(&self) -> NanoTokens {
let stored_records = self.records.len();
let cost = calculate_cost_for_records(
stored_records,
self.received_payment_count,
self.config.max_records,
);
pub(crate) fn store_cost(&self, key: &Key) -> (NanoTokens, QuotingMetrics) {
let quoting_metrics = QuotingMetrics {
records_stored: self.records.len(),
max_records: self.config.max_records,
received_payment_count: self.received_payment_count,
live_time: self.timestamp.elapsed().as_secs(),
};

let cost = if self.contains(key) {
0
} else {
calculate_cost_for_records(
quoting_metrics.records_stored,
quoting_metrics.received_payment_count,
quoting_metrics.max_records,
quoting_metrics.live_time,
)
};

// vdash metric (if modified please notify at https://github.com/happybeing/vdash/issues):
info!("Cost is now {cost:?} for {stored_records:?} stored of {MAX_RECORDS_COUNT:?} max, {:?} times got paid.",
self.received_payment_count);
NanoTokens::from(cost)
info!("Cost is now {cost:?} for quoting_metrics {quoting_metrics:?}");
(NanoTokens::from(cost), quoting_metrics)
}

/// Notify the node received a payment.
Expand Down Expand Up @@ -661,33 +675,42 @@ impl RecordStore for ClientRecordStore {
fn remove_provider(&mut self, _key: &Key, _provider: &PeerId) {}
}

// Using a linear growth function, and be tweaked by `received_payment_count` and `max_records`,
// Using a linear growth function, and be tweaked by `received_payment_count`,
// `max_records` and `live_time`(in seconds),
// to allow nodes receiving too many replication copies can still got paid,
// and gives an exponential pricing curve when storage reaches high.
// and give extra reward (lower the quoting price to gain a better chance) to long lived nodes.
fn calculate_cost_for_records(
records_stored: usize,
received_payment_count: usize,
max_records: usize,
live_time: u64,
) -> u64 {
use std::cmp::{max, min};

let ori_cost = (10 * records_stored) as u64;
let divider = max(1, records_stored / max(1, received_payment_count)) as u64;

// 1.05.powf(200) = 18157
// Gaining one step for every day that staying in the network
let reward_steps: u64 = live_time / (24 * 3600);
let base_multiplier = 1.1_f32;
let rewarder = max(1, base_multiplier.powf(reward_steps as f32) as u64);

// 1.05.powf(800) = 9E+16
// Given currently the max_records is set at 2048,
// hence setting the multiplier trigger at 90% of the max_records
let exponential_pricing_trigger = 9 * max_records / 10;
// hence setting the multiplier trigger at 60% of the max_records
let exponential_pricing_trigger = 6 * max_records / 10;

let base_multiplier = 1.05_f32;
let multiplier = max(
1,
base_multiplier.powf(records_stored.saturating_sub(exponential_pricing_trigger) as f32)
as u64,
);
let charge = max(10, ori_cost * multiplier / divider);

let charge = max(10, ori_cost.saturating_mul(multiplier) / divider / rewarder);
// Deploy an upper cap safe_guard to the store_cost
min(3456788899, charge)
min(TOTAL_SUPPLY / CLOSE_GROUP_SIZE as u64, charge)
}

#[allow(trivial_casts)]
Expand Down Expand Up @@ -742,8 +765,8 @@ mod tests {

#[test]
fn test_calculate_cost_for_records() {
let sut = calculate_cost_for_records(2049, 2050, 2048);
assert_eq!(sut, 474814770);
let sut = calculate_cost_for_records(2049, 2050, 2048, 1);
assert_eq!(sut, TOTAL_SUPPLY / CLOSE_GROUP_SIZE as u64);
}

#[test]
Expand Down Expand Up @@ -771,13 +794,13 @@ mod tests {
swarm_cmd_sender,
);

let store_cost_before = store.store_cost();
let store_cost_before = store.store_cost(&r.key);
// An initial unverified put should not write to disk
assert!(store.put(r.clone()).is_ok());
assert!(store.get(&r.key).is_none());
// Store cost should not change if no PUT has been added
assert_eq!(
store.store_cost(),
store.store_cost(&r.key),
store_cost_before,
"store cost should not change over unverified put"
);
Expand Down Expand Up @@ -1073,8 +1096,12 @@ mod tests {
for peer in peers_in_replicate_range.iter() {
let entry = peers.entry(*peer).or_insert((0, 0, 0));
if *peer == payee {
let cost =
calculate_cost_for_records(entry.0, entry.2, MAX_RECORDS_COUNT);
let cost = calculate_cost_for_records(
entry.0,
entry.2,
MAX_RECORDS_COUNT,
0,
);
entry.1 += cost;
entry.2 += 1;
}
Expand All @@ -1096,7 +1123,7 @@ mod tests {
let mut max_store_cost = 0;

for (_peer_id, stats) in peers.iter() {
let cost = calculate_cost_for_records(stats.0, stats.2, MAX_RECORDS_COUNT);
let cost = calculate_cost_for_records(stats.0, stats.2, MAX_RECORDS_COUNT, 0);
// println!("{peer_id:?}:{stats:?} with storecost to be {cost}");
received_payment_count += stats.2;
if stats.1 == 0 {
Expand Down Expand Up @@ -1187,7 +1214,7 @@ mod tests {

for peer in peers_in_close {
if let Some(stats) = peers.get(peer) {
let store_cost = calculate_cost_for_records(stats.0, stats.2, MAX_RECORDS_COUNT);
let store_cost = calculate_cost_for_records(stats.0, stats.2, MAX_RECORDS_COUNT, 0);
if store_cost < cheapest_cost {
cheapest_cost = store_cost;
payee = Some(*peer);
Expand Down
8 changes: 4 additions & 4 deletions sn_networking/src/record_store_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use libp2p::kad::{
KBucketDistance as Distance, ProviderRecord, Record, RecordKey,
};
use sn_protocol::{storage::RecordType, NetworkAddress};
use sn_transfers::NanoTokens;
use sn_transfers::{NanoTokens, QuotingMetrics};
use std::{borrow::Cow, collections::HashMap};

pub enum UnifiedRecordStore {
Expand Down Expand Up @@ -112,13 +112,13 @@ impl UnifiedRecordStore {
}
}

pub(crate) fn store_cost(&self) -> NanoTokens {
pub(crate) fn store_cost(&self, key: &RecordKey) -> (NanoTokens, QuotingMetrics) {
match self {
Self::Client(_) => {
warn!("Calling store cost calculation at Client. This should not happen");
NanoTokens::zero()
(NanoTokens::zero(), Default::default())
}
Self::Node(store) => store.store_cost(),
Self::Node(store) => store.store_cost(key),
}
}

Expand Down
9 changes: 7 additions & 2 deletions sn_node/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ impl Node {
let store_cost = network.get_local_storecost(record_key.clone()).await;

match store_cost {
Ok(cost) => {
Ok((cost, quoting_metrics)) => {
if cost == NanoTokens::zero() {
QueryResponse::GetStoreCost {
quote: Err(ProtocolError::RecordExists(
Expand All @@ -504,7 +504,12 @@ impl Node {
}
} else {
QueryResponse::GetStoreCost {
quote: Self::create_quote_for_storecost(network, cost, &address),
quote: Self::create_quote_for_storecost(
network,
cost,
&address,
&quoting_metrics,
),
payment_address,
peer_address: NetworkAddress::from_peer(self_id),
}
Expand Down
13 changes: 10 additions & 3 deletions sn_node/src/quote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,18 @@
use crate::{node::Node, Error, Result};
use sn_networking::Network;
use sn_protocol::{error::Error as ProtocolError, NetworkAddress};
use sn_transfers::{NanoTokens, PaymentQuote};
use sn_transfers::{NanoTokens, PaymentQuote, QuotingMetrics};

impl Node {
pub(crate) fn create_quote_for_storecost(
network: &Network,
cost: NanoTokens,
address: &NetworkAddress,
quoting_metrics: &QuotingMetrics,
) -> Result<PaymentQuote, ProtocolError> {
let content = address.as_xorname().unwrap_or_default();
let timestamp = std::time::SystemTime::now();
let bytes = PaymentQuote::bytes_for_signing(content, cost, timestamp);
let bytes = PaymentQuote::bytes_for_signing(content, cost, timestamp, quoting_metrics);

let Ok(signature) = network.sign(&bytes) else {
return Err(ProtocolError::QuoteGenerationFailed);
Expand All @@ -29,6 +30,7 @@ impl Node {
content,
cost,
timestamp,
quoting_metrics: quoting_metrics.clone(),
signature,
};

Expand All @@ -54,7 +56,12 @@ impl Node {
}

// check sig
let bytes = PaymentQuote::bytes_for_signing(quote.content, quote.cost, quote.timestamp);
let bytes = PaymentQuote::bytes_for_signing(
quote.content,
quote.cost,
quote.timestamp,
&quote.quoting_metrics,
);
let signature = quote.signature;
if !self.network.verify(&bytes, &signature) {
return Err(Error::InvalidQuoteSignature);
Expand Down
3 changes: 2 additions & 1 deletion sn_transfers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ pub use genesis::{
calculate_royalties_fee, create_faucet_wallet, create_first_cash_note_from_key,
get_faucet_data_dir, is_genesis_parent_tx, is_genesis_spend, load_genesis_wallet,
Error as GenesisError, GENESIS_CASHNOTE, GENESIS_CASHNOTE_SK, NETWORK_ROYALTIES_PK,
TOTAL_SUPPLY,
};
pub use wallet::{
bls_secret_from_hex, Error as WalletError, HotWallet, Payment, PaymentQuote,
bls_secret_from_hex, Error as WalletError, HotWallet, Payment, PaymentQuote, QuotingMetrics,
Result as WalletResult, WatchOnlyWallet, QUOTE_EXPIRATION_SECS,
};

Expand Down
2 changes: 1 addition & 1 deletion sn_transfers/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ use crate::{NanoTokens, UniquePubkey};
use wallet_file::wallet_file_name;

pub use self::{
data_payments::{Payment, PaymentQuote, QUOTE_EXPIRATION_SECS},
data_payments::{Payment, PaymentQuote, QuotingMetrics, QUOTE_EXPIRATION_SECS},
error::{Error, Result},
hot_wallet::HotWallet,
keys::bls_secret_from_hex,
Expand Down

0 comments on commit 5d638bd

Please sign in to comment.