node: Only fetch from tracked nodes

The tracked nodes include nodes marked tracked in the db, and
the delegates and `Namespaces::All` if it is a new repo.

Signed-off-by: Han Xu <keepsimple@gmail.com>
This commit is contained in:
Han Xu 2023-02-24 17:26:58 -08:00 committed by Alexis Sellier
parent a5746b4f08
commit b030e9ea80
No known key found for this signature in database
4 changed files with 206 additions and 17 deletions

View File

@ -50,6 +50,7 @@ pub use crate::service::session::Session;
use self::gossip::Gossip; use self::gossip::Gossip;
use self::message::InventoryAnnouncement; use self::message::InventoryAnnouncement;
use self::reactor::Reactor; use self::reactor::Reactor;
use self::tracking::NamespacesError;
/// Target number of peers to maintain connections to. /// Target number of peers to maintain connections to.
pub const TARGET_OUTBOUND_PEERS: usize = 8; pub const TARGET_OUTBOUND_PEERS: usize = 8;
@ -102,6 +103,8 @@ pub enum Error {
Routing(#[from] routing::Error), Routing(#[from] routing::Error),
#[error(transparent)] #[error(transparent)]
Tracking(#[from] tracking::Error), Tracking(#[from] tracking::Error),
#[error("namespaces error: {0}")]
Namespaces(#[from] NamespacesError),
} }
/// Function used to query internal service state. /// Function used to query internal service state.
@ -871,27 +874,29 @@ where
} }
// TODO: Buffer/throttle fetches. // TODO: Buffer/throttle fetches.
if self let repo_entry = self.tracking.repo_policy(&message.rid).expect(
.tracking "Service::handle_announcement: error accessing repo tracking configuration",
.is_repo_tracked(&message.rid) );
.expect("Service::handle_announcement: error accessing tracking configuration")
{ if repo_entry.policy == tracking::Policy::Track {
// Refs can be relayed by peers who don't have the data in storage, // Refs can be relayed by peers who don't have the data in storage,
// therefore we only check whether we are connected to the *announcer*, // therefore we only check whether we are connected to the *announcer*,
// which is required by the protocol to only announce refs it has. // which is required by the protocol to only announce refs it has.
if self.sessions.is_connected(announcer) { if self.sessions.is_connected(announcer) {
match message.is_fresh(&self.storage) { match self.should_fetch_refs_announcement(message, &repo_entry.scope) {
Ok(is_fresh) => { Ok(true) => self.fetch(message.rid, announcer),
if is_fresh { Ok(false) => {
// TODO: Only fetch if the refs announced are for peers we're tracking. debug!(target: "service", "Skip fetch the refs from {announcer}")
self.fetch(message.rid, announcer);
}
} }
Err(e) => { Err(e) => {
error!(target: "service", "Failed to check ref announcement freshness: {e}"); error!(target: "service", "Failed to check refs announcement: {e}");
return Err(session::Error::Misbehavior);
} }
} }
} else {
debug!(target: "service", "No sessions connected to {announcer}");
} }
return Ok(relay); return Ok(relay);
} else { } else {
debug!( debug!(
@ -963,6 +968,53 @@ where
Ok(false) Ok(false)
} }
/// A convenient method to check if we should fetch from a `RefsAnnouncement`
/// with `scope`.
fn should_fetch_refs_announcement(
&self,
message: &RefsAnnouncement,
scope: &tracking::Scope,
) -> Result<bool, Error> {
// First, check the freshness.
if !message.is_fresh(&self.storage)? {
debug!(target: "service", "All refs of {} are already in the local node", &message.rid);
return Ok(false);
}
// Second, check the scope.
match scope {
tracking::Scope::All => Ok(true),
tracking::Scope::Trusted => {
match self.tracking.namespaces_for(&self.storage, &message.rid) {
Ok(Namespaces::All) => Ok(true),
Ok(Namespaces::Many(nodes)) => {
// Get the set of trusted nodes except self.
let my_id = self.node_id();
let node_set: HashSet<_> =
nodes.iter().filter(|key| *key != &my_id).collect();
// Check if there is at least one trusted ref.
Ok(message
.refs
.iter()
.any(|(pub_key, _refs)| node_set.contains(pub_key)))
}
Ok(Namespaces::One(key)) => {
Ok(message.refs.iter().any(|(pub_key, _refs)| pub_key == &key))
}
Err(NamespacesError::NoTrusted { rid }) => {
debug!(target: "service", "No trusted nodes to fetch {}", &rid);
Ok(false)
}
Err(e) => {
error!(target: "service", "Failed to obtain namespaces: {e}");
Err(e.into())
}
}
}
}
}
pub fn handle_message( pub fn handle_message(
&mut self, &mut self,
remote: &NodeId, remote: &NodeId,

View File

@ -5,6 +5,7 @@ use std::ops::{Deref, DerefMut};
use crossbeam_channel as chan; use crossbeam_channel as chan;
use log::*; use log::*;
use radicle::storage::ReadRepository;
use crate::address; use crate::address;
use crate::address::Store; use crate::address::Store;
@ -250,7 +251,20 @@ where
} }
pub fn refs_announcement(&self, rid: Id) -> Message { pub fn refs_announcement(&self, rid: Id) -> Message {
let refs = BoundedVec::new(); let mut refs = BoundedVec::new();
if let Ok(repo) = self.storage().repository(rid) {
if let Ok(false) = repo.is_empty() {
if let Ok(remotes) = repo.remotes() {
for (remote_id, remote) in remotes.into_iter() {
if let Err(e) = refs.push((remote_id, remote.refs.unverified())) {
debug!(target: "test", "Failed to push {remote_id} to refs: {e}");
break;
}
}
}
}
}
let ann = AnnouncementMessage::from(RefsAnnouncement { let ann = AnnouncementMessage::from(RefsAnnouncement {
rid, rid,
refs, refs,

View File

@ -669,6 +669,98 @@ fn test_refs_announcement_relay() {
); );
} }
/// Even if Alice is not tracking Bob, Alice will fetch Bob's refs for a repo she doesn't have.
#[test]
fn test_refs_announcement_fetch_trusted_no_inventory() {
logger::init(log::Level::Debug);
let tmp = tempfile::tempdir().unwrap();
let mut alice = Peer::config(
"alice",
[7, 7, 7, 7],
Storage::open(tmp.path().join("alice")).unwrap(),
peer::Config::default(),
);
let bob = {
let mut rng = fastrand::Rng::new();
let signer = MockSigner::new(&mut rng);
let storage = fixtures::storage(tmp.path().join("bob"), &signer).unwrap();
Peer::config(
"bob",
[9, 9, 9, 9],
storage,
peer::Config {
signer,
rng,
..peer::Config::default()
},
)
};
let bob_inv = bob.storage().inventory().unwrap();
let rid = bob_inv[0];
alice.track_repo(&rid, tracking::Scope::Trusted).unwrap();
alice.connect_to(&bob);
// Alice receives Bob's refs.
alice.receive(bob.id(), bob.refs_announcement(rid));
// Alice fetches Bob's refs as this is a new repo.
assert_eq!(
alice.messages(bob.id()).next(),
Some(Message::Fetch { rid })
);
}
/// Alice and Bob both have the same repo.
///
/// First, Alice will not fetch from Bob's `RefsAnnouncement` as Alice does not
/// track Bob as `Trusted`.
///
/// Later Alice tracks Bob, and will be able to fetch Bob's refs.
#[test]
fn test_refs_announcement_trusted() {
logger::init(log::Level::Debug);
// Create MockStorage for Alice and Bob. Both will have repo with `rid`.
let storage_alice = arbitrary::nonempty_storage(1);
let rid = *storage_alice.inventory.keys().next().unwrap();
let storage_bob = storage_alice.clone();
let mut alice = Peer::with_storage("alice", [7, 7, 7, 7], storage_alice);
let mut bob = Peer::with_storage("bob", [8, 8, 8, 8], storage_bob);
// Generate some refs for Bob under their own node_id.
let refs = arbitrary::gen::<Refs>(8);
let signed_refs = refs.signed(bob.signer()).unwrap();
let node_id = bob.id;
bob.storage_mut().insert_remote(rid, node_id, signed_refs);
// Alice uses Scope::Trusted, and did not track Bob yet.
alice.connect_to(&bob);
alice.track_repo(&rid, tracking::Scope::Trusted).unwrap();
// Alice receives Bob's refs
alice.receive(bob.id(), bob.refs_announcement(rid));
// Alice does not fetch as Alice is not tracking Bob.
assert!(
alice.messages(bob.id()).next().is_none(),
"Alice is not tracking bob yet."
);
// Alice starts to track Bob.
let (sender, receiver) = chan::bounded(1);
alice.command(Command::TrackNode(bob.id, Some("bob".to_string()), sender));
let policy_change = receiver.recv().map_err(runtime::HandleError::from).unwrap();
assert!(policy_change);
// Bob announces refs again.
bob.elapse(LocalDuration::from_mins(1)); // Make sure our announcement is fresh.
alice.receive(bob.id(), bob.refs_announcement(rid));
assert_matches!(alice.messages(bob.id()).next(), Some(Message::Fetch { .. }));
}
#[test] #[test]
fn test_refs_announcement_no_subscribe() { fn test_refs_announcement_no_subscribe() {
let storage = arbitrary::nonempty_storage(1); let storage = arbitrary::nonempty_storage(1);

View File

@ -8,6 +8,7 @@ use radicle_git_ext as git_ext;
use crate::crypto::{Signer, Verified}; use crate::crypto::{Signer, Verified};
use crate::identity::doc::{Doc, Id}; use crate::identity::doc::{Doc, Id};
use crate::identity::IdentityError; use crate::identity::IdentityError;
use crate::node::NodeId;
pub use crate::storage::*; pub use crate::storage::*;
@ -15,6 +16,10 @@ pub use crate::storage::*;
pub struct MockStorage { pub struct MockStorage {
pub path: PathBuf, pub path: PathBuf,
pub inventory: HashMap<Id, Doc<Verified>>, pub inventory: HashMap<Id, Doc<Verified>>,
/// All refs keyed by RID.
/// Each value is a map of refs keyed by node Id (public key).
pub remotes: HashMap<Id, HashMap<NodeId, refs::SignedRefs<Verified>>>,
} }
impl MockStorage { impl MockStorage {
@ -22,6 +27,7 @@ impl MockStorage {
Self { Self {
path: PathBuf::default(), path: PathBuf::default(),
inventory: inventory.into_iter().collect(), inventory: inventory.into_iter().collect(),
remotes: HashMap::new(),
} }
} }
@ -29,8 +35,22 @@ impl MockStorage {
Self { Self {
path: PathBuf::default(), path: PathBuf::default(),
inventory: HashMap::new(), inventory: HashMap::new(),
remotes: HashMap::new(),
} }
} }
/// Add a remote `node` with `signed_refs` for the repo `rid`.
pub fn insert_remote(
&mut self,
rid: Id,
node: NodeId,
signed_refs: refs::SignedRefs<Verified>,
) {
self.remotes
.entry(rid)
.or_insert(HashMap::new())
.insert(node, signed_refs);
}
} }
impl ReadStorage for MockStorage { impl ReadStorage for MockStorage {
@ -64,6 +84,7 @@ impl ReadStorage for MockStorage {
Ok(MockRepository { Ok(MockRepository {
id: rid, id: rid,
doc: doc.clone(), doc: doc.clone(),
remotes: self.remotes.get(&rid).cloned().unwrap_or_default(),
}) })
} }
} }
@ -76,6 +97,7 @@ impl WriteStorage for MockStorage {
Ok(MockRepository { Ok(MockRepository {
id: rid, id: rid,
doc: doc.clone(), doc: doc.clone(),
remotes: self.remotes.get(&rid).cloned().unwrap_or_default(),
}) })
} }
@ -84,9 +106,11 @@ impl WriteStorage for MockStorage {
} }
} }
#[derive(Clone, Debug)]
pub struct MockRepository { pub struct MockRepository {
id: Id, id: Id,
doc: Doc<Verified>, doc: Doc<Verified>,
remotes: HashMap<NodeId, refs::SignedRefs<Verified>>,
} }
impl ReadRepository for MockRepository { impl ReadRepository for MockRepository {
@ -95,7 +119,7 @@ impl ReadRepository for MockRepository {
} }
fn is_empty(&self) -> Result<bool, git2::Error> { fn is_empty(&self) -> Result<bool, git2::Error> {
Ok(true) Ok(self.remotes.is_empty())
} }
fn head(&self) -> Result<(fmt::Qualified, Oid), IdentityError> { fn head(&self) -> Result<(fmt::Qualified, Oid), IdentityError> {
@ -114,12 +138,19 @@ impl ReadRepository for MockRepository {
todo!() todo!()
} }
fn remote(&self, _remote: &RemoteId) -> Result<Remote<Verified>, refs::Error> { fn remote(&self, remote: &RemoteId) -> Result<Remote<Verified>, refs::Error> {
todo!() self.remotes
.get(remote)
.map(|refs| Remote::new(*remote, refs.clone()))
.ok_or(refs::Error::InvalidRef)
} }
fn remotes(&self) -> Result<Remotes<Verified>, refs::Error> { fn remotes(&self) -> Result<Remotes<Verified>, refs::Error> {
todo!() Ok(self
.remotes
.iter()
.map(|(id, refs)| (*id, Remote::new(*id, refs.clone())))
.collect())
} }
fn commit(&self, _oid: Oid) -> Result<git2::Commit, git_ext::Error> { fn commit(&self, _oid: Oid) -> Result<git2::Commit, git_ext::Error> {