tangle


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

lib.rs (15476B)


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