diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index 5a51c6927f..aae0ebb7cb 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -97,6 +97,10 @@ jobs: run: cargo test --no-run --release timeout-minutes: 30 + - name: Run network tests + timeout-minutes: 25 + run: cargo test --release -p safenode -- network + - name: Run protocol tests timeout-minutes: 25 run: cargo test --release -p safenode -- protocol @@ -104,3 +108,87 @@ jobs: - name: Run storage tests timeout-minutes: 25 run: cargo test --release -p safenode -- storage + + e2e: + if: "!startsWith(github.event.head_commit.message, 'chore(release):')" + name: E2E tests + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + steps: + - uses: actions/checkout@v2 + + - name: Install Rust + id: toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - uses: Swatinem/rust-cache@v1 + continue-on-error: true + with: + cache-on-failure: true + sharedKey: ${{github.run_id}} + + - name: install ripgrep ubuntu + run: sudo apt-get install ripgrep + if: matrix.os == 'ubuntu-latest' + + - name: install ripgrep mac + run: brew install ripgrep + if: matrix.os == 'macos-latest' + + - name: install ripgrep windows + run: choco install ripgrep + if: matrix.os == 'windows-latest' + + - name: Build sn bins + run: cargo build --release --bins + timeout-minutes: 30 + + - name: Start a local network + run: cargo run --release --bin testnet -- --interval 1 --node-path ./target/release/safenode + id: section-startup + env: + RUST_LOG: "safenode,safe=trace" + timeout-minutes: 10 + + - name: Start a client to carry out chunk actions + run: cargo run --release --bin safe -- --upload-chunks ./README.md --get-chunk confirm_uploaded + id: client-chunk-actions + env: + RUST_LOG: "safenode,safe=trace" + timeout-minutes: 2 + + - name: Start a client to carry out register actions + run: cargo run --release --bin safe -- --create-register myregister --query-register myregister + id: client-register-actions + env: + RUST_LOG: "safenode,safe=trace" + timeout-minutes: 2 + + - name: Kill all nodes + shell: bash + timeout-minutes: 1 + if: failure() + continue-on-error: true + run: | + pkill safenode + echo "$(pgrep safenode | wc -l) nodes still running" + + - name: Tar log files + shell: bash + continue-on-error: true + run: find ~/.safe/node/local-test-network -iname '*.log*' | tar -zcvf log_files.tar.gz --files-from - + if: failure() + + - name: Upload Node Logs + uses: actions/upload-artifact@main + with: + name: sn_node_logs_e2e_${{matrix.os}} + path: log_files.tar.gz + if: failure() + continue-on-error: true diff --git a/.gitignore b/.gitignore index da5ce54f30..030990918d 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,7 @@ peers.json /.trunk -settings.json \ No newline at end of file +settings.json + +# ignore log files +*.log \ No newline at end of file diff --git a/README.md b/README.md index 6527b4e956..f40c707fef 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# stableset_net +# The Safe Network This is the Safe Network as it was supposed to be, on a kademlia network, enabled by libp2p. @@ -44,7 +44,10 @@ cargo run --release --example registers -- --user bob --reg-nickname myregister ### TODO -- [ ] Basic messaging to target nodes - [ ] Add RPC for simplest node/net interaction (do libp2p CLIs help here?) -- [ ] Add in chunking etc -- [ ] Add in DBCs and validation handling + + + +### Archive + +The elder-membership agreed, section tree backed implementation of the safe network can be found [here](https://github.com/maidsafe/safe_network_archive) diff --git a/safenode/src/bin/kadclient.rs b/safenode/src/bin/kadclient.rs index 6b14f6b99e..f2e7bef29b 100644 --- a/safenode/src/bin/kadclient.rs +++ b/safenode/src/bin/kadclient.rs @@ -7,28 +7,37 @@ // permissions and limitations relating to use of the SAFE Network Software. use safenode::{ - client::{Client, ClientEvent, Error as ClientError, Files, WalletClient}, + client::{Client, ClientEvent, Error as ClientError, Files}, log::init_node_logging, - protocol::{address::ChunkAddress, wallet::LocalWallet}, + protocol::{ + address::ChunkAddress, + wallet::{DepositWallet, LocalWallet, Wallet}, + }, }; +use sn_dbc::Dbc; + use bytes::Bytes; use clap::Parser; use dirs_next::home_dir; use eyre::Result; use std::{fs, path::PathBuf}; -use tracing::info; +use tracing::{info, warn}; use walkdir::WalkDir; use xor_name::XorName; #[derive(Parser, Debug)] #[clap(name = "safeclient cli")] struct Opt { - #[clap(long)] - client_dir: Option, - #[clap(long)] log_dir: Option, + /// The location of the wallet file. + #[clap(long)] + wallet_dir: Option, + /// Tries to load a hex encoded `Dbc` from the + /// given path and deposit it to the wallet. + #[clap(long)] + deposit: Option, #[clap(long)] upload_chunks: Option, @@ -53,13 +62,9 @@ async fn main() -> Result<()> { info!("Instantiating a SAFE client..."); - let client_dir = opt.client_dir.unwrap_or(get_client_dir().await?); - let wallet = LocalWallet::load_from(&client_dir).await?; - let secret_key = bls::SecretKey::random(); let client = Client::new(secret_key)?; - let file_api = Files::new(client.clone()); - let _wallet_client = WalletClient::new(client.clone(), wallet); + let file_api: Files = Files::new(client.clone()); let mut client_events_rx = client.events_channel(); if let Ok(event) = client_events_rx.recv().await { @@ -70,9 +75,80 @@ async fn main() -> Result<()> { } } + wallet(&opt).await?; + files(&opt, file_api).await?; + registers(&opt, client).await?; + + Ok(()) +} + +async fn wallet(opt: &Opt) -> Result<()> { + let wallet_dir = opt.wallet_dir.clone().unwrap_or(get_client_dir().await?); + let mut wallet = LocalWallet::load_from(&wallet_dir).await?; + + if let Some(deposit_path) = &opt.deposit { + let mut deposits = vec![]; + + for entry in WalkDir::new(deposit_path).into_iter().flatten() { + if entry.file_type().is_file() { + let file_name = entry.file_name(); + info!("Reading deposited tokens from {file_name:?}."); + println!("Reading deposited tokens from {file_name:?}."); + + let dbc_data = fs::read_to_string(entry.path())?; + let dbc = match Dbc::from_hex(dbc_data.trim()) { + Ok(dbc) => dbc, + Err(_) => { + warn!( + "This file does not appear to have valid hex-encoded DBC data. \ + Skipping it." + ); + println!( + "This file does not appear to have valid hex-encoded DBC data. \ + Skipping it." + ); + continue; + } + }; + + deposits.push(dbc); + } + } + + let previous_balance = wallet.balance(); + wallet.deposit(deposits); + let new_balance = wallet.balance(); + let deposited = previous_balance.as_nano() - new_balance.as_nano(); + + if deposited > 0 { + if let Err(err) = wallet.store().await { + warn!("Failed to store deposited amount: {:?}", err); + println!("Failed to store deposited amount: {:?}", err); + } else { + info!("Deposited {:?}.", sn_dbc::Token::from_nano(deposited)); + println!("Deposited {:?}.", sn_dbc::Token::from_nano(deposited)); + } + } else { + info!("Nothing deposited."); + println!("Nothing deposited."); + } + } + + Ok(()) +} + +async fn get_client_dir() -> Result { + let mut home_dirs = home_dir().expect("A homedir to exist."); + home_dirs.push(".safe"); + home_dirs.push("client"); + tokio::fs::create_dir_all(home_dirs.as_path()).await?; + Ok(home_dirs) +} + +async fn files(opt: &Opt, file_api: Files) -> Result<()> { let mut chunks_to_fetch = Vec::new(); - if let Some(files_path) = opt.upload_chunks { + if let Some(files_path) = &opt.upload_chunks { for entry in WalkDir::new(files_path).into_iter().flatten() { if entry.file_type().is_file() { let file = fs::read(entry.path())?; @@ -95,7 +171,7 @@ async fn main() -> Result<()> { } } - if let Some(input_str) = opt.get_chunk { + if let Some(input_str) = &opt.get_chunk { println!("String passed in via get_chunk is {input_str}..."); if input_str.len() == 64 { let vec = hex::decode(input_str).expect("Failed to decode xorname!"); @@ -115,7 +191,11 @@ async fn main() -> Result<()> { } } - if let Some(reg_nickname) = opt.create_register { + Ok(()) +} + +async fn registers(opt: &Opt, client: Client) -> Result<()> { + if let Some(reg_nickname) = &opt.create_register { let xorname = XorName::from_content(reg_nickname.as_bytes()); let tag = 3006; println!("Creating Register with '{reg_nickname}' at xorname: {xorname:x} and tag {tag}"); @@ -130,7 +210,7 @@ async fn main() -> Result<()> { ), }; - if let Some(entry) = opt.entry { + if let Some(entry) = &opt.entry { println!("Editing Register '{reg_nickname}' with: {entry}"); match reg_replica.write(entry.as_bytes()).await { Ok(()) => {} @@ -169,11 +249,3 @@ async fn main() -> Result<()> { Ok(()) } - -async fn get_client_dir() -> Result { - let mut home_dirs = home_dir().expect("A homedir to exist."); - home_dirs.push(".safe"); - home_dirs.push("client"); - tokio::fs::create_dir_all(home_dirs.as_path()).await?; - Ok(home_dirs) -} diff --git a/safenode/src/bin/kadnode.rs b/safenode/src/bin/kadnode.rs index bae79102cb..2dc722e00e 100644 --- a/safenode/src/bin/kadnode.rs +++ b/safenode/src/bin/kadnode.rs @@ -8,7 +8,6 @@ use safenode::{ log::init_node_logging, - network::Network, node::{Node, NodeEvent}, }; @@ -28,12 +27,25 @@ async fn main() -> Result<()> { let _log_appender_guard = init_node_logging(&opt.log_dir)?; let socket_addr = SocketAddr::new(opt.ip, opt.port); + let peers = parse_peer_multiaddreses(&opt.peers)?; info!("Starting a node..."); - let node_events_channel = Node::run(socket_addr).await?; + let node_events_channel = Node::run(socket_addr, peers).await?; let mut node_events_rx = node_events_channel.subscribe(); - if let Ok(event) = node_events_rx.recv().await { + + loop { + let event = match node_events_rx.recv().await { + Ok(e) => e, + Err(tokio::sync::broadcast::error::RecvError::Closed) => { + tracing::error!("Node event channel closed!"); + break; + } + Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { + tracing::warn!("Skipped {n} node events!"); + continue; + } + }; match event { NodeEvent::ConnectedToNetwork => { info!("Connected to the Network"); @@ -62,18 +74,32 @@ struct Opt { /// Defaults to 0.0.0.0, which will bind to all network interfaces. #[clap(long, default_value_t = IpAddr::V4(Ipv4Addr::UNSPECIFIED))] ip: IpAddr, + + /// Nodes we dial at start to help us get connected to the network. Can be specified multiple times. + #[clap(long = "peer")] + peers: Vec, } -// Todo: Implement node bootstrapping to connect to peers from outside the local network -#[allow(dead_code)] -async fn bootstrap_node(network_api: &mut Network, addr: Multiaddr) -> Result<()> { - let peer_id = match addr.iter().last() { - Some(Protocol::P2p(hash)) => PeerId::from_multihash(hash).expect("Valid hash."), - _ => return Err(eyre!("Expect peer multiaddr to contain peer ID.")), - }; - network_api - .dial(peer_id, addr) - .await - .expect("Dial to succeed"); - Ok(()) +/// Parse multiaddresses containing the P2p protocol (`/p2p/`). +/// Returns an error for the first invalid multiaddress. +fn parse_peer_multiaddreses(multiaddrs: &[Multiaddr]) -> Result> { + multiaddrs + .iter() + .map(|multiaddr| { + // Take hash from the `/p2p/` component. + let p2p_multihash = multiaddr + .iter() + .find_map(|p| match p { + Protocol::P2p(hash) => Some(hash), + _ => None, + }) + .ok_or_else(|| eyre!("address does not contain `/p2p/`"))?; + // Parse the multihash into the `PeerId`. + let peer_id = + PeerId::from_multihash(p2p_multihash).map_err(|_| eyre!("invalid p2p PeerId"))?; + + Ok((peer_id, multiaddr.clone())) + }) + // Short circuit on the first error. See rust docs `Result::from_iter`. + .collect::>>() } diff --git a/safenode/src/client/api.rs b/safenode/src/client/api.rs index 1deb5838f6..d87a4dc5c8 100644 --- a/safenode/src/client/api.rs +++ b/safenode/src/client/api.rs @@ -70,6 +70,8 @@ impl Client { match event { // Clients do not handle requests. NetworkEvent::RequestReceived { .. } => {} + // We do not listen on sockets. + NetworkEvent::NewListenAddr(_) => {} NetworkEvent::PeerAdded => { self.events_channel .broadcast(ClientEvent::ConnectedToNetwork); diff --git a/safenode/src/client/wallet.rs b/safenode/src/client/wallet.rs index d077174173..243dbdfd26 100644 --- a/safenode/src/client/wallet.rs +++ b/safenode/src/client/wallet.rs @@ -6,14 +6,15 @@ // 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 sn_dbc::{Dbc, DbcIdSource, DerivedKey, PublicAddress, Token}; +use super::Client; use crate::protocol::{ - transfers::{create_online_transfer, Outputs as TransferDetails}, - wallet::{Result, SendClient, SendWallet}, + client_transfers::{create_online_transfer, Outputs as TransferDetails, SpendRequestParams}, + messages::{Cmd, Request}, + wallet::{Error, Result, SendClient, SendWallet}, }; -use super::Client; +use sn_dbc::{Dbc, DbcIdSource, DerivedKey, PublicAddress, Token}; /// A wallet client can be used to send and /// receive tokens to/from other wallets. @@ -45,7 +46,26 @@ impl SendClient for Client { ) -> Result { let transfer = create_online_transfer(dbcs, to, change_to, self).await?; - // Upload the spends to the network: + for spend_request_params in transfer.all_spend_request_params.clone() { + let SpendRequestParams { + signed_spend, + parent_tx, + fee_ciphers, + } = spend_request_params; + + let cmd = Cmd::SpendDbc { + signed_spend: Box::new(signed_spend), + parent_tx: Box::new(parent_tx), + fee_ciphers, + }; + + let _responses = self + .send_to_closest(Request::Cmd(cmd)) + .await + .map_err(|err| Error::CouldNotSendTokens(err.to_string()))?; + + // TODO: validate responses + } Ok(transfer) } diff --git a/safenode/src/lib.rs b/safenode/src/lib.rs index fed3847445..a52dce01cd 100644 --- a/safenode/src/lib.rs +++ b/safenode/src/lib.rs @@ -48,8 +48,6 @@ pub mod client; pub mod log; /// The main logic of the network. pub mod network; -/// Transfer fees, queues, validation and storage. -pub mod network_transfers; /// SAFE Node pub mod node; /// SAFE Protocol diff --git a/safenode/src/log/mod.rs b/safenode/src/log/mod.rs index 2f7d0da873..70dff98570 100644 --- a/safenode/src/log/mod.rs +++ b/safenode/src/log/mod.rs @@ -122,9 +122,9 @@ pub fn init_node_logging(log_dir: &Option) -> Result &'static str { - // Grab root from module path ("sn_node::log::etc" -> "sn_node") + // Grab root from module path ("safenode::log::etc" -> "safenode") let m = module_path!(); &m[..m.find(':').unwrap_or(m.len())] } diff --git a/safenode/src/network/event.rs b/safenode/src/network/event.rs index d4eae69733..4974c70ecc 100644 --- a/safenode/src/network/event.rs +++ b/safenode/src/network/event.rs @@ -19,7 +19,7 @@ use libp2p::{ multiaddr::Protocol, request_response::{self, ResponseChannel}, swarm::{NetworkBehaviour, SwarmEvent}, - PeerId, + Multiaddr, PeerId, }; use std::collections::HashSet; use tracing::{info, warn}; @@ -69,6 +69,8 @@ pub enum NetworkEvent { }, /// Emitted when the DHT is updated PeerAdded, + /// Started listening on a new address + NewListenAddr(Multiaddr), } impl SwarmDriver { @@ -148,10 +150,11 @@ impl SwarmDriver { }, SwarmEvent::NewListenAddr { address, .. } => { let local_peer_id = *self.swarm.local_peer_id(); - info!( - "Local node is listening on {:?}", - address.with(Protocol::P2p(local_peer_id.into())) - ); + let address = address.with(Protocol::P2p(local_peer_id.into())); + self.event_sender + .send(NetworkEvent::NewListenAddr(address.clone())) + .await?; + info!("Local node is listening on {address:?}"); } SwarmEvent::IncomingConnection { .. } => {} SwarmEvent::ConnectionEstablished { @@ -164,7 +167,9 @@ impl SwarmDriver { } } } - SwarmEvent::ConnectionClosed { .. } => {} + SwarmEvent::ConnectionClosed { peer_id, .. } => { + info!("Peer {} has gone offline", peer_id); + } SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => { if let Some(peer_id) = peer_id { if let Some(sender) = self.pending_dial.remove(&peer_id) { diff --git a/safenode/src/network/mod.rs b/safenode/src/network/mod.rs index b822f1ec9e..4bf4c5749b 100644 --- a/safenode/src/network/mod.rs +++ b/safenode/src/network/mod.rs @@ -145,7 +145,16 @@ impl SwarmDriver { // Create a Kademlia behaviour for client mode, i.e. set req/resp protocol // to outbound-only mode and don't listen on any address let kademlia = Kademlia::with_config(peer_id, MemoryStore::new(peer_id), cfg); - let mdns = mdns::tokio::Behaviour::new(mdns::Config::default(), peer_id)?; + + let mdns_config = mdns::Config { + // lower query interval to speed up peer discovery + // this increases traffic, but means we no longer have clients unable to connect + // after a few minutes + query_interval: Duration::from_secs(5), + ..Default::default() + }; + + let mdns = mdns::tokio::Behaviour::new(mdns_config, peer_id)?; let behaviour = NodeBehaviour { request_response, kademlia, diff --git a/safenode/src/node/api.rs b/safenode/src/node/api.rs index ec054590e3..1ef4079c4e 100644 --- a/safenode/src/node/api.rs +++ b/safenode/src/node/api.rs @@ -14,7 +14,6 @@ use super::{ use crate::{ network::{close_group_majority, NetworkEvent, SwarmDriver}, - network_transfers::{Error as TransferError, Transfers}, protocol::{ address::{dbc_address, DbcAddress}, error::Error as ProtocolError, @@ -22,6 +21,7 @@ use crate::{ Cmd, CmdResponse, Event, Query, QueryResponse, RegisterCmd, Request, Response, SpendQuery, }, + node_transfers::{Error as TransferError, Transfers}, register::User, }, storage::{ChunkStorage, RegisterStorage}, @@ -30,7 +30,7 @@ use crate::{ use sn_dbc::{DbcTransaction, MainKey, SignedSpend}; use futures::future::select_all; -use libp2p::{request_response::ResponseChannel, PeerId}; +use libp2p::{request_response::ResponseChannel, Multiaddr, PeerId}; use std::{collections::BTreeSet, net::SocketAddr, time::Duration}; use tokio::task::spawn; use xor_name::XorName; @@ -48,7 +48,10 @@ impl Node { /// # Errors /// /// Returns an error if there is a problem initializing the `SwarmDriver`. - pub async fn run(addr: SocketAddr) -> Result { + pub async fn run( + addr: SocketAddr, + initial_peers: Vec<(PeerId, Multiaddr)>, + ) -> Result { let (network, mut network_event_receiver, swarm_driver) = SwarmDriver::new(addr)?; let node_events_channel = NodeEventsChannel::default(); let node_id = super::to_node_id(network.peer_id); @@ -59,6 +62,7 @@ impl Node { registers: RegisterStorage::new(), transfers: Transfers::new(node_id, MainKey::random()), events_channel: node_events_channel.clone(), + initial_peers, }; let _handle = spawn(swarm_driver.run()); @@ -99,6 +103,17 @@ impl Node { trace!("For target {target:?}, get closest peers {result:?}"); }); } + NetworkEvent::NewListenAddr(_) => { + let network = self.network.clone(); + let peers = self.initial_peers.clone(); + let _handle = spawn(async move { + for (peer_id, addr) in &peers { + if let Err(err) = network.dial(*peer_id, addr.clone()).await { + tracing::error!("Failed to dial {peer_id}: {err:?}"); + }; + } + }); + } } Ok(()) @@ -175,13 +190,13 @@ impl Node { } Cmd::SpendDbc { signed_spend, - source_tx, + parent_tx, fee_ciphers, } => { // First we fetch all parent spends from the network. // They shall naturally all exist as valid spends for this current // spend attempt to be valid. - let parent_spends = match self.get_parent_spends(source_tx.as_ref()).await { + let parent_spends = match self.get_parent_spends(parent_tx.as_ref()).await { Ok(parent_spends) => parent_spends, Err(Error::Protocol(err)) => return CmdResponse::Spend(Err(err)), Err(error) => { @@ -195,7 +210,7 @@ impl Node { // This will validate all the necessary components of the spend. let res = match self .transfers - .try_add(signed_spend, source_tx, fee_ciphers, parent_spends) + .try_add(signed_spend, parent_tx, fee_ciphers, parent_spends) .await { Err(TransferError::DoubleSpendAttempt { new, existing }) => { @@ -226,7 +241,7 @@ impl Node { // This call makes sure we get the same spend from all in the close group. // If we receive a spend here, it is assumed to be valid. But we will verify // that anyway, in the code right after this for loop. - async fn get_parent_spends(&self, source_tx: &DbcTransaction) -> Result> { + async fn get_parent_spends(&self, parent_tx: &DbcTransaction) -> Result> { // These will be different spends, one for each input that went into // creating the above spend passed in to this function. let mut all_parent_spends = BTreeSet::new(); @@ -234,7 +249,7 @@ impl Node { // First we fetch all parent spends from the network. // They shall naturally all exist as valid spends for this current // spend attempt to be valid. - for parent_input in &source_tx.inputs { + for parent_input in &parent_tx.inputs { let parent_address = dbc_address(&parent_input.dbc_id()); // This call makes sure we get the same spend from all in the close group. // If we receive a spend here, it is assumed to be valid. But we will verify diff --git a/safenode/src/node/mod.rs b/safenode/src/node/mod.rs index b180d3cfd1..1ea632269f 100644 --- a/safenode/src/node/mod.rs +++ b/safenode/src/node/mod.rs @@ -16,11 +16,11 @@ use self::{error::Error, event::NodeEventsChannel}; use crate::{ network::Network, - network_transfers::Transfers, + protocol::node_transfers::Transfers, storage::{ChunkStorage, RegisterStorage}, }; -use libp2p::PeerId; +use libp2p::{Multiaddr, PeerId}; use serde::{Deserialize, Serialize}; use xor_name::{XorName, XOR_NAME_LEN}; @@ -33,6 +33,8 @@ pub struct Node { registers: RegisterStorage, transfers: Transfers, events_channel: NodeEventsChannel, + /// Peers that are dialed at startup of node. + initial_peers: Vec<(PeerId, Multiaddr)>, } /// A unique identifier for a node in the network, diff --git a/safenode/src/protocol/transfers/error.rs b/safenode/src/protocol/client_transfers/error.rs similarity index 100% rename from safenode/src/protocol/transfers/error.rs rename to safenode/src/protocol/client_transfers/error.rs diff --git a/safenode/src/protocol/transfers/mod.rs b/safenode/src/protocol/client_transfers/mod.rs similarity index 74% rename from safenode/src/protocol/transfers/mod.rs rename to safenode/src/protocol/client_transfers/mod.rs index d6b193bf1a..979e9afde0 100644 --- a/safenode/src/protocol/transfers/mod.rs +++ b/safenode/src/protocol/client_transfers/mod.rs @@ -39,7 +39,18 @@ pub(crate) use self::{ online::create_transfer as create_online_transfer, }; -use sn_dbc::{Dbc, DbcIdSource, DerivedKey, PublicAddress, RevealedAmount, Token}; +use super::fees::{FeeCiphers, RequiredFee}; + +use crate::node::NodeId; + +use sn_dbc::{ + Dbc, DbcId, DbcIdSource, DbcTransaction, DerivedKey, PublicAddress, RevealedAmount, + SignedSpend, Token, +}; + +use std::collections::BTreeMap; + +type NodeFeesPerInput = BTreeMap>; /// The input details necessary to /// carry out a transfer of tokens. @@ -52,6 +63,11 @@ pub struct Inputs { pub recipients: Vec<(Token, DbcIdSource)>, /// Any surplus amount after spending the necessary input dbcs. pub change: (Token, PublicAddress), + /// This is the set of input dbc keys, each having a set of + /// node ids and their respective fees to be paid, and the + /// dbc id source to generate the dbc the fees shall be paid to. + /// Used to produce the fee ciphers for the spends. + pub node_fees_per_input: NodeFeesPerInput, } /// The created dbcs and change dbc from a transfer @@ -64,6 +80,20 @@ pub struct Outputs { /// The dbc holding surplus tokens after /// spending the necessary input dbcs. pub change_dbc: Option, + /// The parameters necessary to send all spend requests to the network. + pub all_spend_request_params: Vec, +} + +/// The parameters necessary to send a spend request to the network. +#[derive(Debug, Clone)] +pub struct SpendRequestParams { + /// The dbc to register in the network as spent. + pub signed_spend: SignedSpend, + /// The dbc transaction that the spent dbc was created in. + pub parent_tx: DbcTransaction, + /// This is the set of node ids and their respective fee ciphers. + /// Sent together with spends, so that nodes can verify their fee payments. + pub fee_ciphers: BTreeMap, } /// A resulting dbc from a token transfer. diff --git a/safenode/src/protocol/transfers/offline.rs b/safenode/src/protocol/client_transfers/offline.rs similarity index 79% rename from safenode/src/protocol/transfers/offline.rs rename to safenode/src/protocol/client_transfers/offline.rs index aefefdee4a..94342298e6 100644 --- a/safenode/src/protocol/transfers/offline.rs +++ b/safenode/src/protocol/client_transfers/offline.rs @@ -6,13 +6,15 @@ // 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 super::{CreatedDbc, Error, Inputs, Outputs, Result}; +use super::{CreatedDbc, Error, Inputs, Outputs, Result, SpendRequestParams}; use sn_dbc::{ rng, Dbc, DbcIdSource, DerivedKey, Hash, InputHistory, PublicAddress, RevealedInput, Token, TransactionBuilder, }; +use std::collections::BTreeMap; + /// A function for creating an offline transfer of tokens. /// This is done by creating new dbcs to the recipients (and a change dbc if any) /// by selecting from the available input dbcs, and creating the necessary @@ -35,8 +37,8 @@ pub(crate) fn create_transfer( ) -> Result { // We need to select the necessary number of dbcs from those that we were passed. // This will also account for any fees. - let send_inputs = select_inputs(available_dbcs, recipients, change_to)?; - transfer(send_inputs) + let selected_inputs = select_inputs(available_dbcs, recipients, change_to)?; + create_transfer_with(selected_inputs) } /// Select the necessary number of dbcs from those that we were passed. @@ -106,6 +108,7 @@ fn select_inputs( dbcs_to_spend, recipients, change: (change_amount, change_to), + node_fees_per_input: BTreeMap::new(), }) } @@ -125,14 +128,16 @@ fn verify_amounts(total_input_amount: Token, total_output_amount: Token) -> Resu /// to the network. When those same signed spends can be retrieved from /// enough peers in the network, the transaction will be completed. #[allow(clippy::result_large_err)] -fn transfer(send_inputs: Inputs) -> Result { +fn create_transfer_with(selected_inputs: Inputs) -> Result { let Inputs { dbcs_to_spend, recipients, change: (change, change_to), - } = send_inputs; + .. + } = selected_inputs; let mut inputs = vec![]; + let mut src_txs = BTreeMap::new(); for (dbc, derived_key) in dbcs_to_spend { let revealed_amount = match dbc.revealed_amount(&derived_key) { Ok(amount) => amount, @@ -143,9 +148,10 @@ fn transfer(send_inputs: Inputs) -> Result { }; let input = InputHistory { input: RevealedInput::new(derived_key, revealed_amount), - input_src_tx: dbc.src_tx, + input_src_tx: dbc.src_tx.clone(), }; inputs.push(input); + let _ = src_txs.insert(dbc.id(), dbc.src_tx); } let mut tx_builder = TransactionBuilder::default() @@ -165,6 +171,38 @@ fn transfer(send_inputs: Inputs) -> Result { .build(Hash::default(), &mut rng) .map_err(Error::Dbcs)?; + let signed_spends: BTreeMap<_, _> = dbc_builder + .signed_spends() + .into_iter() + .map(|spend| (spend.dbc_id(), spend)) + .collect(); + + // We must have a source transaction for each signed spend (i.e. the tx where the dbc was created). + // These are required to upload the spends to the network. + if !signed_spends + .iter() + .all(|(dbc_id, _)| src_txs.contains_key(*dbc_id)) + { + return Err(Error::DbcReissueFailed( + "Not all signed spends could be matched to a source dbc transaction.".to_string(), + )); + } + + let mut all_spend_request_params = vec![]; + for (dbc_id, signed_spend) in signed_spends.into_iter() { + let parent_tx = src_txs.get(dbc_id).ok_or(Error::DbcReissueFailed(format!( + "Missing source dbc tx of {dbc_id:?}!" + )))?; + + let spend_request_params = SpendRequestParams { + signed_spend: signed_spend.clone(), + parent_tx: parent_tx.clone(), + fee_ciphers: BTreeMap::new(), + }; + + all_spend_request_params.push(spend_request_params); + } + // Perform validations of input tx and signed spends, // as well as building the output DBCs. let mut created_dbcs: Vec<_> = dbc_builder @@ -187,5 +225,6 @@ fn transfer(send_inputs: Inputs) -> Result { Ok(Outputs { created_dbcs, change_dbc, + all_spend_request_params, }) } diff --git a/safenode/src/protocol/transfers/online.rs b/safenode/src/protocol/client_transfers/online.rs similarity index 78% rename from safenode/src/protocol/transfers/online.rs rename to safenode/src/protocol/client_transfers/online.rs index f158b9d2d0..2b3f08349b 100644 --- a/safenode/src/protocol/transfers/online.rs +++ b/safenode/src/protocol/client_transfers/online.rs @@ -6,21 +6,21 @@ // 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 super::{CreatedDbc, Error, Inputs, Outputs, Result}; +use super::{CreatedDbc, Error, Inputs, Outputs, Result, SpendRequestParams}; use crate::{ client::Client, network::close_group_majority, node::NodeId, protocol::{ - fees::{RequiredFee, SpendPriority}, + fees::{FeeCiphers, RequiredFee, SpendPriority}, messages::{Query, QueryResponse, Request, Response, SpendQuery}, }, }; use sn_dbc::{ - rng, Dbc, DbcId, DbcIdSource, DerivedKey, Hash, InputHistory, PublicAddress, RevealedInput, - Token, TransactionBuilder, + rng, Dbc, DbcId, DbcIdSource, DerivedKey, Hash, InputHistory, PublicAddress, RevealedAmount, + RevealedInput, Token, TransactionBuilder, }; use std::collections::{BTreeMap, BTreeSet}; @@ -78,7 +78,7 @@ async fn select_inputs( })?; let mut change_amount = total_output_amount; - let mut all_fee_cipher_params = BTreeMap::new(); + let mut node_fees_per_input = BTreeMap::new(); let mut fees_paid = Token::zero(); for (dbc, derived_key) in available_dbcs { @@ -143,7 +143,7 @@ async fn select_inputs( .to_string(), ))?; - let mut fee_cipher_params = BTreeMap::new(); + let mut node_fees = BTreeMap::new(); // Add nodes to outputs and generate their fee ciphers. decrypted_node_fees @@ -154,10 +154,10 @@ async fn select_inputs( .reward_address .random_dbc_id_src(&mut rand::thread_rng()); recipients.push((*fee, dbc_id_src)); - let _ = fee_cipher_params.insert(*node_id, (required_fee.clone(), dbc_id_src)); + let _ = node_fees.insert(*node_id, (required_fee.clone(), dbc_id_src)); }); - let _ = all_fee_cipher_params.insert(dbc_id, fee_cipher_params); + let _ = node_fees_per_input.insert(dbc_id, node_fees); fees_paid = fees_paid.checked_add(fee_per_input).ok_or_else(|| { Error::DbcReissueFailed( @@ -224,6 +224,7 @@ async fn select_inputs( dbcs_to_spend, recipients, change: (change_amount, change_to), + node_fees_per_input, }) } @@ -248,9 +249,11 @@ fn create_transfer_with(selected_inputs: Inputs) -> Result { dbcs_to_spend, recipients, change: (change, change_to), + node_fees_per_input, } = selected_inputs; let mut inputs = vec![]; + let mut src_txs = BTreeMap::new(); for (dbc, derived_key) in dbcs_to_spend { let revealed_amount = match dbc.revealed_amount(&derived_key) { Ok(amount) => amount, @@ -261,9 +264,10 @@ fn create_transfer_with(selected_inputs: Inputs) -> Result { }; let input = InputHistory { input: RevealedInput::new(derived_key, revealed_amount), - input_src_tx: dbc.src_tx, + input_src_tx: dbc.src_tx.clone(), }; inputs.push(input); + let _ = src_txs.insert(dbc.id(), dbc.src_tx); } let mut tx_builder = TransactionBuilder::default() @@ -283,6 +287,50 @@ fn create_transfer_with(selected_inputs: Inputs) -> Result { .build(Hash::default(), &mut rng) .map_err(Error::Dbcs)?; + let signed_spends: BTreeMap<_, _> = dbc_builder + .signed_spends() + .into_iter() + .map(|spend| (spend.dbc_id(), spend)) + .collect(); + + // We must have a source transaction for each signed spend (i.e. the tx where the dbc was created). + // These are required to upload the spends to the network. + if !signed_spends + .iter() + .all(|(dbc_id, _)| src_txs.contains_key(*dbc_id)) + { + return Err(Error::DbcReissueFailed( + "Not all signed spends could be matched to a source dbc transaction.".to_string(), + )); + } + + let outputs = dbc_builder + .revealed_outputs + .iter() + .map(|output| (output.dbc_id, output.revealed_amount)) + .collect(); + + let mut all_spend_request_params = vec![]; + for (dbc_id, signed_spend) in signed_spends.into_iter() { + let parent_tx = src_txs.get(dbc_id).ok_or(Error::DbcReissueFailed(format!( + "Missing source dbc tx of {dbc_id:?}!" + )))?; + + let node_fees = node_fees_per_input + .get(dbc_id) + .ok_or(Error::DbcReissueFailed(format!( + "Missing source dbc tx of {dbc_id:?}!" + )))?; + + let spend_request_params = SpendRequestParams { + signed_spend: signed_spend.clone(), + parent_tx: parent_tx.clone(), + fee_ciphers: fee_ciphers(&outputs, node_fees)?, + }; + + all_spend_request_params.push(spend_request_params); + } + // Perform validations of input tx and signed spends, // as well as building the output dbcs. let mut created_dbcs: Vec<_> = dbc_builder @@ -305,6 +353,7 @@ fn create_transfer_with(selected_inputs: Inputs) -> Result { Ok(Outputs { created_dbcs, change_dbc, + all_spend_request_params, }) } @@ -350,3 +399,38 @@ async fn get_fees(dbc_id: DbcId, client: &Client) -> Result, + node_fees: &BTreeMap, +) -> Result> { + let mut input_fee_ciphers = BTreeMap::new(); + for (node_id, (required_fee, dbc_id_src)) in node_fees { + // Encrypt the index to the reward address (`PublicAddress`) of the node. + let derivation_index_cipher = required_fee + .content + .reward_address + .encrypt(&dbc_id_src.derivation_index); + + let dbc_id = dbc_id_src.dbc_id(); + let revealed_amount = outputs + .get(&dbc_id) + .ok_or(Error::DbcReissueFailed("Missing output!".to_string()))?; + + // Encrypt the amount to the _derived key_ (i.e. new dbc id). + let amount_cipher = revealed_amount.encrypt(&dbc_id); + let _ = input_fee_ciphers.insert( + *node_id, + FeeCiphers::new(amount_cipher, derivation_index_cipher), + ); + } + Ok(input_fee_ciphers) +} diff --git a/safenode/src/protocol/error.rs b/safenode/src/protocol/error.rs index 5585f0aab3..43e1acc88e 100644 --- a/safenode/src/protocol/error.rs +++ b/safenode/src/protocol/error.rs @@ -9,11 +9,10 @@ use super::{ address::{ChunkAddress, RegisterAddress}, authority::PublicKey, + node_transfers::Error as TransferError, register::{EntryHash, User}, }; -use crate::network_transfers::Error as TransferError; - use serde::{Deserialize, Serialize}; use std::{fmt::Debug, result}; use thiserror::Error; diff --git a/safenode/src/protocol/messages/cmd.rs b/safenode/src/protocol/messages/cmd.rs index 2dec5092ca..4305b4d43f 100644 --- a/safenode/src/protocol/messages/cmd.rs +++ b/safenode/src/protocol/messages/cmd.rs @@ -49,7 +49,7 @@ pub enum Cmd { signed_spend: Box, /// The transaction that this spend was created in. #[debug(skip)] - source_tx: Box, + parent_tx: Box, /// As to avoid impl separate cmd flow, we send /// all fee ciphers to all Nodes for now. #[debug(skip)] diff --git a/safenode/src/protocol/messages/event.rs b/safenode/src/protocol/messages/event.rs index 62156d6fee..2ac8a263a7 100644 --- a/safenode/src/protocol/messages/event.rs +++ b/safenode/src/protocol/messages/event.rs @@ -6,9 +6,9 @@ // 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::{ - network_transfers::{Error, Result}, - protocol::address::{dbc_address, DataAddress}, +use crate::protocol::{ + address::{dbc_address, DataAddress}, + node_transfers::{Error, Result}, }; use sn_dbc::SignedSpend; diff --git a/safenode/src/protocol/mod.rs b/safenode/src/protocol/mod.rs index 84c9083a8e..69a46c968f 100644 --- a/safenode/src/protocol/mod.rs +++ b/safenode/src/protocol/mod.rs @@ -15,15 +15,17 @@ pub mod address; pub mod authority; /// Chunk type. pub mod chunk; +/// Client handling of token transfers. +pub mod client_transfers; /// Dbc genesis creation. pub mod dbc_genesis; /// Errors. pub mod error; /// Types related to transfer fees. pub mod fees; +/// Node handling of token transfers. +pub mod node_transfers; /// Register type. pub mod register; -/// Token transfers. -pub mod transfers; /// A wallet for network tokens. pub mod wallet; diff --git a/safenode/src/network_transfers/error.rs b/safenode/src/protocol/node_transfers/error.rs similarity index 100% rename from safenode/src/network_transfers/error.rs rename to safenode/src/protocol/node_transfers/error.rs diff --git a/safenode/src/network_transfers/mod.rs b/safenode/src/protocol/node_transfers/mod.rs similarity index 88% rename from safenode/src/network_transfers/mod.rs rename to safenode/src/protocol/node_transfers/mod.rs index cb921803a1..0185e28cb6 100644 --- a/safenode/src/network_transfers/mod.rs +++ b/safenode/src/protocol/node_transfers/mod.rs @@ -6,6 +6,8 @@ // 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. +//! Transfer fees, fee priority queue, validation and storage of spends. + mod error; pub(crate) use self::error::{Error, Result}; @@ -23,9 +25,14 @@ use sn_dbc::{DbcId, DbcTransaction, MainKey, SignedSpend, Token}; use std::collections::{BTreeMap, BTreeSet}; +/// This is an arbitrary number. +/// The supply/demand dynamics enabled by the +/// spend queue, will give the price discovery +/// that results in the correct levels of fees +/// as deemed by the market. const STARTING_FEE: u64 = 4000; // 0.000004 SNT -pub(super) struct Transfers { +pub(crate) struct Transfers { node_id: NodeId, node_reward_key: MainKey, spend_queue: SpendQ, @@ -87,14 +94,14 @@ impl Transfers { pub(crate) async fn try_add( &mut self, signed_spend: Box, - source_tx: Box, + parent_tx: Box, fee_ciphers: BTreeMap, parent_spends: BTreeSet, ) -> Result<()> { // 1. Validate the tx hash. // Ensure that the provided src tx is the same as the // one we have the hash of in the signed spend. - let provided_src_tx_hash = source_tx.hash(); + let provided_src_tx_hash = parent_tx.hash(); let signed_src_tx_hash = signed_spend.src_tx_hash(); if provided_src_tx_hash != signed_src_tx_hash { @@ -105,14 +112,14 @@ impl Transfers { } // 2. Try extract the fee paid for this spend, and validate it. - let paid_fee = self.validate_fee(source_tx.as_ref(), fee_ciphers)?; + let paid_fee = self.validate_fee(parent_tx.as_ref(), fee_ciphers)?; // 3. Validate the spend itself. self.storage.validate(signed_spend.as_ref()).await?; // 4. Validate the parents of the spend. // This also ensures that all parent's dst tx's are the same as the src tx of this spend. - validate_parent_spends(signed_spend.as_ref(), source_tx.as_ref(), parent_spends)?; + validate_parent_spends(signed_spend.as_ref(), parent_tx.as_ref(), parent_spends)?; // This spend is valid and goes into the queue. self.spend_queue.push(*signed_spend, paid_fee.as_nano()); @@ -132,10 +139,10 @@ impl Transfers { fn validate_fee( &self, - tx: &DbcTransaction, + parent_tx: &DbcTransaction, fee_ciphers: BTreeMap, ) -> Result { - let fee_paid = decipher_fee(&self.node_reward_key, tx, self.node_id, fee_ciphers)?; + let fee_paid = decipher_fee(&self.node_reward_key, parent_tx, self.node_id, fee_ciphers)?; let spend_q_snapshot = self.spend_queue.snapshot(); let spend_q_stats = spend_q_snapshot.stats(); @@ -157,7 +164,7 @@ impl Transfers { /// The signed_spend.dbc_id() shall exist among its outputs. fn validate_parent_spends( signed_spend: &SignedSpend, - source_tx: &DbcTransaction, + parent_tx: &DbcTransaction, parent_spends: BTreeSet, ) -> Result<()> { // The parent_spends will be different spends, @@ -179,11 +186,11 @@ fn validate_parent_spends( .map(|s| s.spend.blinded_amount) .collect(); // Here we check that the spend that is attempted, was created in a valid tx. - let src_tx_validity = source_tx.verify(&known_parent_blinded_amounts); + let src_tx_validity = parent_tx.verify(&known_parent_blinded_amounts); if src_tx_validity.is_err() { return Err(Error::InvalidSourceTxProvided { signed_src_tx_hash: signed_spend.src_tx_hash(), - provided_src_tx_hash: source_tx.hash(), + provided_src_tx_hash: parent_tx.hash(), }); } @@ -194,13 +201,17 @@ fn validate_parent_spends( #[cfg(not(feature = "data-network"))] fn decipher_fee( node_reward_key: &MainKey, - tx: &DbcTransaction, + parent_tx: &DbcTransaction, node_id: NodeId, fee_ciphers: BTreeMap, ) -> Result { let fee_ciphers = fee_ciphers.get(&node_id).ok_or(Error::MissingFee)?; let (dbc_id, revealed_amount) = fee_ciphers.decrypt(node_reward_key)?; - let output_proof = match tx.outputs.iter().find(|proof| proof.dbc_id() == &dbc_id) { + let output_proof = match parent_tx + .outputs + .iter() + .find(|proof| proof.dbc_id() == &dbc_id) + { Some(proof) => proof, None => return Err(Error::MissingFee), }; diff --git a/safenode/src/protocol/wallet/error.rs b/safenode/src/protocol/wallet/error.rs index ed0aae7669..23af5f74af 100644 --- a/safenode/src/protocol/wallet/error.rs +++ b/safenode/src/protocol/wallet/error.rs @@ -16,7 +16,7 @@ pub type Result = std::result::Result; pub enum Error { /// Failed to create transfer. #[error("Transfer error {0}")] - CreateTransfer(#[from] crate::protocol::transfers::Error), + CreateTransfer(#[from] crate::protocol::client_transfers::Error), /// A general error when a transfer fails. #[error("Failed to send tokens due to {0}")] CouldNotSendTokens(String), diff --git a/safenode/src/protocol/wallet/local_store.rs b/safenode/src/protocol/wallet/local_store.rs index 1ff934a6d9..bccca424f4 100644 --- a/safenode/src/protocol/wallet/local_store.rs +++ b/safenode/src/protocol/wallet/local_store.rs @@ -12,7 +12,7 @@ use super::{ DepositWallet, KeyLessWallet, Result, SendClient, SendWallet, Wallet, }; -use crate::protocol::transfers::{CreatedDbc, Outputs as TransferDetails}; +use crate::protocol::client_transfers::{CreatedDbc, Outputs as TransferDetails}; use sn_dbc::{Dbc, DbcIdSource, MainKey, PublicAddress, Token}; @@ -168,6 +168,7 @@ impl SendWallet for LocalWallet { let TransferDetails { change_dbc, created_dbcs, + .. } = client.send(available_dbcs, to, self.address()).await?; let spent_dbc_ids: BTreeSet<_> = created_dbcs @@ -196,8 +197,8 @@ mod tests { use super::{get_wallet, store_wallet, LocalWallet}; use crate::protocol::{ + client_transfers::{create_offline_transfer, Outputs as TransferDetails}, dbc_genesis::{create_genesis_dbc, GENESIS_DBC_AMOUNT}, - transfers::{create_offline_transfer, Outputs as TransferDetails}, wallet::{KeyLessWallet, SendClient}, }; diff --git a/safenode/src/protocol/wallet/mod.rs b/safenode/src/protocol/wallet/mod.rs index 262030ac87..89bb8b7372 100644 --- a/safenode/src/protocol/wallet/mod.rs +++ b/safenode/src/protocol/wallet/mod.rs @@ -43,7 +43,7 @@ pub use self::{ // network_store::NetworkWallet, }; -use super::transfers::{CreatedDbc, Outputs as TransferDetails}; +use super::client_transfers::{CreatedDbc, Outputs as TransferDetails}; use sn_dbc::{Dbc, DbcIdSource, DerivedKey, PublicAddress, Token}; diff --git a/safenode/src/storage/spends.rs b/safenode/src/storage/spends.rs index d9edfc4bd9..8474990bbd 100644 --- a/safenode/src/storage/spends.rs +++ b/safenode/src/storage/spends.rs @@ -7,8 +7,10 @@ // permissions and limitations relating to use of the SAFE Network Software. use crate::{ - network_transfers::{Error, Result}, - protocol::address::DbcAddress, + protocol::{ + address::DbcAddress, + node_transfers::{Error, Result}, + }, storage::used_space::UsedSpace, }; diff --git a/sn_testnet/src/main.rs b/sn_testnet/src/main.rs index 5464dbfb1e..b58ade6304 100644 --- a/sn_testnet/src/main.rs +++ b/sn_testnet/src/main.rs @@ -202,12 +202,17 @@ async fn build_node() -> Result<()> { info!("Building safenode"); debug!("Building safenode with args: {:?}", args); - let _ = Command::new("cargo") + let build_result = Command::new("cargo") .args(args.clone()) .current_dir("safenode") .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) .output()?; + + if !build_result.status.success() { + return Err(eyre!("Failed to build safenode")); + } + info!("safenode built successfully"); Ok(()) } diff --git a/startup b/startup index 893b60cda2..60b6dec070 100755 --- a/startup +++ b/startup @@ -5,8 +5,8 @@ do echo Starting $peer... printf "#! /bin/sh -\n cd `pwd`\n - cargo run --bin safenode -- \"$peer\" + cargo run --bin safenode " > /tmp/$peer.command chmod +x /tmp/$peer.command open /tmp/$peer.command -done \ No newline at end of file +done