293 changes: 238 additions & 55 deletions safenode/src/domain/storage/chunks/mod.rs
Expand Up @@ -10,86 +10,269 @@ mod chunk;

pub use self::chunk::Chunk;

use super::{ChunkAddress, Error, Result};
use super::{
error::{Error, Result},
list_files_in, prefix_tree_path, ChunkAddress,
};

use clru::CLruCache;
use std::{num::NonZeroUsize, sync::Arc};
use tokio::sync::RwLock;
use tracing::trace;
use bytes::Bytes;
use hex::FromHex;
use std::{
fmt::{self, Display, Formatter},
io::{self, ErrorKind},
path::{Path, PathBuf},
};
use tokio::{
fs::{create_dir_all, read, remove_file, File},
io::AsyncWriteExt,
};
use tracing::info;
use xor_name::{XorName, XOR_NAME_LEN};

/// We do not want to hinder storage, but a number is needed for the cache ctor.
/// This will be removed as drive store is implemented.
const CHUNKS_CACHE_SIZE: usize = 200 * 1024 * 1024;
const CHUNKS_STORE_DIR_NAME: &str = "chunks";

/// Operations on data chunks.
#[derive(Clone)]
#[derive(Clone, Debug)]
pub(crate) struct ChunkStorage {
cache: Arc<RwLock<CLruCache<ChunkAddress, Chunk>>>,
file_store_path: PathBuf,
}

impl ChunkStorage {
pub(crate) fn new() -> Self {
let capacity =
NonZeroUsize::new(CHUNKS_CACHE_SIZE).expect("Failed to create in-memory Chunk storage");
/// Creates a new `ChunkStorage` at the specified root location
///
/// If the location specified already contains a `ChunkStorage`, it is simply used
pub(crate) fn new(path: &Path) -> Self {
Self {
cache: Arc::new(RwLock::new(CLruCache::new(capacity))),
file_store_path: path.join(CHUNKS_STORE_DIR_NAME),
}
}

// Read chunk from local store
#[allow(unused)]
pub(crate) fn addrs(&self) -> Vec<ChunkAddress> {
list_files_in(&self.file_store_path)
.iter()
.filter_map(|filepath| Self::chunk_filepath_to_address(filepath).ok())
.collect()
}

fn chunk_filepath_to_address(path: &Path) -> Result<ChunkAddress> {
let filename = path
.file_name()
.ok_or_else(|| Error::PathIsNotAFile(path.to_path_buf()))?
.to_str()
.ok_or_else(|| Error::InvalidFilename(path.to_path_buf()))?;

let xorname = XorName(<[u8; XOR_NAME_LEN]>::from_hex(filename)?);
Ok(ChunkAddress::new(xorname))
}

fn chunk_addr_to_filepath(&self, addr: &ChunkAddress) -> Result<PathBuf> {
let xorname = *addr.name();
let path = prefix_tree_path(&self.file_store_path, xorname);
let filename = hex::encode(xorname);
maqi marked this conversation as resolved.
Show resolved Hide resolved
Ok(path.join(filename))
}

/// This is to be used when a node is shrinking the address range it is responsible for.
#[allow(unused)]
pub(super) async fn remove(&self, address: &ChunkAddress) -> Result<()> {
oetyng marked this conversation as resolved.
Show resolved Hide resolved
debug!("Removing chunk, {:?}", address);
let filepath = self.chunk_addr_to_filepath(address)?;
remove_file(filepath).await?;
Ok(())
}

pub(crate) async fn get(&self, address: &ChunkAddress) -> Result<Chunk> {
trace!("Getting Chunk: {address:?}");
if let Some(chunk) = self.cache.read().await.peek(address) {
Ok(chunk.clone())
} else {
Err(Error::ChunkNotFound(*address))
trace!("Getting chunk {:?}", address);

let file_path = self.chunk_addr_to_filepath(address)?;
match read(file_path).await {
Ok(bytes) => {
let chunk = Chunk::new(Bytes::from(bytes));
if chunk.address() != address {
// This can happen if the content read is empty, or incomplete,
// possibly due to an issue with the OS synchronising to disk,
// resulting in a mismatch with recreated address of the Chunk.
Err(Error::ChunkNotFound(*address))
} else {
Ok(chunk)
}
}
Err(io_error @ io::Error { .. }) if io_error.kind() == ErrorKind::NotFound => {
Err(Error::ChunkNotFound(*address))
}
Err(other) => Err(other.into()),
}
}

/// Store a chunk in the local in-memory store unless it is already there
/// Store a chunk in the local disk store unless it is already there
pub(crate) async fn store(&self, chunk: &Chunk) -> Result<()> {
let address = chunk.address();
trace!("About to store Chunk: {address:?}");

let _ = self.cache.write().await.try_put_or_modify(
*address,
|addr, _| {
trace!("Chunk successfully stored: {addr:?}");
Ok::<Chunk, Error>(chunk.clone())
},
|addr, _, _| {
trace!("Chunk data already exists in cache, not storing: {addr:?}");
Ok(())
},
(),
)?;
let addr = chunk.address();
let filepath = self.chunk_addr_to_filepath(addr)?;

if filepath.exists() {
info!(
"{}: Chunk data already exists, not storing: {:?}",
self, addr
);
// Nothing more to do here
return Ok(());
}

// Store the data on disk
trace!("Storing chunk {addr:?}");
if let Some(dirs) = filepath.parent() {
create_dir_all(dirs).await?;
}

let mut file = File::create(filepath).await?;

file.write_all(chunk.value()).await?;
// Sync OS data to disk to reduce the chances of
// concurrent reading failing by reading an empty/incomplete file.
file.sync_data().await?;

trace!("Stored new chunk {addr:?}");

Ok(())
}
}

#[allow(dead_code)]
pub(super) async fn addrs(&self) -> Vec<ChunkAddress> {
self.cache
.read()
.await
.iter()
.map(|(addr, _)| *addr)
.collect()
impl Display for ChunkStorage {
fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
write!(formatter, "ChunkStorage")
}
}

#[cfg(test)]
mod tests {
use super::*;

use eyre::{eyre, Result};
use futures::future::join_all;
use rand::{rngs::OsRng, Rng};
use rayon::{current_num_threads, prelude::*};
use tempfile::tempdir;

fn init_file_store() -> ChunkStorage {
let root = tempdir().expect("Failed to create temporary directory for chunk disk store");
ChunkStorage::new(root.path())
}

#[allow(dead_code)]
pub(super) async fn remove_chunk(&self, address: &ChunkAddress) -> Result<()> {
trace!("Removing Chunk: {address:?}");
if self.cache.write().await.pop(address).is_some() {
Ok(())
} else {
Err(Error::ChunkNotFound(*address))
#[tokio::test]
async fn test_write_read_chunk() {
let storage = init_file_store();
// Test that a range of different chunks return the written chunk.
for _ in 0..10 {
let chunk = Chunk::new(random_bytes(100));

storage.store(&chunk).await.expect("Failed to write chunk.");

let read_chunk = storage
.get(chunk.address())
.await
.expect("Failed to read chunk.");

assert_eq!(chunk.value(), read_chunk.value());
}
}
}

impl Default for ChunkStorage {
fn default() -> Self {
Self::new()
#[tokio::test]
async fn test_write_read_async_multiple_chunks() {
let store = init_file_store();
let size = 100;
let chunks: Vec<Chunk> = std::iter::repeat_with(|| Chunk::new(random_bytes(size)))
.take(7)
.collect();
write_and_read_chunks(&chunks, store).await;
}

#[tokio::test]
async fn test_write_read_async_multiple_identical_chunks() {
let store = init_file_store();
let chunks: Vec<Chunk> = std::iter::repeat(Chunk::new(Bytes::from("test_concurrent")))
.take(7)
.collect();
write_and_read_chunks(&chunks, store).await;
}

#[tokio::test]
async fn test_read_chunk_empty_file() -> Result<()> {
let storage = init_file_store();

let chunk = Chunk::new(random_bytes(100));
let address = chunk.address();

// Create chunk file but with empty content.
let filepath = storage.chunk_addr_to_filepath(address)?;
if let Some(dirs) = filepath.parent() {
create_dir_all(dirs).await?;
}
let mut file = File::create(&filepath).await?;
file.write_all(b"").await?;

// Trying to read the chunk shall return ChunkNotFound error since
// its content shouldn't match chunk address.
match storage.get(address).await {
Ok(chunk) => Err(eyre!(
"Unexpected Chunk read (size: {}): {chunk:?}",
chunk.value().len()
)),
Err(Error::ChunkNotFound(addr)) => {
assert_eq!(addr, *address, "Wrong Chunk address returned in error");
Ok(())
}
Err(other) => Err(eyre!("Unexpected Error type returned: {other:?}")),
}
}

async fn write_and_read_chunks(chunks: &[Chunk], storage: ChunkStorage) {
// Write all chunks.
let mut tasks = Vec::new();
for c in chunks.iter() {
tasks.push(async { storage.store(c).await.map(|_| *c.address()) });
}
let results = join_all(tasks).await;

// Read all chunks.
let tasks = results.iter().flatten().map(|addr| storage.get(addr));
let results = join_all(tasks).await;
let read_chunks: Vec<&Chunk> = results.iter().flatten().collect();

// Verify all written were read.
assert!(chunks
.par_iter()
.all(|c| read_chunks.iter().any(|r| r.value() == c.value())))
}

/// Generates a random vector using provided `length`.
fn random_bytes(length: usize) -> Bytes {
let threads = current_num_threads();

if threads > length {
let mut rng = OsRng;
return ::std::iter::repeat(())
.map(|()| rng.gen::<u8>())
.take(length)
.collect();
}

let per_thread = length / threads;
let remainder = length % threads;

let mut bytes: Vec<u8> = (0..threads)
.par_bridge()
.map(|_| vec![0u8; per_thread])
.map(|mut bytes| {
let bytes = bytes.as_mut_slice();
rand::thread_rng().fill(bytes);
bytes.to_owned()
})
.flatten()
.collect();

bytes.extend(vec![0u8; remainder]);

Bytes::from(bytes)
}
}
59 changes: 57 additions & 2 deletions safenode/src/domain/storage/error.rs
Expand Up @@ -8,11 +8,13 @@

use super::{
register::{EntryHash, User},
ChunkAddress, RegisterAddress,
ChunkAddress, DbcAddress, RegisterAddress,
};

use sn_dbc::{Error as DbcError, SignedSpend};

use serde::{Deserialize, Serialize};
use std::{fmt::Debug, result};
use std::{fmt::Debug, path::PathBuf, result};
use thiserror::Error;

/// A specialised `Result` type for protocol crate.
Expand All @@ -25,6 +27,12 @@ pub enum Error {
/// Chunk not found.
#[error("Chunk not found: {0:?}")]
ChunkNotFound(ChunkAddress),
/// No filename found
#[error("Path is not a file: {0}")]
PathIsNotAFile(PathBuf),
/// Invalid filename
#[error("Invalid chunk filename: {0}")]
InvalidFilename(PathBuf),
/// Register not found.
#[error("Register not found: {0:?}")]
RegisterNotFound(RegisterAddress),
Expand Down Expand Up @@ -65,7 +73,54 @@ pub enum Error {
/// Data authority provided is invalid.
#[error("Provided PublicKey could not validate signature {0:?}")]
InvalidSignature(bls::PublicKey),
/// Spend not found.
#[error("Spend not found: {0:?}")]
SpendNotFound(DbcAddress),
/// A double spend attempt was detected.
#[error("A double spend attempt was detected. Incoming and existing spend are not the same: {new:?}. Existing: {existing:?}")]
DoubleSpendAttempt {
/// New spend that we received.
new: Box<SignedSpend>,
/// Existing spend of same id that we already have.
existing: Box<SignedSpend>,
},
/// We were notified about a double spend attempt, but they were for different dbcs.
#[error("We were notified about a double spend attempt, but they were for different dbcs: {0:?}. Existing: {1:?}")]
NotADoubleSpendAttempt(Box<SignedSpend>, Box<SignedSpend>),
/// An error from the `sn_dbc` crate.
#[error("Dbc error: {0}")]
Dbcs(String),
/// Bincode error.
#[error("Bincode error:: {0}")]
Bincode(String),
/// I/O error.
#[error("I/O error: {0}")]
Io(String),
/// Hex decoding error.
#[error("Hex decoding error:: {0}")]
HexDecoding(String),
}

impl From<DbcError> for Error {
fn from(error: DbcError) -> Self {
Error::Dbcs(error.to_string())
}
}

impl From<bincode::Error> for Error {
fn from(error: bincode::Error) -> Self {
Self::Bincode(error.to_string())
}
}

impl From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
Self::Io(error.to_string())
}
}

impl From<hex::FromHexError> for Error {
fn from(error: hex::FromHexError) -> Self {
Self::HexDecoding(error.to_string())
}
}
36 changes: 36 additions & 0 deletions safenode/src/domain/storage/mod.rs
Expand Up @@ -20,3 +20,39 @@ pub use self::{
};

pub(crate) use self::{chunks::ChunkStorage, registers::RegisterStorage, spends::SpendStorage};

use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use xor_name::XorName;

const BIT_TREE_DEPTH: usize = 20;

// Helper that returns the prefix tree path of depth BIT_TREE_DEPTH for a given xorname
// Example:
// - with a xorname with starting bits `010001110110....`
// - and a BIT_TREE_DEPTH of `6`
// returns the path `ROOT_PATH/0/1/0/0/0/1`
fn prefix_tree_path(root: &Path, xorname: XorName) -> PathBuf {
let bin = format!("{xorname:b}");
let prefix_dir_path: PathBuf = bin.chars().take(BIT_TREE_DEPTH).map(String::from).collect();
root.join(prefix_dir_path)
}

fn list_files_in(path: &Path) -> Vec<PathBuf> {
if !path.exists() {
return vec![];
}

WalkDir::new(path)
.into_iter()
.filter_map(|e| match e {
Ok(direntry) => Some(direntry),
Err(err) => {
warn!("Store: failed to process filesystem entry: {}", err);
None
}
})
.filter(|entry| entry.file_type().is_file())
.map(|entry| entry.path().to_path_buf())
.collect()
}
510 changes: 328 additions & 182 deletions safenode/src/domain/storage/registers/mod.rs

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions safenode/src/domain/storage/registers/register/mod.rs
Expand Up @@ -463,8 +463,7 @@ mod tests {
// Helpers for tests

fn sign_register_op(mut op: RegisterOp<Entry>, sk: &SecretKey) -> Result<RegisterOp<Entry>> {
let bytes =
bincode::serialize(&op.crdt_op).map_err(|err| Error::Bincode(err.to_string()))?;
let bytes = bincode::serialize(&op.crdt_op)?;
let signature = sk.sign(bytes);
op.signature = Some(signature);
Ok(op)
Expand Down
195 changes: 141 additions & 54 deletions safenode/src/domain/storage/spends.rs
Expand Up @@ -6,58 +6,71 @@
// KIND, either express or implied. Please review the Licences for the specific language governing
// permissions and limitations relating to use of the SAFE Network Software.

use crate::domain::{
node_transfers::{Error, Result},
storage::DbcAddress,
};
//! For every DbcId, there is a collection of transactions.
//! Every transaction has a set of peers who reported that they hold this transaction.
//! At a higher level, a peer will store a spend to `valid_spends` if the dbc checks out as valid, _and_ the parents of the dbc checks out as valid.
//! A peer will move a spend from `valid_spends` to `double_spends` if it receives another tx id for the same dbc id.
//! A peer will never again store such a spend to `valid_spends`.

use super::{prefix_tree_path, DbcAddress, Error, Result};

use sn_dbc::{DbcId, SignedSpend};

use bincode::{deserialize, serialize};
use std::{
collections::BTreeMap,
fmt::{self, Display, Formatter},
sync::Arc,
path::{Path, PathBuf},
};
use tokio::{
fs::{create_dir_all, read, remove_file, File},
io::AsyncWriteExt,
};
use tokio::sync::RwLock;
use tracing::trace;
use xor_name::XorName;

/// For every DbcId, there is a collection of transactions.
/// Every transaction has a set of peers who reported that they hold this transaction.
/// At a higher level, a peer will store a spend to `valid_spends` if the dbc checks out as valid, _and_ the parents of the dbc checks out as valid.
/// A peer will move a spend from `valid_spends` to `double_spends` if it receives another tx id for the same dbc id.
/// A peer will never again store such a spend to `valid_spends`.
type ValidSpends<V> = Arc<RwLock<BTreeMap<DbcAddress, V>>>;
type DoubleSpends<V> = Arc<RwLock<BTreeMap<DbcAddress, (V, V)>>>;
const VALID_SPENDS_STORE_DIR_NAME: &str = "valid_spends";
const DOUBLE_SPENDS_STORE_DIR_NAME: &str = "double_spends";

/// Storage of Dbc spends.
///
/// NB: The used space measurement is just an appromixation, and is not exact.
/// Later, when all data types have this, we can verify that it is not wildly different.
#[derive(Clone, Debug)]
pub(crate) struct SpendStorage {
valid_spends: ValidSpends<SignedSpend>,
double_spends: DoubleSpends<SignedSpend>,
valid_spends_path: PathBuf,
double_spends_path: PathBuf,
}

impl SpendStorage {
pub(crate) fn new() -> Self {
pub(crate) fn new(path: &Path) -> Self {
Self {
valid_spends: Arc::new(RwLock::new(BTreeMap::new())),
double_spends: Arc::new(RwLock::new(BTreeMap::new())),
valid_spends_path: path.join(VALID_SPENDS_STORE_DIR_NAME),
double_spends_path: path.join(DOUBLE_SPENDS_STORE_DIR_NAME),
}
}

// Read Spend from local store.
pub(crate) async fn get(&self, address: DbcAddress) -> Result<SignedSpend> {
pub(crate) async fn get(&self, address: &DbcAddress) -> Result<SignedSpend> {
trace!("Getting Spend: {address:?}");
if let Some(spend) = self.valid_spends.read().await.get(&address) {
Ok(spend.clone())
} else {
Err(Error::SpendNotFound(address))
let file_path = self.address_to_filepath(address, &self.valid_spends_path)?;
match read(file_path).await {
Ok(bytes) => {
let spend: SignedSpend = deserialize(&bytes)?;
if address == &dbc_address(spend.dbc_id()) {
Ok(spend)
} else {
// This can happen if the content read is empty, or incomplete,
// possibly due to an issue with the OS synchronising to disk,
// resulting in a mismatch with recreated address of the Spend.
Err(Error::SpendNotFound(*address))
oetyng marked this conversation as resolved.
Show resolved Hide resolved
}
}
Err(_) => Err(Error::SpendNotFound(*address)),
}
}

/// Try store a spend to local file system.
///
/// We need to check that the parent is spent before
/// we try add here.
/// If a double spend attempt is detected, a `DoubleSpendAttempt` error
Expand All @@ -68,20 +81,31 @@ impl SpendStorage {
/// could otherwise happen in parallel in different threads.)
pub(crate) async fn try_add(&mut self, signed_spend: &SignedSpend) -> Result<()> {
self.validate(signed_spend).await?;
let addr = dbc_address(signed_spend.dbc_id());

let address = dbc_address(signed_spend.dbc_id());

let mut valid_spends = self.valid_spends.write().await;
let filepath = self.address_to_filepath(&addr, &self.valid_spends_path)?;

let replaced = valid_spends.insert(address, signed_spend.clone());
if replaced.is_some() {
// The `&mut self` signature prevents any race,
// so this is a second layer of security on that,
// if some developer by mistake removes the &mut self.
drop(valid_spends);
if filepath.exists() {
self.validate(signed_spend).await?;
return Ok(());
}

// Store the spend to local file system.
trace!("Storing spend {addr:?}.");
if let Some(dirs) = filepath.parent() {
create_dir_all(dirs).await?;
}

let mut file = File::create(filepath).await?;

let bytes = serialize(signed_spend)?;
file.write_all(&bytes).await?;
// Sync up OS data to disk to reduce the chances of
// concurrent reading failing by reading an empty/incomplete file.
file.sync_data().await?;

trace!("Stored new spend {addr:?}.");

Ok(())
}

Expand All @@ -98,25 +122,25 @@ impl SpendStorage {

let address = dbc_address(signed_spend.dbc_id());

// The spend id is from the spend hash. That makes sure that a spend is compared based
// on all of `DbcTransaction`, `DbcReason`, `DbcId` and `BlindedAmount` being equal.
let mut valid_spends = self.valid_spends.write().await;
if let Some(existing) = valid_spends.get(&address).cloned() {
if let Ok(existing) = self.get(&address).await {
let tamper_attempted = signed_spend.spend.hash() != existing.spend.hash();
if tamper_attempted {
let mut double_spends = self.double_spends.write().await;
let _replaced =
double_spends.insert(address, (existing.clone(), signed_spend.clone()));
// We don't error if the double spend failed, as we rather want to
// announce the double spend attempt to close group. TODO: how to handle the error then?
let _ = self.try_store_double_spend(&existing, signed_spend).await;

// The spend is now permanently removed from the valid spends.
let _removed = valid_spends.remove(&address);
// We don't error if the remove failed, as we rather want to
// announce the double spend attempt to close group.
// The double spend will still be detected by querying for the spend.
let _ = self.remove(&address, &self.valid_spends_path).await;

return Err(Error::DoubleSpendAttempt {
new: Box::new(signed_spend.clone()),
existing: Box::new(existing.clone()),
existing: Box::new(existing),
});
}
};
}

// This hash input is pointless, since it will compare with
// the same hash in the verify fn.
Expand Down Expand Up @@ -165,20 +189,89 @@ impl SpendStorage {

let address = dbc_address(a_spend.dbc_id());

let mut double_spends = self.double_spends.write().await;
let _replaced = double_spends.insert(address, (a_spend.clone(), b_spend.clone()));
self.try_store_double_spend(a_spend, b_spend).await?;

// The spend is now permanently removed from the valid spends.
let mut valid_spends = self.valid_spends.write().await;
let _removed = valid_spends.remove(&address);
self.remove(&address, &self.valid_spends_path).await?;

Ok(())
}

/// Checks if the given DbcId is unspendable.
async fn is_unspendable(&self, dbc_id: &DbcId) -> bool {
let address = dbc_address(dbc_id);
self.double_spends.read().await.contains_key(&address)
self.try_get_double_spend(&address).await.is_ok()
}

fn address_to_filepath(&self, addr: &DbcAddress, root: &Path) -> Result<PathBuf> {
let xorname = *addr.name();
let path = prefix_tree_path(root, xorname);
let filename = hex::encode(xorname);
Ok(path.join(filename))
}

async fn remove(&self, address: &DbcAddress, root: &Path) -> Result<()> {
debug!("Removing spend, {:?}", address);
let file_path = self.address_to_filepath(address, root)?;
remove_file(file_path).await?;
Ok(())
}

async fn try_get_double_spend(
&self,
address: &DbcAddress,
) -> Result<(SignedSpend, SignedSpend)> {
trace!("Getting double spend: {address:?}");
let file_path = self.address_to_filepath(address, &self.double_spends_path)?;
match read(file_path).await {
Ok(bytes) => {
let (a_spend, b_spend): (SignedSpend, SignedSpend) = deserialize(&bytes)?;
// They should have the same dbc id, so we can use either.
// TODO: Or should we check both? What if they are different?
if address == &dbc_address(a_spend.dbc_id()) {
Ok((a_spend, b_spend))
} else {
// This can happen if the content read is empty, or incomplete,
// possibly due to an issue with the OS synchronising to disk,
// resulting in a mismatch with recreated address of the Spend.
Err(Error::SpendNotFound(*address))
}
}
Err(_) => Err(Error::SpendNotFound(*address)),
}
}

async fn try_store_double_spend(
&mut self,
a_spend: &SignedSpend,
b_spend: &SignedSpend,
) -> Result<()> {
// They have the same dbc id, so we can use either.
let addr = dbc_address(a_spend.dbc_id());

let filepath = self.address_to_filepath(&addr, &self.double_spends_path)?;

if filepath.exists() {
return Ok(());
}

// Store the double spend to local file system.
trace!("Storing double spend {addr:?}.");
if let Some(dirs) = filepath.parent() {
create_dir_all(dirs).await?;
}

let mut file = File::create(filepath).await?;

let bytes = serialize(&(a_spend, b_spend))?;
file.write_all(&bytes).await?;
// Sync up OS data to disk to reduce the chances of
// concurrent reading failing by reading an empty/incomplete file.
file.sync_data().await?;

trace!("Stored double spend {addr:?}.");

Ok(())
}
}

Expand All @@ -188,12 +281,6 @@ impl Display for SpendStorage {
}
}

impl Default for SpendStorage {
fn default() -> Self {
Self::new()
}
}

/// Still thinking of best location for this.
/// Wanted to make the DbcAddress take a dbc id actually..
fn dbc_address(dbc_id: &DbcId) -> DbcAddress {
Expand Down
34 changes: 26 additions & 8 deletions safenode/src/node/api.rs
Expand Up @@ -15,7 +15,10 @@ use super::{
use crate::{
domain::{
node_transfers::{Error as TransferError, Transfers},
storage::{dbc_address, register::User, ChunkStorage, DbcAddress, RegisterStorage},
storage::{
dbc_address, register::User, ChunkStorage, DbcAddress, Error as StorageError,
RegisterStorage,
},
},
network::{close_group_majority, NetworkEvent, SwarmDriver},
protocol::{
Expand Down Expand Up @@ -55,12 +58,13 @@ impl Node {
let (network, mut network_event_receiver, swarm_driver) = SwarmDriver::new(addr)?;
let node_events_channel = NodeEventsChannel::default();
let node_id = NodeId::from(network.peer_id);
let root_dir = get_root_dir().await?;

let mut node = Self {
network,
chunks: ChunkStorage::new(),
registers: RegisterStorage::new(),
transfers: Transfers::new(node_id, MainKey::random()),
chunks: ChunkStorage::new(&root_dir),
registers: RegisterStorage::new(&root_dir),
transfers: Transfers::new(node_id, MainKey::random(), &root_dir),
events_channel: node_events_channel.clone(),
initial_peers,
};
Expand Down Expand Up @@ -225,7 +229,10 @@ impl Node {
.try_add(signed_spend, parent_tx, fee_ciphers, parent_spends)
.await
{
Err(TransferError::DoubleSpendAttempt { new, existing }) => {
Err(TransferError::Storage(StorageError::DoubleSpendAttempt {
new,
existing,
})) => {
warn!("Double spend attempted! New: {new:?}. Existing: {existing:?}");
if let Ok(event) =
Event::double_spend_attempt(new.clone(), existing.clone())
Expand All @@ -238,9 +245,9 @@ impl Node {
}
}

Err(ProtocolError::Transfers(
TransferError::DoubleSpendAttempt { new, existing },
))
Err(ProtocolError::Transfers(TransferError::Storage(
StorageError::DoubleSpendAttempt { new, existing },
)))
}
other => other.map_err(ProtocolError::Transfers),
};
Expand Down Expand Up @@ -397,3 +404,14 @@ impl Node {
responses
}
}

async fn get_root_dir() -> Result<std::path::PathBuf> {
use crate::protocol::error::Error as StorageError;
let mut home_dirs = dirs_next::home_dir().expect("A homedir to exist.");
home_dirs.push(".safe");
home_dirs.push("node");
tokio::fs::create_dir_all(home_dirs.as_path())
.await
.map_err(|err| StorageError::Storage(err.into()))?;
Ok(home_dirs)
}
4 changes: 2 additions & 2 deletions safenode/src/protocol/messages/event.rs
Expand Up @@ -8,7 +8,7 @@

use crate::domain::{
node_transfers::{Error, Result},
storage::{dbc_address, DataAddress},
storage::{dbc_address, DataAddress, Error as StorageError},
};

use sn_dbc::SignedSpend;
Expand Down Expand Up @@ -51,7 +51,7 @@ impl Event {
// with same id are detected as being different.
// A node could erroneously send a notification of a double spend attempt,
// so, we need to validate that.
Err(Error::NotADoubleSpendAttempt(a, b))
Err(Error::Storage(StorageError::NotADoubleSpendAttempt(a, b)))
}
}
}