WeaveDocs
Woven

Event Model

The signed Woven event record — schema, deterministic IDs, identity binding, and on-disk encoding.

Purpose

This page covers the canonical Woven event: the Event struct, the EventBuilder constructor, the deterministic EventRef ID, Ed25519 signing, and the wire schema used when an event is stored in a Strand block. Read this first if you are defining a new event kind, parsing events from a peer, or implementing an identity adapter that signs on behalf of an author.

What an event is

An Event binds an author identity (DID or identity URI), a SpaceId, an event kind (dotted lowercase like social.post), optional refs, attachments, capabilities, a canonical JSON body, a created_at second-resolution timestamp, the signer's 32-byte Ed25519 public key, and an Ed25519 signature over the canonical payload. The EventRef ID is the hex digest of weave_crypto::hash(["woven:event:v1", canonical_payload]), so identical payloads always produce identical IDs.

Note

EventRef is content-addressed. Two identical payloads from the same author produce the same EventRef — useful for deduplication, but it means you cannot publish two semantically distinct events with identical canonical bodies.

Signing an event

use serde_json::json;
use woven::{EventBuilder, SpaceId};

let keypair = weave_crypto::key_pair(Some(&[7u8; 32]));

let event = EventBuilder::new(
    "did:l1fe:agent:alice",
    SpaceId::new("woven://dsocial/global")?,
    "social.post",
)
.body(json!({ "text": "hello p2p web" }))
.created_at(1_777_130_000)
.sign(&keypair)?;

assert!(event.verify(&keypair.public_key)?);

EventBuilder rejects empty authors and event kinds that are not dotted lowercase (a-z0-9_- segments separated by .). Bodies are canonicalised (object keys sorted recursively) before signing so two clients producing the same logical body always agree on the ID.

Signing through an identity adapter

When the signing key lives in a wallet, HSM, or remote service, use sign_with to provide a closure that returns the signature bytes for a canonical payload:

let event = EventBuilder::new(
    "did:l1fe:agent:alice",
    SpaceId::new("woven://dsocial/global")?,
    "social.post",
)
.body(json!({ "text": "signed by identity adapter" }))
.created_at(1_777_130_000)
.sign_with(keypair.public_key, |payload| {
    Ok(weave_crypto::sign(payload, &keypair.secret_key).to_vec())
})?;

The closure must produce a signature over exactly the bytes it is handed. Woven never re-canonicalises the payload after signing, so swapping in a different serializer in your adapter will break verification.

Verification and tampering

Event::verify(&public_key) recomputes the deterministic ID, checks that the supplied key matches event.public_key(), and validates the Ed25519 signature against the canonical payload. Any field mutation — including a single byte changed in body after signing — fails verification.

Storage encoding

Event::to_strand_bytes() wraps an event in a StrandEventRecord { protocol: "woven.event", version: 1, event } envelope and serialises it as JSON. The matching Event::from_strand_bytes() decoder rejects unknown protocols and versions, so the on-disk format is forward- and backward-compatible.

Primary types to know

  • Event — the signed record
  • EventBuilder — typed constructor with sign/sign_with
  • EventRef — deterministic woven:event:<hex> identifier
  • SpaceIdwoven://<app>/<scope> identifier
  • WovenError — typed error enum with InvalidIdentifier, InvalidKind, Schema, and VerificationFailed