diff --git a/src/feed_id.rs b/src/feed_id.rs index 14fb413..97926d2 100644 --- a/src/feed_id.rs +++ b/src/feed_id.rs @@ -7,6 +7,8 @@ use std::cmp::Ordering; pub struct FeedId(pub [u8; 32]); impl FeedId { + pub const LENGTH: usize = 32; + pub fn encode(&self) -> [u8; 32] { self.0.clone() } diff --git a/src/go_set/go_set_claim.rs b/src/go_set/go_set_claim.rs index 1e24da9..46baf15 100644 --- a/src/go_set/go_set_claim.rs +++ b/src/go_set/go_set_claim.rs @@ -1,20 +1,169 @@ use crate::feed_id::FeedId; -use crate::go_set::{GOSet, GOSetXor}; +use crate::go_set::GOSetXor; -use crate::Error; +#[derive(Debug, PartialEq)] +pub enum GOSetClaimError { + InvalidDMX, + InvalidTypeCode, +} +#[derive(Debug)] pub struct GOSetClaim { - lowest_feed_id: FeedId, - highest_feed_id: FeedId, - xor: GOSetXor, - count: u8, + pub(crate) lowest_feed_id: FeedId, + pub(crate) highest_feed_id: FeedId, + pub(crate) xor: GOSetXor, + pub(crate) count: u8, } impl GOSetClaim { - pub fn encode_go_set(go_set: &GOSet) -> [u8; 105] { - todo!() + const GOSET_DMX: [u8; 7] = [0, 1, 2, 3, 4, 5, 6]; + // TODO: calculate actual DMX + // ``` + // GOSET_DMX_MATERIAL = "tinySSB-0.1 GOset 1" + // GOSET_DMX = first 7 bytes of SHA256(GOSET_DMX_MATERIAL) + // ``` + + pub fn encode(&self) -> [u8; 105] { + // NOTE: we're gonna want to make a range of claims + // 1. how do we deal with the concept of a "subset" of a claim? + // PG: I think that GoSets should implement methods that let you create new sets from + // themselves. So GoSet could have a `bisect(&self) -> GoSet` method. If bisect is the + // right word + // 2. where does the logic live about comparing a claim with a goset? + // PG: I implemented PartialEq on GoSet where the right hand side is GoSetClaim. + // 3. what's the algorithm for the claim-dance? + // PG: That's gonna live in some new type we haven't made yet + // + // PG: I think that if GoSet.lowest_feed_id returns an option then GoSetClaim + // lowest_feed_id should also be an option. Same for highest obvs. + + // [DMX (7B) | 'c' (1 byte) | lowest FeedId (32 bytes) | highest FeedId (32 bytes) | XOR (32 bytes) | cnt (1 byte) ] + let mut claim = [0; 105]; + + let dmx = Self::GOSET_DMX; + let type_code: [u8; 1] = [b'c']; + let lowest_feed_id = self.lowest_feed_id.encode(); + let highest_feed_id = self.highest_feed_id.encode(); + let xor = self.xor.encode(); + let count: [u8; 1] = [self.count]; + + let chunks: [&[u8]; 6] = [ + &dmx, + &type_code, + &lowest_feed_id, + &highest_feed_id, + &xor, + &count, + ]; + + let mut offset: usize = 0; + for chunk in chunks { + let len = chunk.len(); + claim[offset..offset + len].copy_from_slice(chunk); + offset += len; + } + + claim + } + + pub fn decode(bytes: &[u8; 105]) -> Result { + let mut dmx = [0; Self::GOSET_DMX.len()]; + let mut type_code = [0; 1]; + let mut lowest_feed_id = [0; FeedId::LENGTH]; + let mut highest_feed_id = [0; FeedId::LENGTH]; + let mut xor = [0; GOSetXor::LENGTH]; + let mut count = [0; 1]; + + let chunks: [&mut [u8]; 6] = [ + &mut dmx, + &mut type_code, + &mut lowest_feed_id, + &mut highest_feed_id, + &mut xor, + &mut count, + ]; + + let mut offset: usize = 0; + + for chunk in chunks { + let len = chunk.len(); + chunk.copy_from_slice(&bytes[offset..offset + len]); + offset += len; + } + + if !dmx.eq(&Self::GOSET_DMX) { + return Err(GOSetClaimError::InvalidDMX); + } + + if !type_code.eq(&[b'c']) { + return Err(GOSetClaimError::InvalidTypeCode); + } + + Ok(GOSetClaim { + lowest_feed_id: FeedId(lowest_feed_id), + highest_feed_id: FeedId(highest_feed_id), + xor: GOSetXor(xor), + count: count[0], + }) } - pub fn decode(bytes: &[u8]) -> Result { - todo!() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::go_set::GOSet; + + #[test] + fn claim_round_trip() { + let feed_a = FeedId([255; 32]); + let feed_b = FeedId([1; 32]); + let feed_c = FeedId([2; 32]); + let go_set = GOSet::new(&[feed_a, feed_b, feed_c]).unwrap(); + + let claim = go_set.create_claim().unwrap(); + let encoded_claim = claim.encode(); + println!("encoded claim:\n {:?}", claim); + + let result = GOSetClaim::decode(&encoded_claim).unwrap(); + println!("decoded claim:\n {:?}", result); + + assert_eq!(&result.lowest_feed_id, go_set.lowest_feed_id().unwrap()); + assert_eq!(&result.highest_feed_id, go_set.highest_feed_id().unwrap()); + assert_eq!(result.xor, go_set.xor()); + assert_eq!(result.count, go_set.count()); + } + + #[test] + fn claim_wrong_dmx() { + let feed_a = FeedId([255; 32]); + let go_set = GOSet::new(&[feed_a]).unwrap(); + + let claim = go_set.create_claim().unwrap(); + let mut encoded_claim = claim.encode(); + let wrong_dmx = [6; 7]; + encoded_claim[0..7].copy_from_slice(&wrong_dmx); + println!("claim with incorrect dmx:\n {:?}", claim); + + match GOSetClaim::decode(&encoded_claim) { + Err(GOSetClaimError::InvalidDMX) => {} // passed + _ => panic!("Expected InvalidDMX error"), + } + } + + #[test] + fn claim_wrong_type_code() { + let feed_a = FeedId([255; 32]); + let go_set = GOSet::new(&[feed_a]).unwrap(); + + let claim = go_set.create_claim().unwrap(); + let mut encoded_claim = claim.encode(); + let wrong_type_code = [b'W']; + encoded_claim[7..8].copy_from_slice(&wrong_type_code); + println!("claim with incorrect type_code:\n {:?}", claim); + + match GOSetClaim::decode(&encoded_claim) { + Err(GOSetClaimError::InvalidTypeCode) => {} // passed + _ => panic!("Expected InvalidTypeCode error"), + } } } diff --git a/src/go_set/go_set_xor.rs b/src/go_set/go_set_xor.rs index e994e68..5fc2aaa 100644 --- a/src/go_set/go_set_xor.rs +++ b/src/go_set/go_set_xor.rs @@ -2,6 +2,8 @@ pub struct GOSetXor(pub [u8; 32]); impl GOSetXor { + pub const LENGTH: usize = 32; + pub fn encode(&self) -> [u8; 32] { self.0.clone() } diff --git a/src/go_set/mod.rs b/src/go_set/mod.rs index 1603c80..18105d9 100644 --- a/src/go_set/mod.rs +++ b/src/go_set/mod.rs @@ -6,49 +6,115 @@ use crate::feed_id::FeedId; pub use go_set_claim::GOSetClaim; pub use go_set_xor::GOSetXor; +#[derive(Debug)] +pub enum Error { + TooManyFeedIds, +} + pub struct GOSet { feed_ids: Vec, } impl GOSet { - pub fn new(feed_ids: &[FeedId]) -> Self { + // PG: I made this return a result because it's better to ensure the GoSet is valid at + // construction time than blow up later on. If you've got a GoSet instance it's valid. + pub fn new(feed_ids: &[FeedId]) -> Result { + if feed_ids.len() > u8::MAX as usize { + return Err(Error::TooManyFeedIds); + } let mut new_feed_ids = Vec::from(feed_ids); // TODO: discuss from() magic with Piet new_feed_ids.sort(); - Self { + Ok(Self { feed_ids: new_feed_ids, - } + }) } + pub fn count(&self) -> u8 { - todo!() + // NOTE: u8 is the max currently supported by the spec + self.feed_ids.len().try_into().unwrap() } + pub fn xor(&self) -> GOSetXor { let mut xor: [u8; 32] = [0; 32]; for feed_id in self.feed_ids.iter() { for i in 0..32 { - xor[i] = xor[i] ^ feed_id.0[i]; + xor[i] ^= feed_id.0[i]; } } GOSetXor(xor) } + pub fn highest_feed_id(&self) -> Option<&FeedId> { self.feed_ids.last() } + pub fn lowest_feed_id(&self) -> Option<&FeedId> { self.feed_ids.first() } + + pub fn create_claim(&self) -> Option { + match ( + self.lowest_feed_id().cloned(), + self.highest_feed_id().cloned(), + ) { + (Some(lowest_feed_id), Some(highest_feed_id)) => { + let xor = self.xor(); + let count = self.count(); + + Some(GOSetClaim { + highest_feed_id, + lowest_feed_id, + xor, + count, + }) + } + _ => None, + } + } +} + +impl PartialEq for GOSet { + fn eq(&self, other: &GOSetClaim) -> bool { + // TODO: If we change the GoSetClaim feeds to be options then we'll need to update the + // comparison here. + self.xor() == other.xor + && self.highest_feed_id() == Some(&other.highest_feed_id) + && self.lowest_feed_id() == Some(&other.lowest_feed_id) + && self.count() == other.count + } +} + +impl PartialEq for GOSetClaim { + fn eq(&self, other: &GOSet) -> bool { + // TODO: If we change the GoSetClaim feeds to be options then we'll need to update the + // comparison here. + self.xor == other.xor() + && Some(&self.highest_feed_id) == other.highest_feed_id() + && Some(&self.lowest_feed_id) == other.lowest_feed_id() + && self.count == other.count() + } } #[cfg(test)] mod tests { use super::*; + #[test] + fn count() { + let feed_a = FeedId([255; 32]); + let feed_b = FeedId([1; 32]); + + let go_set = GOSet::new(&[feed_a, feed_b]).unwrap(); + assert_eq!(go_set.count(), 2) + } + #[test] fn xor() { let feed_a = FeedId([255; 32]); let feed_b = FeedId([1; 32]); - let go_set = GOSet::new(&[feed_a, feed_b]); + let go_set = GOSet::new(&[feed_a, feed_b]).unwrap(); assert_eq!(go_set.xor().encode(), [254; 32]); } @@ -58,7 +124,7 @@ mod tests { let feed_b = FeedId([2; 32]); let feed_c = FeedId([3; 32]); - let go_set = GOSet::new(&[feed_b, feed_a, feed_c]); + let go_set = GOSet::new(&[feed_b, feed_a, feed_c]).unwrap(); assert_eq!(go_set.highest_feed_id().unwrap(), &feed_c); } @@ -69,7 +135,7 @@ mod tests { let feed_b = FeedId([2; 32]); let feed_c = FeedId([3; 32]); - let go_set = GOSet::new(&[feed_b, feed_a, feed_c]); + let go_set = GOSet::new(&[feed_b, feed_a, feed_c]).unwrap(); assert_eq!(go_set.lowest_feed_id().unwrap(), &feed_a); } diff --git a/src/lib.rs b/src/lib.rs index 6fec252..0840f58 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,19 +1,20 @@ pub mod feed_id; pub mod go_set; -use go_set::GOSetClaim; +// use go_set::GOSetClaim; +#[derive(Debug)] pub enum Error {} -enum WirePacket { - // Dunno about this. Might be bytes not sure - Replication(GOSetClaim), - Log(), -} +// enum WirePacket { +// // Dunno about this. Might be bytes not sure +// Replication(GOSetClaim), +// Log(), +// } #[cfg(test)] mod tests { - use super::*; + // use super::*; use bipf_rs::bipf::{Bipf, decode}; #[test]