Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rpc-client): be able to decrpyt received Transfers by providing a secret key #908

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion sn_client/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use sn_protocol::{
NetworkAddress, PrettyPrintRecordKey,
};
use sn_registers::SignedRegister;
use sn_transfers::{NanoTokens, SignedSpend, Transfer, UniquePubkey};
use sn_transfers::{CashNote, LocalWallet, NanoTokens, SignedSpend, Transfer, UniquePubkey};
use std::{
collections::{HashMap, HashSet},
time::Duration,
Expand Down Expand Up @@ -590,6 +590,25 @@ impl Client {
self.network.publish_on_topic(topic_id, msg)?;
Ok(())
}

/// This function is used to receive a Transfer and turn it back into spendable CashNotes.
/// Needs Network connection.
/// Verify Transfer and rebuild spendable currency from it
/// Returns an `Error::FailedToDecypherTransfer` if the transfer cannot be decyphered
/// (This means the transfer is not for us as it was not encrypted to our key)
/// Returns an `Error::InvalidTransfer` if the transfer is not valid
/// Else returns a list of CashNotes that can be deposited to our wallet and spent
pub async fn verify_and_unpack_transfer(
&self,
transfer: &Transfer,
wallet: &LocalWallet,
) -> Result<Vec<CashNote>> {
let cash_notes = self
.network
.verify_and_unpack_transfer(transfer, wallet)
.await?;
Ok(cash_notes)
}
}

fn get_register_from_record(record: Record) -> Result<SignedRegister> {
Expand Down
3 changes: 3 additions & 0 deletions sn_networking/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ pub enum Error {
#[error("Network Metric error")]
NetworkMetricError,

#[error("Gossipsub config Error: {0}")]
GossipsubConfigError(String),

#[error("Gossipsub publish Error: {0}")]
GossipsubPublishError(#[from] PublishError),

Expand Down
2 changes: 1 addition & 1 deletion sn_node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ open-metrics = ["sn_networking/open-metrics", "prometheus-client"]
quic=["sn_networking/quic"]

[dependencies]
assert_fs = "1.0.0"
async-trait = "0.1"
bincode = "1.3.1"
bls = { package = "blsttc", version = "8.0.1" }
Expand Down Expand Up @@ -81,7 +82,6 @@ tiny_http = { version="0.12", features = ["ssl-rustls"] }
color-eyre = "0.6.2"

[dev-dependencies]
assert_fs = "1.0.0"
tempfile = "3.6.0"

[build-dependencies]
Expand Down
106 changes: 85 additions & 21 deletions sn_node/examples/safenode_rpc_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,25 @@
// 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 assert_fs::TempDir;
use bls::SecretKey;
use clap::Parser;
use eyre::Result;
use eyre::{eyre, Result};
use libp2p::{Multiaddr, PeerId};
use safenode_proto::{
safe_node_client::SafeNodeClient, GossipsubPublishRequest, GossipsubSubscribeRequest,
GossipsubUnsubscribeRequest, NetworkInfoRequest, NodeEventsRequest, NodeInfoRequest,
RecordAddressesRequest, RestartRequest, StopRequest, UpdateRequest,
};
use sn_client::Client;
use sn_logging::LogBuilder;
use sn_node::NodeEvent;
use sn_protocol::storage::SpendAddress;
use sn_transfers::{LocalWallet, MainSecretKey, Transfer};
use std::{fs, net::SocketAddr, path::PathBuf, str::FromStr, time::Duration};
use tokio_stream::StreamExt;
use tonic::Request;
use tracing::warn;
use tracing_core::Level;

// this includes code generated from .proto files
Expand Down Expand Up @@ -53,6 +58,9 @@ enum Cmd {
/// Note this blocks the app and it will print events as they are broadcasted by the node
#[clap(name = "transfers")]
TransfersEvents {
/// The hex-encoded BLS secret key to decrypt the transfers received and convert
/// them into spendable CashNotes.
sk: String,
/// Path where to store CashNotes received.
/// Each CashNote is written to a separate file in respective
/// recipient public address dir in the created cash_notes dir.
Expand Down Expand Up @@ -120,8 +128,10 @@ async fn main() -> Result<()> {
match opt.cmd {
Cmd::Info => node_info(addr).await,
Cmd::Netinfo => network_info(addr).await,
Cmd::Events => node_events(addr, false, None).await,
Cmd::TransfersEvents { log_cash_notes } => node_events(addr, true, log_cash_notes).await,
Cmd::Events => node_events(addr).await,
Cmd::TransfersEvents { sk, log_cash_notes } => {
transfers_events(addr, sk, log_cash_notes).await
}
Cmd::Subscribe { topic } => gossipsub_subscribe(addr, topic).await,
Cmd::Unsubscribe { topic } => gossipsub_unsubscribe(addr, topic).await,
Cmd::Publish { topic, msg } => gossipsub_publish(addr, topic, msg).await,
Expand Down Expand Up @@ -179,38 +189,89 @@ pub async fn network_info(addr: SocketAddr) -> Result<()> {
Ok(())
}

pub async fn node_events(
addr: SocketAddr,
only_transfers: bool,
log_cash_notes: Option<PathBuf>,
) -> Result<()> {
pub async fn node_events(addr: SocketAddr) -> Result<()> {
let endpoint = format!("https://{addr}");
let mut client = SafeNodeClient::connect(endpoint).await?;
let response = client
.node_events(Request::new(NodeEventsRequest {}))
.await?;

if only_transfers {
println!("Listening to transfers notifications... (press Ctrl+C to exit)");
if let Some(ref path) = log_cash_notes {
// create cash_notes dir
fs::create_dir_all(path)?;
println!("Writing cash notes to: {}", path.display());
println!("Listening to node events... (press Ctrl+C to exit)");

let mut stream = response.into_inner();
while let Some(Ok(e)) = stream.next().await {
match NodeEvent::from_bytes(&e.event) {
Ok(event) => println!("New event received: {event:?}"),
Err(_) => {
println!("Error while parsing received NodeEvent");
}
}
}

Ok(())
}

pub async fn transfers_events(
addr: SocketAddr,
sk: String,
log_cash_notes: Option<PathBuf>,
) -> Result<()> {
let (client, mut wallet) = match SecretKey::from_hex(&sk) {
Ok(sk) => {
let client = Client::new(sk.clone(), None, None).await?;
let main_sk = MainSecretKey::new(sk);
let wallet_dir = TempDir::new()?;
let wallet = LocalWallet::load_from_main_key(&wallet_dir, main_sk)?;
(client, wallet)
}
} else {
println!("Listening to node events... (press Ctrl+C to exit)");
Err(err) => return Err(eyre!("Failed to parse hex-encoded SK: {err:?}")),
};
let endpoint = format!("https://{addr}");
let mut node_client = SafeNodeClient::connect(endpoint).await?;
let response = node_client
.node_events(Request::new(NodeEventsRequest {}))
.await?;

println!("Listening to transfers notifications... (press Ctrl+C to exit)");
if let Some(ref path) = log_cash_notes {
// create cash_notes dir
fs::create_dir_all(path)?;
println!("Writing cash notes to: {}", path.display());
}
println!();

let mut stream = response.into_inner();
while let Some(Ok(e)) = stream.next().await {
match NodeEvent::from_bytes(&e.event) {
Ok(NodeEvent::TransferNotif { key, cash_notes }) if only_transfers => {
Ok(NodeEvent::TransferNotif { key, transfers }) => {
println!(
"New transfer notification received for {key:?}, containing {} cash note/s.",
cash_notes.len()
"New transfer notification received for {key:?}, containing {} transfer/s.",
transfers.len()
);

let mut cash_notes = vec![];
for transfer in transfers {
match transfer {
Transfer::Encrypted(_) => {
match client.verify_and_unpack_transfer(&transfer, &wallet).await {
// transfer not for us
Err(err) => {
warn!("Transfer received is invalid or not for us. Ignoring it: {err:?}");
continue;
}
// transfer ok
Ok(cns) => cash_notes.extend(cns),
}
}
Transfer::NetworkRoyalties(_) => {
// we should always send Transfers as they are lighter weight.
warn!("Unencrypted network royalty received via TransferNotification. Ignoring it.");
}
}
}

wallet.deposit(&cash_notes)?;

for cn in cash_notes {
println!(
"CashNote received with {:?}, value: {}",
Expand All @@ -232,10 +293,13 @@ pub async fn node_events(
fs::write(cash_note_file_path, &hex)?;
}
}
println!(
"New balance after depositing received CashNote/s: {}",
wallet.balance()
);
println!();
}
Ok(_) if only_transfers => continue,
Ok(event) => println!("New event received: {event:?}"),
Ok(_) => continue,
Err(_) => {
println!("Error while parsing received NodeEvent");
}
Expand Down
6 changes: 3 additions & 3 deletions sn_node/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::error::{Error, Result};
use bls::PublicKey;
use serde::{Deserialize, Serialize};
use sn_protocol::storage::{ChunkAddress, RegisterAddress};
use sn_transfers::{CashNote, UniquePubkey};
use sn_transfers::{Transfer, UniquePubkey};
use tokio::sync::broadcast;

const NODE_EVENT_CHANNEL_SIZE: usize = 10_000;
Expand Down Expand Up @@ -71,8 +71,8 @@ pub enum NodeEvent {
TransferNotif {
/// Public key the transfer notification is about
key: PublicKey,
/// The cash notes
cash_notes: Vec<CashNote>,
/// The transfers
transfers: Vec<Transfer>,
},
}

Expand Down
6 changes: 3 additions & 3 deletions sn_node/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use sn_protocol::{
messages::{Cmd, CmdResponse, Query, QueryResponse, Request, Response},
NetworkAddress, PrettyPrintRecordKey,
};
use sn_transfers::{CashNote, LocalWallet, MainPubkey, MainSecretKey};
use sn_transfers::{LocalWallet, MainPubkey, MainSecretKey, Transfer};
use std::{
net::SocketAddr,
path::PathBuf,
Expand Down Expand Up @@ -520,7 +520,7 @@ fn try_decode_transfer_notif(msg: &[u8]) -> eyre::Result<NodeEvent> {
);
let key = PublicKey::from_bytes(key_bytes)?;

let cash_notes: Vec<CashNote> = bincode::deserialize(&msg[PK_SIZE..])?;
let transfers: Vec<Transfer> = bincode::deserialize(&msg[PK_SIZE..])?;

Ok(NodeEvent::TransferNotif { key, cash_notes })
Ok(NodeEvent::TransferNotif { key, transfers })
}
25 changes: 17 additions & 8 deletions sn_node/src/put_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,16 +388,16 @@ impl Node {
}

/// Gets CashNotes out of a Payment, this includes network verifications of the Transfer.
/// Return the CashNotes corresponding to our wallet and the ones corresponding to network royalties payment.
/// Return the CashNotes corresponding to our wallet and the trasfers corresponding to network royalties payment.
async fn cash_notes_from_payment(
&self,
payment: &Vec<Transfer>,
wallet: &LocalWallet,
pretty_key: PrettyPrintRecordKey<'static>,
) -> Result<(NanoTokens, Vec<CashNote>, Vec<CashNote>), ProtocolError> {
) -> Result<(NanoTokens, Vec<CashNote>, Vec<Transfer>), ProtocolError> {
let royalties_pk = *NETWORK_ROYALTIES_PK;
let mut cash_notes = vec![];
let mut royalties_cash_notes = vec![];
let mut royalties_transfers = vec![];
let mut received_fee = NanoTokens::zero();

for transfer in payment {
Expand Down Expand Up @@ -426,7 +426,16 @@ impl Node {
"{} network royalties payment cash notes found for record {pretty_key} for a total value of {received_royalties:?}",
cash_notes.len()
);
royalties_cash_notes.extend(cash_notes);
let encrypted_cashnote_redemptions = cash_notes
.into_iter()
.map(Transfer::transfers_from_cash_note)
.collect::<Result<Vec<Transfer>, sn_transfers::Error>>()
.map_err(|err| {
error!("Error generating royalty transfer: {err:?}");
ProtocolError::FailedToEncryptTransfer
})?;

royalties_transfers.extend(encrypted_cashnote_redemptions);
received_fee = received_fee
.checked_add(received_royalties)
.ok_or_else(|| ProtocolError::PaymentExceedsTotalTokens)?;
Expand All @@ -448,7 +457,7 @@ impl Node {
.checked_add(received_fee_to_our_node)
.ok_or_else(|| ProtocolError::PaymentExceedsTotalTokens)?;

Ok((received_fee, cash_notes, royalties_cash_notes))
Ok((received_fee, cash_notes, royalties_transfers))
}
}

Expand All @@ -468,7 +477,7 @@ impl Node {

// unpack transfer
trace!("Unpacking incoming Transfers for record {pretty_key}");
let (received_fee, cash_notes, royalties_cash_notes) = self
let (received_fee, cash_notes, royalties_transfers) = self
.cash_notes_from_payment(&payment, &wallet, pretty_key.clone())
.await?;

Expand All @@ -484,14 +493,14 @@ impl Node {
.reward_wallet_balance
.set(wallet.balance().as_nano() as i64);

if royalties_cash_notes.is_empty() {
if royalties_transfers.is_empty() {
return Err(ProtocolError::NoNetworkRoyaltiesPayment(
pretty_key.into_owned(),
));
}

// publish a notification over gossipsub topic TRANSFER_NOTIF_TOPIC for the network royalties payment.
match bincode::serialize(&royalties_cash_notes) {
match bincode::serialize(&royalties_transfers) {
Ok(serialised) => {
let royalties_pk = *NETWORK_ROYALTIES_PK;
trace!("Publishing a royalties transfer notification over gossipsub for record {pretty_key} and beneficiary {royalties_pk:?}");
Expand Down
2 changes: 1 addition & 1 deletion sn_node/tests/nodes_rewards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ fn spawn_royalties_payment_listener(endpoint: String) -> JoinHandle<Result<usize
println!("Awaiting transfers notifs...");
while let Some(Ok(e)) = stream.next().await {
match NodeEvent::from_bytes(&e.event) {
Ok(NodeEvent::TransferNotif { key, cash_notes: _ }) => {
Ok(NodeEvent::TransferNotif { key, transfers: _ }) => {
println!("Transfer notif received for key {key:?}");
if key == royalties_pk {
count += 1;
Expand Down
2 changes: 2 additions & 0 deletions sn_protocol/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ pub enum Error {
// ---------- transfer errors
#[error("Failed to decypher transfer, we probably are not the recipient")]
FailedToDecypherTransfer,
#[error("Failed to encrypt transfer")]
FailedToEncryptTransfer,
#[error("Failed to get transfer parent spend")]
FailedToGetTransferParentSpend,
#[error("Transfer is invalid: {0}")]
Expand Down
22 changes: 22 additions & 0 deletions sn_transfers/src/wallet/local_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,28 @@ impl LocalWallet {
Ok(())
}

/// Store the given cash_notes on the wallet (without storing them to disk).
pub fn deposit(&mut self, received_cash_notes: &Vec<CashNote>) -> Result<()> {
for cash_note in received_cash_notes {
let id = cash_note.unique_pubkey();

if self.wallet.spent_cash_notes.contains(&id) {
debug!("skipping: cash_note is spent");
continue;
}

if cash_note.derived_key(&self.key).is_err() {
debug!("skipping: cash_note is not our key");
continue;
}

let value = cash_note.value()?;
self.wallet.available_cash_notes.insert(id, value);
}

Ok(())
}

/// Store the given cash_notes to the `cash_notes` dir in the wallet dir.
/// Update and store the updated wallet to disk
/// This function locks the wallet to prevent concurrent processes from writing to it
Expand Down