Skip to content

Commit

Permalink
feat: include output DBC within payment proof for Chunks storage
Browse files Browse the repository at this point in the history
  • Loading branch information
bochaco committed Jun 13, 2023
1 parent 4a98902 commit 1955c82
Show file tree
Hide file tree
Showing 9 changed files with 91 additions and 72 deletions.
6 changes: 2 additions & 4 deletions sn_cli/src/subcommands/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ use super::wallet::pay_for_storage;
use bytes::Bytes;
use clap::Parser;
use color_eyre::Result;
use sn_client::{Client, Files};
use sn_client::{Client, Files, PaymentProofsMap};
use sn_protocol::storage::ChunkAddress;
use sn_transfers::payment_proof::PaymentProofsMap;

use std::{
fs,
Expand Down Expand Up @@ -90,8 +89,7 @@ async fn upload_files(

// We make the payment for Chunks storage only if requested by the user
let payment_proofs = if pay {
let (_dbc, payment_proofs) = pay_for_storage(&client, root_dir, &files_path).await?;
payment_proofs
pay_for_storage(&client, root_dir, &files_path).await?
} else {
PaymentProofsMap::default()
};
Expand Down
13 changes: 5 additions & 8 deletions sn_cli/src/subcommands/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +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 sn_client::{Client, Files, WalletClient};
use sn_dbc::{Dbc, Token};
use sn_transfers::{
payment_proof::PaymentProofsMap,
wallet::{parse_public_address, LocalWallet},
};
use sn_client::{Client, Files, PaymentProofsMap, WalletClient};
use sn_dbc::Token;
use sn_transfers::wallet::{parse_public_address, LocalWallet};

use bytes::Bytes;
use clap::Parser;
Expand Down Expand Up @@ -142,7 +139,7 @@ pub(super) async fn pay_for_storage(
client: &Client,
root_dir: &Path,
files_path: &Path,
) -> Result<(Dbc, PaymentProofsMap)> {
) -> Result<PaymentProofsMap> {
let wallet = LocalWallet::load_from(root_dir).await?;
let mut wallet_client = WalletClient::new(client.clone(), wallet);
let file_api: Files = Files::new(client.clone());
Expand Down Expand Up @@ -175,5 +172,5 @@ pub(super) async fn pay_for_storage(
wallet.store_created_dbc(new_dbc.clone()).await?;
println!("Successfully stored new dbc ({dbc_id:?}) to wallet dir. It can now be sent to the storage nodes when uploading paid chunks.");

Ok((new_dbc, proofs))
Ok(proofs)
}
2 changes: 1 addition & 1 deletion sn_client/src/file_apis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
use super::{
chunks::{to_chunk, DataMapLevel, Error, LargeFile, SmallFile},
error::Result,
wallet::PaymentProofsMap,
Client,
};

use sn_protocol::storage::{Chunk, ChunkAddress};
use sn_transfers::payment_proof::PaymentProofsMap;

use bincode::deserialize;
use bytes::Bytes;
Expand Down
2 changes: 1 addition & 1 deletion sn_client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub use self::{
faucet::{get_tokens_from_faucet, load_faucet_wallet},
file_apis::Files,
register::{Register, RegisterOffline},
wallet::{send, WalletClient},
wallet::{send, PaymentProofsMap, WalletClient},
};

use self::event::ClientEventsChannel;
Expand Down
30 changes: 26 additions & 4 deletions sn_client/src/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,21 @@
use super::Client;

use sn_dbc::{Dbc, PublicAddress, Token};
use sn_protocol::messages::{MerkleTreeNodesType, PaymentProof};
use sn_transfers::{
client_transfers::TransferOutputs,
payment_proof::{build_payment_proofs, PaymentProofsMap},
payment_proof::build_payment_proofs,
wallet::{Error, LocalWallet, Result},
};

use bls::SecretKey;
use futures::future::join_all;
use std::iter::Iterator;
use std::{collections::BTreeMap, iter::Iterator};
use xor_name::XorName;

/// Map from content address name to its corresponding PaymentProof.
pub type PaymentProofsMap = BTreeMap<MerkleTreeNodesType, PaymentProof>;

/// A wallet client can be used to send and
/// receive tokens to/from other wallets.
pub struct WalletClient {
Expand Down Expand Up @@ -80,13 +84,31 @@ impl WalletClient {
let to = vec![(amount, PublicAddress::new(SecretKey::random().public_key()))];

// Let's build the payment proofs for list of content addresses
let (reason_hash, payment_proofs) = build_payment_proofs(content_addrs)
let (reason_hash, audit_trail_info) = build_payment_proofs(content_addrs)
.map_err(|err| Error::StoragePaymentReason(err.to_string()))?;

let transfer = self.wallet.local_send(to, Some(reason_hash)).await?;

match &transfer.created_dbcs[..] {
[info, ..] => Ok((info.dbc.clone(), payment_proofs)),
[info, ..] => {
let dbc = info.dbc.clone();
let payment_proofs = audit_trail_info
.into_iter()
.map(|(addr, (audit_trail, path))| {
(
addr,
PaymentProof {
reason_hash,
dbc: dbc.clone(),
audit_trail,
path,
},
)
})
.collect();

Ok((dbc, payment_proofs))
}
[] => Err(Error::CouldNotSendTokens(
"No DBCs were returned from the wallet.".into(),
)),
Expand Down
15 changes: 5 additions & 10 deletions sn_networking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,7 @@ mod tests {
use rand::{thread_rng, Rng};
use sn_logging::init_test_logger;
use sn_protocol::{
messages::{Cmd, CmdResponse, Hash, PaymentProof, Request, Response},
messages::{CmdResponse, Query, Request, Response},
storage::Chunk,
};
use std::{net::SocketAddr, path::Path, time::Duration};
Expand Down Expand Up @@ -724,17 +724,12 @@ mod tests {
}
});

// Send a request to store a random chunk to `self`.
// Send a request to query a random chunk to `self`.
let mut random_data = [0u8; 128];
thread_rng().fill(&mut random_data);
let req = Request::Cmd(Cmd::StoreChunk {
chunk: Chunk::new(Bytes::copy_from_slice(&random_data)),
payment: Some(PaymentProof {
reason_hash: Hash::hash(&random_data),
audit_trail: vec![],
path: vec![],
}),
});
let req = Request::Query(Query::GetChunk(
*Chunk::new(Bytes::copy_from_slice(&random_data)).address(),
));
// Send the request to `self` and wait for a response.
let now = tokio::time::Instant::now();
loop {
Expand Down
16 changes: 13 additions & 3 deletions sn_node/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ use sn_networking::{
};
use sn_protocol::{
error::Error as ProtocolError,
messages::{Cmd, CmdResponse, Query, QueryResponse, RegisterCmd, Request, Response},
messages::{
Cmd, CmdResponse, PaymentProof, Query, QueryResponse, RegisterCmd, Request, Response,
},
storage::{registers::User, Chunk, DbcAddress},
NetworkAddress,
};
Expand Down Expand Up @@ -248,9 +250,17 @@ impl Node {
debug!("That's a store chunk in for :{addr_name:?}");

// TODO: temporarily payment proof is optional
if let Some(payment_proof) = payment {
if let Some(PaymentProof {
reason_hash,
audit_trail,
path,
..
}) = &payment
{
// Let's make sure the payment proof is valid for the chunk's name
if let Err(err) = validate_payment_proof(addr_name, &payment_proof) {
if let Err(err) =
validate_payment_proof(addr_name, reason_hash, audit_trail, path)
{
debug!("Chunk payment proof deemed invalid: {err:?}");
let resp =
CmdResponse::StoreChunk(Err(ProtocolError::InvalidPaymentProof {
Expand Down
9 changes: 6 additions & 3 deletions sn_protocol/src/messages/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::{
use super::RegisterCmd;

// TODO: remove this dependency and define these types herein.
pub use sn_dbc::{Hash, SignedSpend};
pub use sn_dbc::{Dbc, Hash, SignedSpend};

use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -103,11 +103,14 @@ pub type MerkleTreeNodesType = [u8; 32];

#[derive(Clone, Eq, PartialEq, Serialize, Deserialize, custom_debug::Debug)]
pub struct PaymentProof {
// Output DBC for nodes to check the Chunk payment is valid and inputs have
// been effectivelly spent on the network.
pub dbc: Dbc,
// Reason-hash value set in the input/parent DBCs spent for this storage payment.
// TOOD: pass the output DBC instead, nodes can check input/parent DBCs' reason-hash among other pending validations.
// TODO: remove it since nodes should get this from the input/parent spent DBC/s

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
pub reason_hash: Hash,
// Merkletree audit trail to prove the Chunk has been paid by the
// given DBC (using the DBC's 'reason' field)
// given DBC (using DBC's parent/s 'reason' field)
pub audit_trail: Vec<MerkleTreeNodesType>,
// Path of the audit trail
pub path: Vec<usize>,
Expand Down
70 changes: 32 additions & 38 deletions sn_transfers/src/payment_proof/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ mod hasher;
use error::{Error, Result};
use hasher::Sha256Hasher;

use sn_protocol::messages::{Hash, MerkleTreeNodesType, PaymentProof};
use sn_protocol::messages::{Hash, MerkleTreeNodesType};

use merkletree::{
hash::Algorithm,
Expand All @@ -73,17 +73,17 @@ use std::collections::BTreeMap;
use typenum::{UInt, UTerm, B0, B1};
use xor_name::XorName;

/// Map from content address name to its corresponding audit trail and trail path.
pub type PaymentProofsTrailInfoMap =
BTreeMap<MerkleTreeNodesType, (Vec<MerkleTreeNodesType>, Vec<usize>)>;

// We use a binary Merkle-tree to build payment proofs
type BinaryMerkletreeProofType = Proof<MerkleTreeNodesType, UInt<UInt<UTerm, B1>, B0>>;

/// Map from content address name to its corresponding PaymentProof
pub type PaymentProofsMap = BTreeMap<MerkleTreeNodesType, PaymentProof>;

/// Build a Merkletree to generate the PaymentProofs for each of the content addresses provided
// TODO: provide fix against https://en.wikipedia.org/wiki/Preimage_attack ?
/// Build a Merkletree to generate the audit trail and path for each of the content addresses provided.
pub fn build_payment_proofs<'a>(
content_addrs: impl Iterator<Item = &'a XorName>,
) -> Result<(Hash, PaymentProofsMap)> {
) -> Result<(Hash, PaymentProofsTrailInfoMap)> {
// Let's build the Merkle-tree from list of addresses needed to generate the payment proofs
let mut addrs: Vec<_> = content_addrs
.map(|addr| {
Expand Down Expand Up @@ -125,34 +125,29 @@ pub fn build_payment_proofs<'a>(
reason: err.to_string(),
})?;

payment_proofs.insert(
addr,
PaymentProof {
reason_hash,
audit_trail: proof.lemma().to_vec(),
path: proof.path().to_vec(),
},
);
payment_proofs.insert(addr, (proof.lemma().to_vec(), proof.path().to_vec()));
}

Ok((reason_hash, payment_proofs))
}

/// Verify if the payment proof is valid and contains a valid audit trail for the given xorname
pub fn validate_payment_proof(addr_name: XorName, payment: &PaymentProof) -> Result<()> {
pub fn validate_payment_proof(
addr_name: XorName,
reason_hash: &Hash,
audit_trail: &[MerkleTreeNodesType],
path: &[usize],
) -> Result<()> {
trace!("Verifying payment proof for chunk store {addr_name:?} ...");

// We build the merkletree leaf value from the received xorname, i.e. hash(xorname).
// The DBC's reason-hash should match the root of the PaymentProof's audit trail (lemma)
let mut hasher = Sha256Hasher::default();
let leaf_to_validate = hasher.leaf(addr_name.0);

let proof = BinaryMerkletreeProofType::new::<UTerm, UTerm>(
None,
payment.audit_trail.clone(),
payment.path.clone(),
)
.map_err(|err| Error::InvalidAuditTrail(err.to_string()))?;
let proof =
BinaryMerkletreeProofType::new::<UTerm, UTerm>(None, audit_trail.to_vec(), path.to_vec())
.map_err(|err| Error::InvalidAuditTrail(err.to_string()))?;

if leaf_to_validate != proof.item() {
return Err(Error::AuditTrailItemMismatch(addr_name));
Expand All @@ -167,10 +162,10 @@ pub fn validate_payment_proof(addr_name: XorName, payment: &PaymentProof) -> Res
));
}

if payment.reason_hash == proof.root().into() {
if *reason_hash == proof.root().into() {
Ok(())
} else {
Err(Error::ReasonHashMismatch(payment.reason_hash.to_hex()))
Err(Error::ReasonHashMismatch(reason_hash.to_hex()))
}
}

Expand Down Expand Up @@ -247,48 +242,47 @@ mod tests {

let addrs = [name0, name1, name2];

let (_, payment_proofs) = build_payment_proofs(addrs.iter())?;
let (reason_hash, payment_proofs) = build_payment_proofs(addrs.iter())?;

assert_eq!(payment_proofs.len(), addrs.len());

assert!(
matches!(payment_proofs.get(&name0.0), Some(proof) if validate_payment_proof(name0, proof).is_ok())
matches!(payment_proofs.get(&name0.0), Some((audit_trail, path)) if validate_payment_proof(name0, &reason_hash, audit_trail, path).is_ok())
);
assert!(
matches!(payment_proofs.get(&name1.0), Some(proof) if validate_payment_proof(name1, proof).is_ok())
matches!(payment_proofs.get(&name1.0), Some((audit_trail, path)) if validate_payment_proof(name1, &reason_hash, audit_trail, path).is_ok())
);
assert!(
matches!(payment_proofs.get(&name2.0), Some(proof) if validate_payment_proof(name2, proof).is_ok())
matches!(payment_proofs.get(&name2.0), Some(( audit_trail, path)) if validate_payment_proof(name2, &reason_hash, audit_trail, path).is_ok())
);

let mut proof = payment_proofs
let (audit_trail, path) = payment_proofs
.get(&name2.0)
.cloned()
.ok_or_else(|| eyre!("Failed to obtain valid payment proof"))?;
let invalid_name = XorName([99; 32]);
assert!(matches!(
validate_payment_proof(invalid_name, &proof),
validate_payment_proof(invalid_name, &reason_hash, &audit_trail, &path),
Err(Error::AuditTrailItemMismatch(name)) if name == invalid_name
));

let mut corrupted_proof = proof.clone();
corrupted_proof.audit_trail[1][0] = 0; // corrupt one byte of the audit trail
let mut corrupted_audit_trail = audit_trail.clone();
corrupted_audit_trail[1][0] = 0; // corrupt one byte of the audit trail
assert!(matches!(
validate_payment_proof(name2, &corrupted_proof),
validate_payment_proof(name2, &reason_hash, &corrupted_audit_trail, &path),
Err(Error::AuditTrailSelfValidation(_))
));

let mut corrupted_proof = proof.clone();
corrupted_proof.audit_trail.push(name0.0); // corrupt the audit trail by adding some random item
let mut corrupted_audit_trail = audit_trail.clone();
corrupted_audit_trail.push(name0.0); // corrupt the audit trail by adding some random item
assert!(matches!(
validate_payment_proof(name2, &corrupted_proof),
validate_payment_proof(name2, &reason_hash, &corrupted_audit_trail, &path),
Err(Error::InvalidAuditTrail(_))
));

let invalid_reason_hash: Hash = [66; 32].into();
proof.reason_hash = invalid_reason_hash; // corrupt the PaymentProof by setting an invalid reason hash value
assert!(matches!(
validate_payment_proof(name2, &proof),
validate_payment_proof(name2, &invalid_reason_hash, &audit_trail, &path),
Err(Error::ReasonHashMismatch(hex)) if hex == invalid_reason_hash.to_hex()
));

Expand Down

0 comments on commit 1955c82

Please sign in to comment.