Skip to content

serder write path forces i==d on inception — cannot emit basic-derivation identifiers keripy produces #144

Description

@joeldsouzax

Summary

serder's inception serializer unconditionally sets the identifier prefix i equal to the computed SAID d (the double-SAID / self-addressing case). It has no path to emit a basic-derivation inception, where i is the qb64 public key and i ≠ d. keripy produces such events (transferable identifiers with basic key derivation), and one is in our own differential corpus. Reading such an event and re-serializing it corrupts both d and i.

Reproduction

The first event in keri/tests/corpus/keystate.jsonl is a keripy-generated icp with i ≠ d:

  • d = ENPqUQcJMv0oydINXTnu4RgIxNXZxGO5bVVaIwad9p5j (Blake3-256 SAID)
  • i = DEPIjjhH8mxoUqbrIeKv0mWS1Nj-K8Z0ikpuehf6t7Kf (Ed25519 basic-derivation public key)

Read it with deserialize_event (works — the #142 read path handles i ≠ d correctly), then re-serialize:

let event = deserialize_event(raw).unwrap();      // OK: cesr READS basic inceptions faithfully
let reser = event.serialize().unwrap();
assert_eq!(reser.as_bytes(), raw);                // FAILS

Result — both fields are rewritten:

field keripy original after cesr read→write
i DEPIjjhH… (public key) ECdHUvPz… (overwritten with a SAID)
d ENPqUQcJ… (keripy single-SAID) ECdHUvPz… (recomputed as double-SAID)

d also changes because the writer recomputes the SAID over a double-# placeholder (both d and i blanked), whereas keripy computed d over the event with i = the real key (single-SAID).

Root cause

  • EventRef::is_double_said (cesr/src/serder/serialize.rs) returns true for every Inception/DelegatedInception, so serialize_with backpatches the i slot with the computed SAID.
  • The direct writer (serialize/direct.rs render_icp) writes the SAID placeholder into i, never the event's actual prefix.

Provenance (not a #140/#141 regression)

This predates the #141 backend seam. At #139 (f274048), the pre-seam build_icp_json already hardcoded map.insert("i", said_value) with a doc comment framing it as intentional ("the double-SAID property … where the prefix is self-addressing"). #141 was a byte-identical refactor that faithfully preserved it. The i=d assumption dates to the original keri-serder implementation — a deliberate but incorrect "all inceptions are self-addressing" assumption.

Why no test caught it

keri/tests/differential.rs drives corpus events through deserialize_event and checks key state — it never re-serializes and byte-compares. The write-path lossiness on basic inceptions was therefore invisible. Surfaced by the #142 deterministic variant matrix, which tried to construct a single-SAID (i ≠ d) inception and found it isn't constructible through the writer.

Proposed fix (separate PR)

  1. Serialize the inception's actual prefix into i; only set i = d when the prefix is self-addressing and equals the SAID (i.e. derive double-vs-single-SAID from the event's Identifier variant, not unconditionally).
  2. Compute the SAID single-SAID (only d blanked) when i ≠ d.
  3. Add a corpus round-trip byte-identity test (read → re-serialize → assert equal) over keystate.jsonl so this class can never regress silently again.

Scope

Write-path only; orthogonal to #142 (the read path is correct and handles these events faithfully). Basic-derivation identifiers are a keripy-parity requirement.

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions