Skip to content

Commit c104d65

Browse files
author
SyntheticBird45
committed
Implements P2P Bucket data structure
This commit implements a "Bucket" data structure that is a collection of data that discriminates its items into "buckets" (vector of size N) following a defined function. - Implements Bucket data structure and Bucketable trait - Implements Bucketable for Ipv4Addr - Added the crate to the workspace dependencies - Added arrayvec as a dependency
1 parent b57ee2f commit c104d65

File tree

5 files changed

+197
-0
lines changed

5 files changed

+197
-0
lines changed

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ members = [
1616
"net/wire",
1717
"p2p/p2p",
1818
"p2p/p2p-core",
19+
"p2p/bucket",
1920
"p2p/dandelion-tower",
2021
"p2p/async-buffer",
2122
"p2p/address-book",
@@ -64,6 +65,7 @@ cuprate-levin = { path = "net/levin" ,default-feature
6465
cuprate-wire = { path = "net/wire" ,default-features = false}
6566
cuprate-p2p = { path = "p2p/p2p" ,default-features = false}
6667
cuprate-p2p-core = { path = "p2p/p2p-core" ,default-features = false}
68+
cuprate-p2p-bucket = { path = "p2p/p2p-bucket" ,default-features = false}
6769
cuprate-dandelion-tower = { path = "p2p/dandelion-tower" ,default-features = false}
6870
cuprate-async-buffer = { path = "p2p/async-buffer" ,default-features = false}
6971
cuprate-address-book = { path = "p2p/address-book" ,default-features = false}
@@ -80,6 +82,7 @@ cuprate-rpc-interface = { path = "rpc/interface" ,default-feature
8082

8183
# External dependencies
8284
anyhow = { version = "1.0.89", default-features = false }
85+
arrayvec = { version = "0.7", default-features = false }
8386
async-trait = { version = "0.1.82", default-features = false }
8487
bitflags = { version = "2.6.0", default-features = false }
8588
blake3 = { version = "1", default-features = false }

books/architecture/src/appendix/crates.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ cargo doc --open --package cuprate-blockchain
3535
| [`cuprate-async-buffer`](https://doc.cuprate.org/cuprate_async_buffer) | [`p2p/async-buffer/`](https://github.com/Cuprate/cuprate/tree/main/p2p/async-buffer) | A bounded SPSC, FIFO, asynchronous buffer that supports arbitrary weights for values
3636
| [`cuprate-dandelion-tower`](https://doc.cuprate.org/cuprate_dandelion_tower) | [`p2p/dandelion-tower/`](https://github.com/Cuprate/cuprate/tree/main/p2p/dandelion-tower) | TODO
3737
| [`cuprate-p2p`](https://doc.cuprate.org/cuprate_p2p) | [`p2p/p2p/`](https://github.com/Cuprate/cuprate/tree/main/p2p/p2p) | TODO
38+
| [`cuprate-p2p-bucket`](https://doc.cuprate.org/cuprate_p2p_bucket) | [`p2p/bucket/`](https://github.com/Cuprate/cuprate/tree/main/p2p/bucket) | A collection data structure discriminating its items into "buckets" of limited size.
3839
| [`cuprate-p2p-core`](https://doc.cuprate.org/cuprate_p2p_core) | [`p2p/p2p-core/`](https://github.com/Cuprate/cuprate/tree/main/p2p/p2p-core) | TODO
3940

4041
## Storage

p2p/bucket/Cargo.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "cuprate-p2p-bucket"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license = "MIT"
6+
authors = ["SyntheticBird"]
7+
8+
[dependencies]
9+
arrayvec = { workspace = true }
10+
rand = { workspace = true, features = ["std", "std_rng"]}
11+
12+
[lints]
13+
workspace = true

p2p/bucket/src/lib.rs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
//! Bucket data structure
2+
//!
3+
//! A collection data structure that discriminates its unique items and place them into "buckets".
4+
//!
5+
//! The item must implement the [`Bucketable`] trait that defines how to create the discriminant
6+
//! from the item type. The data structure will internally contain any item into "buckets" or vectors
7+
//! of sized capacity `N` that regroup all the stored items with this specific discriminant.
8+
//!
9+
//! A practical example of this data structure is for storing `N` amount of IP discriminated by their subnets.
10+
//! You can store in each "buckets" corresponding to a `/16` subnet up to `N` IPs of that subnet.
11+
//!
12+
//! # Example
13+
//!
14+
//! ```
15+
//! use cuprate_p2p_bucket::Bucket;
16+
//! use std::net::Ipv4Addr;
17+
//!
18+
//! // Create a new bucket that can store at most 2 IPs in a particular `/16` subnet.
19+
//! let mut bucket = Bucket::<2,Ipv4Addr>::new();
20+
//!
21+
//! // Fulfill the `96.96.0.0/16` bucket.
22+
//! bucket.push("96.96.0.1".parse().unwrap());
23+
//! bucket.push("96.96.0.2".parse().unwrap());
24+
//! assert_eq!(2, bucket.len());
25+
//! assert_eq!(2, bucket.len_bucket(&[96_u8,96_u8]).unwrap());
26+
//!
27+
//! // Push a new IP from another subnet
28+
//! bucket.push("127.0.0.1".parse().unwrap());
29+
//! assert_eq!(3, bucket.len());
30+
//! assert_eq!(2, bucket.len_bucket(&[96_u8,96_u8]).unwrap());
31+
//! assert_eq!(1, bucket.len_bucket(&[127_u8,0_u8]).unwrap());
32+
//!
33+
//! // Attempting to push a new IP within `96.96.0.0/16` bucket will return the IP back
34+
//! // as this subnet is already full.
35+
//! let pushed = bucket.push("96.96.0.3".parse().unwrap());
36+
//! assert!(pushed.is_some());
37+
//! assert_eq!(2, bucket.len_bucket(&[96_u8,96_u8]).unwrap());
38+
//!
39+
//! ```
40+
41+
use arrayvec::{ArrayVec, CapacityError};
42+
use rand::random;
43+
44+
use std::{collections::BTreeMap, net::Ipv4Addr};
45+
46+
/// A discriminant that can be computed from the type.
47+
pub trait Bucketable: Sized + Eq + Clone {
48+
/// The type of the discriminant being used in the Binary tree.
49+
type Discriminant: Ord + AsRef<[u8]>;
50+
51+
/// Method that can compute the discriminant from the item.
52+
fn discriminant(&self) -> Self::Discriminant;
53+
}
54+
55+
/// A collection data structure discriminating its unique items
56+
/// with a specified method. Limiting the amount of items stored
57+
/// with that discriminant to the const `N`.
58+
pub struct Bucket<const N: usize, I: Bucketable> {
59+
/// The storage of the bucket
60+
storage: BTreeMap<I::Discriminant, ArrayVec<I, N>>,
61+
}
62+
63+
impl<const N: usize, I: Bucketable> Bucket<N, I> {
64+
/// Create a new Bucket
65+
pub const fn new() -> Self {
66+
Self {
67+
storage: BTreeMap::new(),
68+
}
69+
}
70+
71+
/// Push a new element into the Bucket
72+
///
73+
/// Will internally create a new vector for each new discriminant being
74+
/// generated from an item.
75+
///
76+
/// This function WILL NOT push the element if it already exists.
77+
///
78+
/// Return `None` if the item has been pushed or ignored. `Some(I)` if
79+
/// the vector is full.
80+
///
81+
/// # Example
82+
///
83+
/// ```
84+
/// use cuprate_p2p_bucket::Bucket;
85+
/// use std::net::Ipv4Addr;
86+
///
87+
/// let mut bucket = Bucket::<8,Ipv4Addr>::new();
88+
///
89+
/// // Push a first IP address.
90+
/// bucket.push("127.0.0.1".parse().unwrap());
91+
/// assert_eq!(1, bucket.len());
92+
///
93+
/// // Push the same IP address a second time.
94+
/// bucket.push("127.0.0.1".parse().unwrap());
95+
/// assert_eq!(1, bucket.len());
96+
/// ```
97+
pub fn push(&mut self, item: I) -> Option<I> {
98+
let discriminant = item.discriminant();
99+
100+
if let Some(vec) = self.storage.get_mut(&discriminant) {
101+
// Push the item if it doesn't exist.
102+
if !vec.contains(&item) {
103+
return vec.try_push(item).err().map(CapacityError::element);
104+
}
105+
} else {
106+
// Initialize the vector if not found.
107+
let mut vec = ArrayVec::<I, N>::new();
108+
vec.push(item);
109+
self.storage.insert(discriminant, vec);
110+
}
111+
112+
None
113+
}
114+
115+
/// Will attempt to remove an item from the bucket.
116+
pub fn remove(&mut self, item: &I) -> Option<I> {
117+
self.storage.get_mut(&item.discriminant()).and_then(|vec| {
118+
vec.iter()
119+
.enumerate()
120+
.find_map(|(i, v)| (item == v).then_some(i))
121+
.map(|index| vec.swap_remove(index))
122+
})
123+
}
124+
125+
/// Return the number of item stored within the storage
126+
pub fn len(&self) -> usize {
127+
self.storage.values().map(ArrayVec::len).sum()
128+
}
129+
130+
/// Return the number of item stored with a specific discriminant.
131+
///
132+
/// This method returns None if the bucket with this discriminant
133+
/// doesn't exist.
134+
pub fn len_bucket(&self, discriminant: &I::Discriminant) -> Option<usize> {
135+
self.storage.get(discriminant).map(ArrayVec::len)
136+
}
137+
138+
/// Return `true` if the storage contains no items
139+
pub fn is_empty(&self) -> bool {
140+
self.len() == 0
141+
}
142+
143+
/// Return a reference to an item chosen at random.
144+
///
145+
/// Repeated use of this function will provide a normal distribution of
146+
/// items based on their discriminants.
147+
pub fn get_random(&mut self) -> Option<&I> {
148+
// Get the total amount of discriminants to explore.
149+
let len = self.storage.len();
150+
151+
// Get a random bucket.
152+
let (_, vec) = self.storage.iter().nth(random::<usize>() / len).unwrap();
153+
154+
// Return a reference chose at random.
155+
vec.get(random::<usize>() / vec.len())
156+
}
157+
}
158+
159+
impl<const N: usize, I: Bucketable> Default for Bucket<N, I> {
160+
fn default() -> Self {
161+
Self::new()
162+
}
163+
}
164+
165+
impl Bucketable for Ipv4Addr {
166+
/// We are discriminating by `/16` subnets.
167+
type Discriminant = [u8; 2];
168+
169+
fn discriminant(&self) -> Self::Discriminant {
170+
[self.octets()[0], self.octets()[1]]
171+
}
172+
}

0 commit comments

Comments
 (0)