tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

lib.rs (14646B)


      1 #![forbid(unsafe_code)]
      2 
      3 use core::fmt;
      4 use pocket_types::OwnedEvent as PocketOwnedEvent;
      5 use tangle_crypto::RelaySigner;
      6 use tangle_groups::{
      7     CanonicalRelayUrl, GroupGeneratedEventBuilder, GroupLimitsConfig, GroupOutboxPayload,
      8     GroupPolicyConfig, GroupRuntimeConfig, GroupRuntimeSettingsConfig, KIND_GROUP_CREATE_GROUP,
      9     KIND_GROUP_DELETE_EVENT, KIND_GROUP_DELETE_GROUP, KIND_GROUP_EDIT_METADATA,
     10     KIND_GROUP_JOIN_REQUEST, KIND_GROUP_LEAVE_REQUEST, KIND_GROUP_PUT_USER, KIND_GROUP_REMOVE_USER,
     11     RelaySecret,
     12 };
     13 use tangle_protocol::{
     14     Event, EventId, Kind, PublicKeyHex, Tag, UnixTimestamp, UnsignedEvent, event_to_value,
     15 };
     16 
     17 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     18 pub enum FixtureKey {
     19     Relay,
     20     Owner,
     21     Admin,
     22     Member,
     23     Outsider,
     24 }
     25 
     26 impl FixtureKey {
     27     pub fn public_key(self) -> PublicKeyHex {
     28         self.signer().public_key().clone()
     29     }
     30 
     31     fn signer(self) -> RelaySigner {
     32         let secret_byte = match self {
     33             Self::Relay => 9_u8,
     34             Self::Owner => 10_u8,
     35             Self::Admin => 11_u8,
     36             Self::Member => 12_u8,
     37             Self::Outsider => 13_u8,
     38         };
     39         RelaySigner::from_secret_hex(&lower_hex(&[secret_byte; 32])).expect("fixture signing key")
     40     }
     41 }
     42 
     43 impl fmt::Display for FixtureKey {
     44     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     45         formatter.write_str(match self {
     46             Self::Relay => "relay",
     47             Self::Owner => "owner",
     48             Self::Admin => "admin",
     49             Self::Member => "member",
     50             Self::Outsider => "outsider",
     51         })
     52     }
     53 }
     54 
     55 pub fn build_fixture_event_from_parts(
     56     fixture_key: FixtureKey,
     57     created_at: u64,
     58     kind: u64,
     59     tags: Vec<Vec<String>>,
     60     content: &str,
     61 ) -> Result<Event, String> {
     62     let unsigned = UnsignedEvent::new(
     63         fixture_key.public_key(),
     64         UnixTimestamp::new(created_at),
     65         Kind::new(kind)?,
     66         tags.into_iter()
     67             .map(Tag::new)
     68             .collect::<Result<Vec<_>, _>>()?,
     69         content,
     70     );
     71     sign_unsigned_event(fixture_key, unsigned)
     72 }
     73 
     74 pub const TANGLE_V2_RELAY_URL: &str = "wss://relay.radroots.test";
     75 pub const TANGLE_V2_RELAY_SECRET_HEX: &str =
     76     "7777777777777777777777777777777777777777777777777777777777777777";
     77 
     78 pub fn tangle_v2_group_config(
     79     owner: FixtureKey,
     80     admins: &[FixtureKey],
     81 ) -> Result<GroupRuntimeConfig, String> {
     82     GroupRuntimeConfig::new(
     83         true,
     84         Some(CanonicalRelayUrl::new(TANGLE_V2_RELAY_URL).map_err(|error| error.to_string())?),
     85         Some(RelaySecret::from_hex(TANGLE_V2_RELAY_SECRET_HEX).map_err(|error| error.to_string())?),
     86         vec![owner.public_key()],
     87         admins.iter().map(|admin| admin.public_key()).collect(),
     88         GroupRuntimeSettingsConfig::new(GroupPolicyConfig::strict(), GroupLimitsConfig::default())
     89             .map_err(|error| error.to_string())?,
     90     )
     91     .map_err(|error| error.to_string())
     92 }
     93 
     94 pub fn tangle_v2_relay_signer() -> Result<RelaySigner, String> {
     95     RelaySigner::from_secret_hex(TANGLE_V2_RELAY_SECRET_HEX).map_err(|error| error.to_string())
     96 }
     97 
     98 pub fn tangle_v2_generated_pocket_event(
     99     payload: &GroupOutboxPayload,
    100 ) -> Result<PocketOwnedEvent, String> {
    101     GroupGeneratedEventBuilder::new(tangle_v2_relay_signer()?)
    102         .sign_payload_pocket(payload)
    103         .map_err(|error| error.to_string())
    104 }
    105 
    106 pub fn tangle_v2_event(
    107     fixture_key: FixtureKey,
    108     created_at: u64,
    109     kind: u64,
    110     tags: Vec<Tag>,
    111     content: &str,
    112 ) -> Result<Event, String> {
    113     let unsigned = UnsignedEvent::new(
    114         fixture_key.public_key(),
    115         UnixTimestamp::new(created_at),
    116         Kind::new(kind)?,
    117         tags,
    118         content,
    119     );
    120     sign_unsigned_event(fixture_key, unsigned)
    121 }
    122 
    123 pub fn tangle_v2_auth_event(
    124     fixture_key: FixtureKey,
    125     challenge: &str,
    126     created_at: u64,
    127 ) -> Result<Event, String> {
    128     tangle_v2_event(
    129         fixture_key,
    130         created_at,
    131         22_242,
    132         vec![
    133             tangle_v2_tag("relay", &[TANGLE_V2_RELAY_URL])?,
    134             tangle_v2_tag("challenge", &[challenge])?,
    135         ],
    136         "",
    137     )
    138 }
    139 
    140 pub fn tangle_v2_group_create_event(
    141     fixture_key: FixtureKey,
    142     group_id: &str,
    143     created_at: u64,
    144     flags: &[&str],
    145 ) -> Result<Event, String> {
    146     let mut tags = vec![
    147         tangle_v2_group_tag(group_id)?,
    148         tangle_v2_tag("name", &[group_id])?,
    149     ];
    150     for flag in flags {
    151         tags.push(tangle_v2_tag(flag, &[])?);
    152     }
    153     tangle_v2_event(
    154         fixture_key,
    155         created_at,
    156         KIND_GROUP_CREATE_GROUP.into(),
    157         tags,
    158         "",
    159     )
    160 }
    161 
    162 pub fn tangle_v2_group_metadata_event(
    163     fixture_key: FixtureKey,
    164     group_id: &str,
    165     name: &str,
    166     created_at: u64,
    167     flags: &[&str],
    168 ) -> Result<Event, String> {
    169     let mut tags = vec![
    170         tangle_v2_group_tag(group_id)?,
    171         tangle_v2_tag("name", &[name])?,
    172     ];
    173     for flag in flags {
    174         tags.push(tangle_v2_tag(flag, &[])?);
    175     }
    176     tangle_v2_event(
    177         fixture_key,
    178         created_at,
    179         KIND_GROUP_EDIT_METADATA.into(),
    180         tags,
    181         "",
    182     )
    183 }
    184 
    185 pub fn tangle_v2_put_user_event(
    186     fixture_key: FixtureKey,
    187     group_id: &str,
    188     target: FixtureKey,
    189     created_at: u64,
    190 ) -> Result<Event, String> {
    191     tangle_v2_event(
    192         fixture_key,
    193         created_at,
    194         KIND_GROUP_PUT_USER.into(),
    195         vec![
    196             tangle_v2_group_tag(group_id)?,
    197             tangle_v2_pubkey_tag(target)?,
    198         ],
    199         "",
    200     )
    201 }
    202 
    203 pub fn tangle_v2_remove_user_event(
    204     fixture_key: FixtureKey,
    205     group_id: &str,
    206     target: FixtureKey,
    207     created_at: u64,
    208 ) -> Result<Event, String> {
    209     tangle_v2_event(
    210         fixture_key,
    211         created_at,
    212         KIND_GROUP_REMOVE_USER.into(),
    213         vec![
    214             tangle_v2_group_tag(group_id)?,
    215             tangle_v2_pubkey_tag(target)?,
    216         ],
    217         "",
    218     )
    219 }
    220 
    221 pub fn tangle_v2_join_event(
    222     fixture_key: FixtureKey,
    223     group_id: &str,
    224     created_at: u64,
    225 ) -> Result<Event, String> {
    226     tangle_v2_group_event(
    227         fixture_key,
    228         group_id,
    229         created_at,
    230         KIND_GROUP_JOIN_REQUEST.into(),
    231         "",
    232     )
    233 }
    234 
    235 pub fn tangle_v2_leave_event(
    236     fixture_key: FixtureKey,
    237     group_id: &str,
    238     created_at: u64,
    239 ) -> Result<Event, String> {
    240     tangle_v2_group_event(
    241         fixture_key,
    242         group_id,
    243         created_at,
    244         KIND_GROUP_LEAVE_REQUEST.into(),
    245         "",
    246     )
    247 }
    248 
    249 pub fn tangle_v2_delete_event_event(
    250     fixture_key: FixtureKey,
    251     group_id: &str,
    252     target: &Event,
    253     created_at: u64,
    254 ) -> Result<Event, String> {
    255     tangle_v2_event(
    256         fixture_key,
    257         created_at,
    258         KIND_GROUP_DELETE_EVENT.into(),
    259         vec![
    260             tangle_v2_group_tag(group_id)?,
    261             tangle_v2_event_tag(target.id())?,
    262         ],
    263         "",
    264     )
    265 }
    266 
    267 pub fn tangle_v2_delete_group_event(
    268     fixture_key: FixtureKey,
    269     group_id: &str,
    270     created_at: u64,
    271 ) -> Result<Event, String> {
    272     tangle_v2_group_event(
    273         fixture_key,
    274         group_id,
    275         created_at,
    276         KIND_GROUP_DELETE_GROUP.into(),
    277         "",
    278     )
    279 }
    280 
    281 pub fn tangle_v2_group_event(
    282     fixture_key: FixtureKey,
    283     group_id: &str,
    284     created_at: u64,
    285     kind: u64,
    286     content: &str,
    287 ) -> Result<Event, String> {
    288     tangle_v2_event(
    289         fixture_key,
    290         created_at,
    291         kind,
    292         vec![tangle_v2_group_tag(group_id)?],
    293         content,
    294     )
    295 }
    296 
    297 pub fn tangle_v2_group_tag(group_id: &str) -> Result<Tag, String> {
    298     tangle_v2_tag("h", &[group_id])
    299 }
    300 
    301 pub fn tangle_v2_address_group_tag(group_id: &str) -> Result<Tag, String> {
    302     tangle_v2_tag("d", &[group_id])
    303 }
    304 
    305 pub fn tangle_v2_pubkey_tag(fixture_key: FixtureKey) -> Result<Tag, String> {
    306     let pubkey = fixture_key.public_key();
    307     tangle_v2_tag("p", &[pubkey.as_str()])
    308 }
    309 
    310 pub fn tangle_v2_event_tag(event_id: &EventId) -> Result<Tag, String> {
    311     tangle_v2_tag("e", &[event_id.as_str()])
    312 }
    313 
    314 pub fn tangle_v2_tag(name: &str, values: &[&str]) -> Result<Tag, String> {
    315     let mut parts = Vec::with_capacity(values.len() + 1);
    316     parts.push(name.to_owned());
    317     parts.extend(values.iter().map(|value| (*value).to_owned()));
    318     Tag::new(parts)
    319 }
    320 
    321 pub fn fixture_event_json(event: &Event) -> serde_json::Value {
    322     event_to_value(event)
    323 }
    324 
    325 fn sign_unsigned_event(fixture_key: FixtureKey, unsigned: UnsignedEvent) -> Result<Event, String> {
    326     Ok(fixture_key.signer().sign_unsigned_event(unsigned))
    327 }
    328 
    329 #[cfg(test)]
    330 fn fixed_hex_bytes(value: &str, expected: usize, scalar: &str) -> Result<Vec<u8>, String> {
    331     if value.len() != expected * 2 {
    332         return Err(format!(
    333             "{scalar} must decode to {expected} bytes, got {} hex characters",
    334             value.len()
    335         ));
    336     }
    337     let mut output = Vec::with_capacity(expected);
    338     for chunk in value.as_bytes().chunks_exact(2) {
    339         output.push((hex_value(chunk[0], scalar)? << 4) | hex_value(chunk[1], scalar)?);
    340     }
    341     Ok(output)
    342 }
    343 
    344 #[cfg(test)]
    345 fn hex_value(value: u8, scalar: &str) -> Result<u8, String> {
    346     match value {
    347         b'0'..=b'9' => Ok(value - b'0'),
    348         b'a'..=b'f' => Ok(value - b'a' + 10),
    349         _ => Err(format!("{scalar} must be lowercase hex")),
    350     }
    351 }
    352 
    353 fn lower_hex(bytes: &[u8]) -> String {
    354     const HEX: &[u8; 16] = b"0123456789abcdef";
    355     let mut output = String::with_capacity(bytes.len() * 2);
    356     for byte in bytes {
    357         output.push(char::from(HEX[usize::from(byte >> 4)]));
    358         output.push(char::from(HEX[usize::from(byte & 0x0f)]));
    359     }
    360     output
    361 }
    362 
    363 #[cfg(test)]
    364 mod tests {
    365     use super::{
    366         FixtureKey, build_fixture_event_from_parts, fixed_hex_bytes, fixture_event_json,
    367         tangle_v2_auth_event, tangle_v2_generated_pocket_event, tangle_v2_group_config,
    368         tangle_v2_group_create_event, tangle_v2_group_event, tangle_v2_group_metadata_event,
    369         tangle_v2_join_event, tangle_v2_put_user_event,
    370     };
    371     use tangle_crypto::{event_id_matches, verify_event_signature};
    372     use tangle_groups::{GroupOutboxPayload, KIND_GROUP_CREATE_GROUP, KIND_GROUP_METADATA};
    373     use tangle_protocol::UnixTimestamp;
    374 
    375     #[test]
    376     fn fixture_keys_have_stable_synthetic_public_keys() {
    377         assert_eq!(FixtureKey::Relay.to_string(), "relay");
    378         assert_eq!(FixtureKey::Owner.to_string(), "owner");
    379         assert_eq!(FixtureKey::Admin.to_string(), "admin");
    380         assert_eq!(FixtureKey::Member.to_string(), "member");
    381         assert_eq!(FixtureKey::Outsider.to_string(), "outsider");
    382         assert_eq!(
    383             FixtureKey::Owner.public_key().as_str(),
    384             "f76a39d05686e34a4420897e359371836145dd3973e3982568b60f8433adde6e"
    385         );
    386         assert_ne!(
    387             FixtureKey::Owner.public_key(),
    388             FixtureKey::Admin.public_key()
    389         );
    390     }
    391 
    392     #[test]
    393     fn tangle_v2_builders_create_deterministic_signed_events() {
    394         let first =
    395             tangle_v2_group_create_event(FixtureKey::Owner, "Farm", 1_714_124_433, &["private"])
    396                 .expect("first");
    397         let second =
    398             tangle_v2_group_create_event(FixtureKey::Owner, "Farm", 1_714_124_433, &["private"])
    399                 .expect("second");
    400         let auth =
    401             tangle_v2_auth_event(FixtureKey::Owner, "challenge-001", 1_714_124_434).expect("auth");
    402 
    403         assert_eq!(first.id(), second.id());
    404         assert_eq!(verify_event_signature(&first), Ok(()));
    405         assert_eq!(verify_event_signature(&auth), Ok(()));
    406         assert!(event_id_matches(&first));
    407         assert_eq!(first.unsigned().kind().as_u32(), KIND_GROUP_CREATE_GROUP);
    408         assert_eq!(auth.unsigned().kind().as_u32(), 22_242);
    409     }
    410 
    411     #[test]
    412     fn tangle_v2_builders_cover_group_config_and_generated_events() {
    413         let config =
    414             tangle_v2_group_config(FixtureKey::Owner, &[FixtureKey::Admin]).expect("config");
    415         let metadata = tangle_v2_group_metadata_event(
    416             FixtureKey::Owner,
    417             "Farm",
    418             "Market",
    419             1_714_124_435,
    420             &["hidden"],
    421         )
    422         .expect("metadata");
    423         let put =
    424             tangle_v2_put_user_event(FixtureKey::Admin, "Farm", FixtureKey::Member, 1_714_124_436)
    425                 .expect("put");
    426         let join = tangle_v2_join_event(FixtureKey::Member, "Farm", 1_714_124_437).expect("join");
    427         let normal = tangle_v2_group_event(FixtureKey::Member, "Farm", 1_714_124_438, 1, "harvest")
    428             .expect("normal");
    429         let payload = GroupOutboxPayload::new(
    430             KIND_GROUP_METADATA,
    431             UnixTimestamp::new(1_714_124_439),
    432             vec![vec!["d".to_owned(), "Farm".to_owned()]],
    433             "",
    434         );
    435         let generated = tangle_v2_generated_pocket_event(&payload).expect("generated");
    436 
    437         assert!(config.enabled());
    438         assert_eq!(config.owner_pubkeys(), &[FixtureKey::Owner.public_key()]);
    439         assert_eq!(config.admin_pubkeys(), &[FixtureKey::Admin.public_key()]);
    440         assert_eq!(verify_event_signature(&metadata), Ok(()));
    441         assert_eq!(verify_event_signature(&put), Ok(()));
    442         assert_eq!(verify_event_signature(&join), Ok(()));
    443         assert_eq!(verify_event_signature(&normal), Ok(()));
    444         generated.verify().expect("generated signature");
    445         assert_eq!(u32::from(generated.kind().as_u16()), KIND_GROUP_METADATA);
    446     }
    447 
    448     #[test]
    449     fn fixture_json_uses_signed_event_shape() {
    450         let event =
    451             build_fixture_event_from_parts(FixtureKey::Member, 1_714_124_440, 1, Vec::new(), "hi")
    452                 .expect("event");
    453         let json = fixture_event_json(&event);
    454 
    455         assert_eq!(verify_event_signature(&event), Ok(()));
    456         assert_eq!(json["kind"], 1);
    457         assert_eq!(json["content"], "hi");
    458     }
    459 
    460     #[test]
    461     fn fixture_builder_rejects_invalid_parts() {
    462         let bad_tag = build_fixture_event_from_parts(FixtureKey::Owner, 1, 1, vec![Vec::new()], "")
    463             .expect_err("tag");
    464         let bad_kind =
    465             build_fixture_event_from_parts(FixtureKey::Owner, 1, 4_294_967_296, Vec::new(), "")
    466                 .expect_err("kind");
    467 
    468         assert_eq!(bad_tag, "tag must not be empty");
    469         assert_eq!(bad_kind, "kind must fit in u32, got 4294967296");
    470     }
    471 
    472     #[test]
    473     fn fixed_hex_bytes_validates_expected_width_and_lowercase() {
    474         assert_eq!(fixed_hex_bytes("0a", 1, "value"), Ok(vec![10]));
    475         assert_eq!(
    476             fixed_hex_bytes("0a", 2, "value").expect_err("width"),
    477             "value must decode to 2 bytes, got 2 hex characters"
    478         );
    479         assert_eq!(
    480             fixed_hex_bytes("0G", 1, "value").expect_err("case"),
    481             "value must be lowercase hex"
    482         );
    483     }
    484 }