Surfaced by the P2.2 (#32) examples research pass. Correctness / write-path–read-path parity gap.
Summary
The event builders cannot construct a rotation/interaction event whose identifier prefix ("i") matches the self-addressing identifier its own inception produced. The deserializer accepts self-addressing KELs that the builders can never emit. This blocks building a genuine icp → ixn → rot KEL (and a self-addressing delegated inception) through the public API — the deferred #32 examples #5 (KEL chain) and #6 (delegated inception).
Reproduction (all from source)
The crate models identifiers correctly as a sum type:
// src/keri/identifier.rs:15
pub enum Identifier<'a> {
Basic(Prefixer<'a>), // key-coded, non-transferable
SelfAddressing(Saider<'a>), // digest-coded (SAID), transferable → rotatable
}
- Read path is correct:
src/serder/deserialize.rs:384 (parse_qb64_identifier) maps a digest-coded "i" → Identifier::SelfAddressing (line 399) and a key-coded "i" → Identifier::Basic (line 390). So it accepts self-addressing rotation/interaction events.
- Write path is not: the builders take
Prefixer (= Matter<VerKeyCode>), never Identifier:
src/serder/builder/rot.rs:82 pub fn prefix(self, prefix: Prefixer<'static>)
src/serder/builder/ixn.rs:62 pub fn prefix(self, prefix: Prefixer<'static>)
src/serder/builder/dip.rs:98 pub fn delegator(self, delegator: Prefixer<'static>)
- witness fields:
icp.rs:133, dip.rs:134, plus rot.rs witness_additions/removals — all Vec<Prefixer<'static>>
rot.build() (src/serder/builder/rot.rs:253) does RotationEvent::new(prefix.into(), …), and From<Prefixer> for Identifier (src/keri/identifier.rs:51) yields Identifier::Basic. Every builder-produced rot/ixn "i" is therefore Basic.
- An
InceptionBuilder produces a self-addressing prefix: SerializedEvent::prefix() returns Option<&Saider<'static>> (digest-coded) — src/serder/serialize.rs:90. Saider = Matter<DigestCode> vs Prefixer = Matter<VerKeyCode> (src/core/primitives/mod.rs:43,46) are distinct types with no From<Saider> for Prefixer.
Result: you cannot feed an inception's Saider prefix into RotationBuilder::prefix(). The repo's own tests sidestep this by fabricating an unrelated key prefix: rot.rs make_prefixer() builds Matter<VerKeyCode> from vec![3u8; 32] — no relationship to any inception. So the tests never chain a real KEL, and the write path can only emit Basic prefixes the read path treats as non-transferable.
This is a Mandatory-Rule-3 violation: "Read-path and write-path must enforce the same invariants the same way."
Proposed cleanup
Notes
Breaking change acceptable pre-1.0 — call out in PR + CHANGELOG. This is a parity item (keripy emits self-addressing KELs).
Summary
The event builders cannot construct a rotation/interaction event whose identifier prefix (
"i") matches the self-addressing identifier its own inception produced. The deserializer accepts self-addressing KELs that the builders can never emit. This blocks building a genuineicp → ixn → rotKEL (and a self-addressing delegated inception) through the public API — the deferred #32 examples #5 (KEL chain) and #6 (delegated inception).Reproduction (all from source)
The crate models identifiers correctly as a sum type:
src/serder/deserialize.rs:384(parse_qb64_identifier) maps a digest-coded"i"→Identifier::SelfAddressing(line 399) and a key-coded"i"→Identifier::Basic(line 390). So it accepts self-addressing rotation/interaction events.Prefixer(= Matter<VerKeyCode>), neverIdentifier:src/serder/builder/rot.rs:82pub fn prefix(self, prefix: Prefixer<'static>)src/serder/builder/ixn.rs:62pub fn prefix(self, prefix: Prefixer<'static>)src/serder/builder/dip.rs:98pub fn delegator(self, delegator: Prefixer<'static>)icp.rs:133,dip.rs:134, plusrot.rswitness_additions/removals — allVec<Prefixer<'static>>rot.build()(src/serder/builder/rot.rs:253) doesRotationEvent::new(prefix.into(), …), andFrom<Prefixer> for Identifier(src/keri/identifier.rs:51) yieldsIdentifier::Basic. Every builder-produced rot/ixn"i"is thereforeBasic.InceptionBuilderproduces a self-addressing prefix:SerializedEvent::prefix()returnsOption<&Saider<'static>>(digest-coded) —src/serder/serialize.rs:90.Saider = Matter<DigestCode>vsPrefixer = Matter<VerKeyCode>(src/core/primitives/mod.rs:43,46) are distinct types with noFrom<Saider> for Prefixer.Result: you cannot feed an inception's
Saiderprefix intoRotationBuilder::prefix(). The repo's own tests sidestep this by fabricating an unrelated key prefix:rot.rsmake_prefixer()buildsMatter<VerKeyCode>fromvec![3u8; 32]— no relationship to any inception. So the tests never chain a real KEL, and the write path can only emitBasicprefixes the read path treats as non-transferable.This is a Mandatory-Rule-3 violation: "Read-path and write-path must enforce the same invariants the same way."
Proposed cleanup
impl Into<Identifier<'static>>(orIdentifier<'static>) forRotationBuilder::prefix,InteractionBuilder::prefix,DelegatedInceptionBuilder::delegator, and witness fields — so a self-addressing prefix is expressible.From<&SerializedEvent> for Identifier, orSerializedEvent::identifier() -> Identifier, so chaining doesn't require re-parsing JSON.Prefixer = Matter<VerKeyCode>alias — structurally it cannot hold a digest prefix; the "identifier prefix" concept isIdentifier, notPrefixer.icp → ixn → rotwhere each event's"i"equals the inception SAID, serialize + deserialize each, assert the chain is consistent. (This is the test that would have caught the gap.)Notes
Breaking change acceptable pre-1.0 — call out in PR + CHANGELOG. This is a parity item (keripy emits self-addressing KELs).