heddle-thread-api 0.25.4

Native Thread clients and durable peer replication over Iroh
Documentation
#![cfg(feature = "replication")]

use crypto::{Ed25519Signer, Signer};
use heddle_object_model::object::{StateId, thread_replication::ThreadGenesis};
use heddle_thread_api::{creation::ThreadCreation, replication::opening::verify_genesis};

fn genesis(signer: &Ed25519Signer) -> ThreadGenesis {
    ThreadGenesis {
        version: 1,
        spool: "01980000-0000-7000-8000-000000000001".into(),
        parent: None,
        base: StateId::from_bytes([17; 32]),
        name: "independent work".into(),
        intent: "retain identity while publishing".into(),
        creator: signer.public_key().try_into().expect("public key"),
        owner: heddle_object_model::object::thread_replication::GenesisOwner::LocalKey(
            signer.public_key().try_into().expect("public key"),
        ),
        nonce: vec![23; 16],
    }
}

#[test]
fn creation_and_later_publication_retain_the_original_record_and_thread_id() {
    let signer = Ed25519Signer::from_seed(&[11; 32]).expect("test creator");
    let genesis = genesis(&signer);
    let creation = ThreadCreation::sign("01980000-0000-7000-8000-000000000002", &genesis, &signer)
        .expect("prepare signed creation without a network call");
    assert_eq!(
        creation.reference().id.as_ref().expect("Thread ID").value,
        genesis.id().expect("native ID").as_bytes()
    );
    let original = creation
        .request()
        .thread_genesis
        .clone()
        .expect("signed genesis");
    assert_eq!(
        verify_genesis(&original, creation.reference()).expect("same replication verifier"),
        genesis
    );
    let retry =
        ThreadCreation::from_signed("01980000-0000-7000-8000-000000000003", original.clone())
            .expect("a later relay retains the creator's record");
    assert_eq!(retry.reference(), creation.reference());
    assert_eq!(retry.request().thread_genesis, Some(original));
    let mut independent = genesis;
    independent.nonce[0] += 1;
    let other = ThreadCreation::sign(
        "01980000-0000-7000-8000-000000000004",
        &independent,
        &signer,
    )
    .expect("separate attempt");
    assert_ne!(other.reference(), creation.reference());
}

#[test]
fn creation_rejects_a_changed_creator_record_and_invalid_operation_identity() {
    let signer = Ed25519Signer::from_seed(&[11; 32]).expect("creator");
    let genesis = genesis(&signer);
    assert!(ThreadCreation::sign("not-an-operation-uuid", &genesis, &signer).is_err());
    let other = Ed25519Signer::from_seed(&[12; 32]).expect("different key");
    assert!(
        ThreadCreation::sign("01980000-0000-7000-8000-000000000002", &genesis, &other).is_err()
    );
    let creation = ThreadCreation::sign("01980000-0000-7000-8000-000000000002", &genesis, &signer)
        .expect("signed genesis");
    let mut changed = creation.request().thread_genesis.clone().expect("record");
    let mut claimed = genesis;
    claimed.name = "different initial name".into();
    changed.canonical_record = claimed.encode().expect("changed canonical record");
    assert!(ThreadCreation::from_signed("01980000-0000-7000-8000-000000000002", changed).is_err());
}

#[test]
fn creation_rejects_mutable_or_noncanonical_spool_identity() {
    let signer = Ed25519Signer::from_seed(&[11; 32]).expect("creator");
    for spool in [
        "org/name",
        "00000000-0000-0000-0000-000000000000",
        "01980000-0000-7000-8000-ABCDEFABCDEF",
    ] {
        let mut genesis = genesis(&signer);
        genesis.spool = spool.into();
        assert!(
            ThreadCreation::sign("01980000-0000-7000-8000-000000000002", &genesis, &signer)
                .is_err(),
            "a private Thread must be publishable without changing its identity: {spool}"
        );
    }
}

#[test]
fn signed_genesis_matches_the_cross_language_vector() {
    let signer = Ed25519Signer::from_seed(&[11; 32]).expect("public test seed");
    let genesis = genesis(&signer);
    let creation = ThreadCreation::sign("01980000-0000-7000-8000-000000000002", &genesis, &signer)
        .expect("creation");
    let record = creation.request().thread_genesis.as_ref().expect("record");
    let values: std::collections::BTreeMap<_, _> = include_str!("fixtures/thread-genesis-v1.txt")
        .lines()
        .map(|line| line.split_once('=').expect("vector entry"))
        .collect();
    assert_eq!(hex::encode(&record.canonical_record), values["canonical"]);
    assert_eq!(
        hex::encode(&record.signatures[0].signature),
        values["signature"]
    );
    assert_eq!(hex::encode(genesis.creator), values["key"]);
    assert_eq!(genesis.id().expect("ID").to_hex(), values["id"]);
}