Skip to content

feat(15.0.0)!: FedCode is non_exhaustive + admission is unconstructible unless verified (#274); CC decimals assigned (#275) - #276

Merged
emooreatx merged 1 commit into
mainfrom
fix/274-non-exhaustive-fedcode-and-enforcing-admission
Sep 6, 2026
Merged

feat(15.0.0)!: FedCode is non_exhaustive + admission is unconstructible unless verified (#274); CC decimals assigned (#275)#276
emooreatx merged 1 commit into
mainfrom
fix/274-non-exhaustive-fedcode-and-enforcing-admission

Conversation

@emooreatx

Copy link
Copy Markdown
Contributor

Closes #274 and #275.

Thanks for the review — the verdict framing and the DoS analysis were both useful, and both structural items are right.


1. FedCode grew a public field in a MINOR — twice

v14.1.0 added owned_nodes; v14.2.0 added ml_dsa_65_pubkey_sha256. Both to a public non-#[non_exhaustive] struct — a compile break for every struct-literal constructor, 12 call sites on your side, and, as you put it, the version number said it was safe to take blind.

That is exactly the hazard #257 raised and v14.0.0 was cut to fix. But that sweep annotated 31 error enums and zero structs — so the class survived in the half it never looked at, and I shipped it twice through that half. You caught the follow-through, not a new problem.

FedCode and OwnedNode are now #[non_exhaustive], with FedCode::new + with_* builders and OwnedNode::new.

Scope decided by evidence, not taste

236 public structs here carry public fields. Blanket-annotating would break destructuring for readers to buy nothing, so I asked which are actually constructed by consumers:

type constructed downstream
FedCode 12
OwnedNode 5
VerifyConfig 0
FullAttestationRequest 0
RegisterArgs 0
AttestationProof 0

Those two are the entire demonstrated hazard. Result types are read, not built.

2. The check had no enforcing call site

You're right that this is the wrong shape, not the wrong primitive. A free function returning Result<(), _> leaves nothing structurally stopping a host from registering an unchecked pull, and the failure is silent.

New AdmittedHybridKey — private fields, one fallible constructor taking the code and the pulled bytes together, so a value exists only on match:

let admitted = AdmittedHybridKey::admit(&code, &pulled)?;   // the only way in
host.register_federation_key(&admitted);                    // unchecked input unconstructible

Same discipline as Validity::checked. Exactly your suggested shape.

Your framing, adopted into the FSD

The subtlety you said a reader will ask about is now normative text in §3A.5, close to verbatim — the commitment proves the ML-DSA half is the one the minter committed to, not that one party holds both halves; nobody cross-signs them at registration and need not, since persist admits hybrid only and every row must verify under both signatures. Joint control is proven at first use, not registration. That's a better articulation than what shipped and it deserved to be in the spec rather than an issue thread.

Your DoS read matches mine: a wrong body is detectable locally, immediately, and attributably, and the fedcode door is human-initiated — the spam surface is the unsolicited announce path, which this doesn't create.


Also in this cut

#275 — all 7 UNASSIGNED evidence rows now carry their CC decimals (CIRISConstitution#101). 53 citations resolve, none unassigned. One note back on the homes, below.

bump-version.sh now rewrites intra-workspace sibling pins — a major bump had left ciris-crypto = { version = "N" } stale, fixed by hand at 13.0.0, 14.0.0 and 15.0.0 before I automated it rather than doing it a fourth time.


1491 workspace green, clippy clean, doc clean, guards + self-test pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01U4djyL5Vx6whagKB7J4586

…le unless verified (#274); CC decimals assigned (#275)

CIRISEdge reviewed v14.2.0 during its v20.2.0 adopt, found the design sound,
and raised two structural items. Both are right.

1. FedCode grew a public field in a MINOR -- twice.

   v14.1.0 added owned_nodes; v14.2.0 added ml_dsa_65_pubkey_sha256. Both to a
   public non-#[non_exhaustive] struct, which is a compile break for every
   struct-literal constructor: 12 call sites in edge, and the version number
   said it was safe to take blind.

   That is exactly the hazard #257 raised and v14.0.0 was cut to fix. But that
   sweep annotated 31 error ENUMS and ZERO structs -- so the class survived in
   the half it never looked at, and I shipped it twice through that half.

   FedCode and OwnedNode are now #[non_exhaustive] with FedCode::new + with_*
   builders and OwnedNode::new, so a future field is an added method.

   Scope decided by evidence, not taste: 236 public structs here carry public
   fields, and blanket-annotating would break destructuring for readers to buy
   nothing. A downstream sweep asked which are actually CONSTRUCTED by
   consumers -- FedCode (12) and OwnedNode (5) are the only two. VerifyConfig,
   FullAttestationRequest, RegisterArgs, AttestationProof: all zero. Result
   types are read, not built.

2. The commitment check had no enforcing call site.

   verify_pulled_ml_dsa_65_pubkey is the right primitive in the wrong shape: a
   free function returning Result<(), _>, so nothing structurally stopped a
   host from pulling an ML-DSA body and registering it unchecked -- and that
   failure is SILENT, a registered hybrid whose PQC half came from whoever
   answered the Pull.

   New AdmittedHybridKey fixes the class rather than trusting each call site:
   private fields, one fallible constructor taking the code and the pulled
   bytes together, so a value exists only on match. A host taking it as its
   registration input cannot express the unchecked path. Same discipline as
   Validity::checked.

Edge's framing is adopted into FSD-003 because a reader will ask: the
commitment proves the ML-DSA half is the one the minter committed to, NOT that
one party holds both halves. Nobody cross-signs them at registration and need
not -- persist admits hybrid only and every row must verify under both
signatures, so joint control is proven at FIRST USE, not registration.

Also: bump-version.sh now rewrites intra-workspace sibling pins, which a major
bump had left stale by hand at 13.0.0, 14.0.0 and 15.0.0 before automating it.

#275: all 7 UNASSIGNED evidence rows carry CC decimals (CIRISConstitution#101);
53 citations resolve, none unassigned.

1491 workspace green, clippy clean, doc clean, guards pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4djyL5Vx6whagKB7J4586
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@emooreatx
emooreatx merged commit 3531334 into main Sep 6, 2026
27 checks passed
@emooreatx
emooreatx deleted the fix/274-non-exhaustive-fedcode-and-enforcing-admission branch September 6, 2026 16:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fedcode v3 is sound; two structural follow-ups (unenforced check surface, FedCode field added in a minor)

1 participant