How Xudanu answers "who wrote this?" at the character level — Ed25519 span signing, the tamper-evident attribution log, derivation chains, attestation reports, and federation cluster consensus with W3C-PROV export.
In any collaborative document system, the question "who wrote this?" is fundamental. In a wiki, you trust the server. In Git, you trust the commit signature. In Google Docs, you trust... whoever has the account.
Xudanu makes a stronger guarantee: every run of characters is cryptographically signed by its author's Ed25519 key. Not just "user X edited this page" but "user X's key signed this exact sequence of characters at this timestamp on this server." The signature covers the content fingerprint, so changing even one character invalidates the signature.
This matters for:
Three layers: signing (purple) creates the cryptographic proof, audit (amber) makes it tamper-evident, federation (cyan) extends trust across servers.
Xudanu's provenance system uses three cryptographic building blocks:
| Primitive | Algorithm | Used for |
|---|---|---|
| Signing | Ed25519 | Author signs content fingerprint; 64-byte signature, 32-byte key |
| Content hash | BLAKE3 | Fingerprint of each RangeElement; 32-byte hash |
| Chain hash | SHA-256 | Tamper-evident log chaining; each entry links to the previous |
The fundamental unit of attribution is the Provenance struct:
pub struct Provenance {
pub author_public_key: [u8; 32], // WHO: Ed25519 verifying key
pub signature: [u8; 64], // PROOF: Ed25519 signature
pub timestamp: u64, // WHEN: Unix epoch seconds
pub server_id: [u8; 32], // WHERE: which server witnessed the signing
}
This is intentionally minimal. It contains everything needed to independently verify
attribution without trusting the server, and nothing more. The author_public_key
identifies the signer; the signature proves they signed something; the
timestamp and server_id are part of the signed payload so
they can't be forged after the fact.
Before signing, the content is reduced to a 32-byte BLAKE3 fingerprint. Each
RangeElement in the O-tree has a content_fingerprint() method
that hashes its text content. A span fingerprint combines multiple
element fingerprints:
fn compute_span_fingerprint(fingerprints: &[[u8; 32]]) -> [u8; 32] {
let mut hasher = Hasher::new();
hasher.update(PROVENANCE_DOMAIN); // domain separation
for fp in fingerprints {
hasher.update(fp);
}
hasher.finalize().into()
}
The sign_span function is the core signing operation:
pub fn sign_span(
signing_key: &SigningKey, // author's Ed25519 private key
element_fingerprints: &[[u8; 32]], // one per character/element in the span
timestamp: u64,
server_id: &[u8; 32],
) -> Provenance {
// Step 1: Hash all element fingerprints into one span fingerprint
let span_fp = compute_span_fingerprint(element_fingerprints);
// Step 2: Build the signing payload (domain || span_fp || pub_key || ts || server)
let payload = compute_signing_payload(
&span_fp,
&signing_key.verifying_key().to_bytes(),
timestamp,
server_id,
);
// Step 3: Ed25519 sign the payload
let signature = sign_bytes(signing_key, &payload);
Provenance {
author_public_key: signing_key.verifying_key().to_bytes(),
signature: signature.to_bytes(),
timestamp,
server_id: *server_id,
}
}
Signing pipeline: each character is BLAKE3-fingerprinted, combined into a span fingerprint, mixed with metadata, and Ed25519-signed.
Verification reconstructs the payload and checks the Ed25519 signature:
pub fn verify_span_provenance(
provenance: &Provenance,
element_fingerprints: &[[u8; 32]],
) -> bool {
let span_fp = compute_span_fingerprint(element_fingerprints);
verify_span_provenance_with_span_fp(provenance, &span_fp)
}
pub fn verify_span_provenance_with_span_fp(
provenance: &Provenance,
span_fingerprint: &[u8; 32],
) -> bool {
let verifying_key = VerifyingKey::from_bytes(&provenance.author_public_key);
let payload = compute_signing_payload(
span_fingerprint,
&provenance.author_public_key,
provenance.timestamp,
&provenance.server_id,
);
let signature = Signature::from_bytes(&provenance.signature);
verify_signature(&verifying_key, &payload, &signature).is_ok()
}
The test suite verifies that the following attacks all fail:
Every signing function begins with a domain separation tag:
const PROVENANCE_DOMAIN: &[u8] = b"xudanu/v1/provenance"; const ELEMENT_PROVENANCE_DOMAIN: &[u8] = b"xudanu/v1/element-provenance"; const HISTORICAL_ATTESTATION_DOMAIN: &[u8] = b"xudanu/v1/historical-attestation"; const FEDERATION_PROVENANCE_DOMAIN: &[u8] = b"xudanu/v1/federation-provenance";
SpanProvenance attaches position information to a Provenance,
saying "this signature covers characters from position start to
end":
pub struct SpanProvenance {
pub start: i64, // first position (inclusive)
pub end: i64, // last position + 1 (exclusive)
pub provenance: Provenance // who signed it
}
An Edition stores a Vec<SpanProvenance> — a list
of non-overlapping runs, each signed by its author. When you query attribution, you
get back the spans that cover the character range you asked about.
At the element (per-character) level, Xudanu tracks richer metadata:
pub struct ElementProvenance {
pub author_public_key: [u8; 32],
pub author_display_name: String,
pub author_club_id: BeId,
pub timestamp: u64,
pub author_type: AuthorType,
pub llm_model: Option<String>, // if LLM-generated
pub historical_author_id: Option<BeId>, // if imported historical text
pub source_work_id: Option<BeId>, // if transcluded
pub transcluded_by: Option<TransclusionInfo>,
pub derived_by: Option<DerivationInfo>,
}
This metadata is stored per-element in the O-tree and used during materialization to
build span-level provenance. The transcluded_by and derived_by
fields trace content back through transclusion and merge operations.
Xudanu distinguishes three kinds of author, each with different trust semantics:
| Type | Signed by | Use case | Display |
|---|---|---|---|
Human | Author's Ed25519 key | Normal collaborative editing | Name + colored avatar |
Llm | Server's key (on behalf of LLM) | AI-assisted narration, auto-title, writing feedback | Name + "AI" badge + model name |
Historical | Server's key (historical attestation) | Imported public-domain texts (Project Gutenberg, etc.) | Author name + "historical" badge |
historical_attestation with a different domain tag that includes the
historical author's ID. The build_edition_provenance function in
otree_crdt.rs groups adjacent elements by author and type, signing each
run as a single span.
When a CRDT document is materialized into an Edition for storage or query, the server walks the O-tree and builds span provenance:
// otree_crdt.rs: build_edition_provenance (simplified)
fn build_edition_provenance(edition, fallback_key, server_id, timestamp, author_keys) {
let entries = edition.all_entries();
// Check if elements have per-element provenance
let has_element_prov = entries.iter().any(|(_, c)| c.provenance.is_some());
if !has_element_prov {
// No per-element provenance: sign the whole edition as one span
return vec![SpanProvenance {
start: first_pos, end: last_pos + 1,
provenance: sign_span(fallback_key, all_fingerprints, timestamp, server_id),
}];
}
// Group adjacent elements by (author, type) and sign each group
let mut spans = Vec::new();
while i < entries.len() {
let author_key = carrier.provenance.author_club_id;
let author_type = carrier.provenance.author_type;
// Collect adjacent elements with the same author
while j < entries.len()
&& same_author(entries[j], author_key, author_type)
{
fingerprints.push(entries[j].content_fingerprint());
j += 1;
}
// Sign this run
let signing_key = match author_type {
AuthorType::Llm => fallback_key, // server signs for LLM
_ => author_keys.get(&author_key) // author signs
.unwrap_or(fallback_key),
};
spans.push(SpanProvenance {
start: entries[i].pos, end: entries[j-1].pos + 1,
provenance: sign_span(signing_key, &fingerprints, timestamp, server_id),
});
i = j;
}
spans
}
Materialization groups adjacent elements by author and signs each run as a separate SpanProvenance. Different authors get different signatures.
When content is transcluded (copied with attribution) from one document to another,
Xudanu preserves the chain of custody. The apply_transclusion_attribution
method:
ElementProvenance for the matched elementsTransclusionInfo noting who performed the transclusionpub struct TransclusionInfo {
pub club_id: BeId, // who performed the transclusion
pub display_name: String,
pub public_key: [u8; 32],
pub timestamp: u64,
}
This means if Alice writes a paragraph in document A, and Bob transcludes it into
document B, the elements in B carry both Alice's original authorship (via
author_public_key) and Bob's transclusion action (via
transcluded_by). The derivation chain is preserved.
PendingAttribution and re-applied when the origin work is next revised.
This handles the case where transclusion links are created before the origin content
is materialized.
When importing public-domain texts (e.g., from Project Gutenberg), the original author is dead and cannot sign. Instead, the server signs on their behalf using a special historical attestation domain:
pub fn sign_historical_attestation(
server_signing_key: &SigningKey,
element_fingerprints: &[[u8; 32]],
historical_author_id: BeId,
timestamp: u64,
server_id: &[u8; 32],
) -> Provenance {
// Uses HISTORICAL_ATTESTATION_DOMAIN, not PROVENANCE_DOMAIN
let payload = blake3(
"xudanu/v1/historical-attestation"
|| span_fp
|| server_pub_key
|| historical_author_id // <-- which historical author
|| timestamp
|| server_id
);
let signature = ed25519_sign(server_signing_key, payload);
Provenance { ... }
}
The verification function checks the same payload. Because it uses a different domain
tag, a historical attestation signature cannot be confused with a direct author
signature. The historical_author_id is part of the signed payload, so
the server is committing to "this content is by author X" — it cannot later
reassign it.
Every time span provenance is stamped on an edition revision, the server also appends
an entry to the attribution log — a tamper-evident, chained,
append-only file stored at data/attribution/attribution.log.
pub struct AttributionEntry {
pub sequence: u64,
pub timestamp: u64,
pub author_pk_hex: String, // who signed
pub span_fp_hex: String, // what they signed (span fingerprint)
pub signature_hex: String, // the Ed25519 signature
pub server_id_hex: String, // which server witnessed it
pub work_id: u64,
pub revision: u64,
}
Each entry is a JSON line. The log uses the same chaining technique as the security log (see Section 16).
Each log line includes a chain= field that is the SHA-256 hash of
the previous line's chain hash concatenated with the current line's content.
This creates a chain where altering any entry invalidates all subsequent entries:
// Append:
let line = format!(
"{{\"seq\":{},\"ts\":{},\"author\":\"{}\",...,\"rev\":{}}}",
entry.sequence, entry.timestamp, entry.author_pk_hex, ..., entry.revision
);
let chain_input = format!("{}{}", self.prev_hash, line);
let chain_hash = sha256_hex(chain_input);
let chained_line = format!("{} chain={}\n", line, chain_hash);
write_to_file(chained_line);
self.prev_hash = chain_hash;
Tampering with Entry 2 changes its chain hash. Entry 3 was computed from the original Entry 2 hash, so the chain breaks at that point and all subsequent entries fail verification.
The verify_attribution_log function walks the entire log from the seed
and checks each chain hash:
pub fn verify_attribution_log(content: &str, seed: &str) -> Result<(usize, String), ChainVerifyError> {
let mut prev_hash = seed;
for line in content.lines() {
let chain_value = extract_chain_hash(line);
let expected = sha256_hex(format!("{}{}", prev_hash, line_without_chain));
if chain_value != expected {
return Err(ChainVerifyError { line_number, expected, found: chain_value, ... });
}
prev_hash = chain_value; // advance the chain
}
Ok((line_count, prev_hash)
}
The test suite verifies three attack scenarios:
"work":42 to "work":99 — detected
For transcluded content, Xudanu can walk the provenance chain —
the sequence of links that brought content to the current document. The
provenance_ancestry method does a breadth-first walk of incoming
transclusion links:
pub fn provenance_ancestry(&self, work_id: BeId) -> Vec<ProvenanceHop> {
let mut chain = Vec::new();
let mut queue = vec![work_id];
while let Some(current) = queue.pop() {
for (link_id, origin, dest) in self.list_links_for_work(current) {
if dest != current { continue; }
// Add embedded provenance hops from the link itself
for hop in link.end_at("LeftEnd").provenance_chain() {
chain.push(hop);
}
// Add this link as a hop
chain.push(ProvenanceHop::new(origin, link_id));
// Continue walking backward
queue.push(origin);
}
}
chain
}
Each ProvenanceHop identifies a (source_work, link) pair. The
enrich_provenance_hops method then resolves these into human-readable
payloads with titles and author names.
The generate_attestation_report method produces a standalone JSON document
that proves the provenance of a work at a point in time:
{
"type": "xudanu-attestation-report",
"version": 1,
"generated_at": 1738246800,
"document": {
"work_id": "04a2",
"title": "Chapter 1: The Beginning",
"revision": 7,
"character_count": 15234,
"content_hash_blake3": "a3b1c2..."
},
"server_identity": {
"server_id": "f4e2...",
"verifying_key_ed25519": "9a8b..."
},
"attribution": {
"span_count": 3,
"spans": [
{
"range": [0, 5000],
"author": "Alice",
"author_type": "human",
"signature_valid": true,
"timestamp": 1738240000,
"provenance_chain": [
{ "source_work_id": "03b1", "source_title": "Draft", "source_author": "Alice" }
]
},
{
"range": [5000, 12000],
"author": "Bob",
"author_type": "human",
"signature_valid": true,
...
},
{
"range": [12000, 15234],
"author": "Project Gutenberg",
"author_type": "historical",
"signature_valid": true,
...
}
]
},
"attribution_log": {
"entry_count": 42,
"chain_valid": true,
"last_sequence": 42,
"has_log": true
}
}
chain_valid field proves the
attribution log hasn't been tampered with since the signatures were recorded.
In a federated cluster, content authored on one server can be replicated to others. To extend provenance trust across server boundaries, Xudanu uses cross-server signatures. When server A vouches for a piece of provenance from server B, A signs a payload derived from B's base provenance:
pub fn sign_cross_server(
signing_key: &SigningKey, // server A's signing key
base_provenance: &Provenance, // the original provenance from server B
server_id: &[u8; 32], // server A's ID
verifying_key: &[u8; 32],
timestamp: u64,
) -> CrossServerSignature {
let payload = compute_federation_payload(base_provenance, server_id, timestamp);
// payload = blake3(FEDERATION_PROVENANCE_DOMAIN || base_pk || base_sig
// || base_ts || base_server || this_server || timestamp)
let signature = sign_bytes(signing_key, &payload);
CrossServerSignature { server_id, verifying_key, signature, timestamp }
}
pub struct CrossServerSignature {
pub server_id: [u8; 32], // which server is vouching
pub verifying_key: [u8; 32], // that server's public key
pub signature: [u8; 64], // their Ed25519 signature
pub timestamp: u64,
}
A FederatedProvenance bundles the base provenance with all cross-server
signatures and the cluster consensus:
pub struct FederatedProvenance {
pub base_provenance: Provenance,
pub cross_server_signatures: Vec<CrossServerSignature>,
pub consensus: ClusterConsensus,
}
Three peer servers each sign a cross-server signature vouching for the base provenance. Together they form a FederatedProvenance with cluster consensus.
ClusterConsensus records how many servers verified the provenance and
whether the threshold was met:
pub struct ClusterConsensus {
pub consensus_type: ConsensusType,
pub verifications: Vec<ServerVerification>,
pub threshold_met: bool,
pub total_servers: u32,
pub approving_servers: u32,
pub timestamp: u64,
}
pub enum ConsensusType {
Unanimous, // all servers must sign
Majority, // > 50%
Supermajority, // ≥ 67%
}
The verify_federation_provenance function checks all cross-server
signatures and then verifies the consensus threshold:
pub fn verify_federation_provenance(federated: &FederatedProvenance, ...) -> bool {
// 1. Verify the base provenance signature
if !verify_base(&federated.base_provenance) { return false; }
// 2. Verify each cross-server signature
for cross_sig in &federated.cross_server_signatures {
if !verify_cross_sig(cross_sig) { return false; }
}
// 3. Check the consensus threshold
match federated.consensus.consensus_type {
ConsensusType::Unanimous => {
federated.cross_server_signatures.len() as u32
== federated.consensus.total_servers
}
ConsensusType::Majority => {
federated.cross_server_signatures.len() as u32
> federated.consensus.total_servers / 2
}
ConsensusType::Supermajority => {
let threshold = (total as f64 * 0.67).ceil() as u32;
federated.cross_server_signatures.len() as u32 >= threshold
}
}
}
Xudanu can export provenance as W3C PROV-JSON — the standard provenance interchange format from the World Wide Web Consortium. This allows interoperability with provenance tools, academic attribution systems, and audit frameworks.
The PROV model maps Xudanu concepts as follows:
| Xudanu Concept | PROV Concept | Example |
|---|---|---|
| Author (club) | prov:Agent (prov:Person or xudanu:LLMAgent) | Alice's club as an agent |
| Server | prov:Agent (xudanu:FederationServer) | Server B as an agent |
| Content revision | prov:Activity | "Alice edited revision 7" |
| Transclusion | prov:wasDerivedFrom (xudanu:Transclusion) | Document B derived from A |
| Merge | prov:wasDerivedFrom (xudanu:Merge) | Revision 8 merged from revisions 6 and 7 |
| Import | prov:wasDerivedFrom (prov:Revision) | Gutenberg import |
| Cross-server signature | prov:Entity (xudanu:CrossServerSignature) | Server A's signature entity |
| Cluster verification | prov:Activity (xudanu:ClusterVerification) | "3 servers verified" |
The FederationProvenanceBundle assembles all these into a PROV-JSON
document with proper prefixes (xudanu: namespace), agents, activities,
associations, and entities. The to_prov_bundle() method generates the
complete document.
Provenance and attribution operations are exposed via the WebSocket protocol:
| Opcode | Operation | Description |
|---|---|---|
0x0D01 | AttributionQuery | Query attribution spans for a work (optionally a character range) |
0x0D02 | AttributionVerify | Verify a signature against a span fingerprint |
0x0D03 | AttributionLogStatus | Check attribution log chain validity and entry count |
0x0D11 | WorkApplySourceAttribution | Apply source import attribution (historical author) |
0x0D12 | WorkApplyTransclusionAttribution | Apply transclusion attribution for a link |
0x1E04 | AttributionQueryResolved | Query with resolved author names and types |
| (via dispatch) | ProvenanceAncestry | Walk the transclusion derivation chain |
| (via dispatch) | GenerateAttestationReport | Generate the full JSON attestation report |
| File | Path | Key Types |
|---|---|---|
| Signing & verification | ||
provenance.rs | src/edition/provenance.rs | Provenance, SpanProvenance, ElementProvenance, AuthorType, DerivationInfo, TransclusionInfo, sign_span, verify_span_provenance, sign_historical_attestation, sign_cross_server |
| Audit log | ||
attribution_log.rs | src/server/transport/attribution_log.rs | AttributionLog, AttributionEntry, FileAttributionLog, verify_attribution_log, ChainVerifyError |
| Federation provenance | ||
federation.rs | src/server/federation.rs | CrossServerSignature, ServerVerification, ClusterConsensus, ConsensusType, FederatedProvenance, export_cross_server_signatures_prov, create_federation_verification_bundle |
provenance.rs | src/edition/provenance.rs | FederationMetadata, FederationServerAgent, FederationAttestation, ClusterVerificationActivity, FederationProvenanceBundle, ProvEntity/Activity/Association/Agent/Bundle/Value/JsonDocument |
| Server integration | ||
server.rs | src/server/server.rs | materialize_with_provenance, build_edition_provenance, attribution_query, attribution_verify, attribution_log_status, generate_attestation_report, apply_transclusion_attribution, provenance_ancestry, compute_provenance_chain |
otree_crdt.rs | src/server/otree_crdt.rs | materialize_edition_with_provenance, build_edition_provenance |
chained_log.rs | src/server/transport/chained_log.rs | ChainedLogWriter, ChainVerifyError (the security log, same pattern as attribution log) |