Ported from validate_staged_commit_account_identity_proofs (D-01, D-02,
D-03), extended in Phase 9 (UPD-01) with replacement-leaf identity binding.
Rejects a commit that drifts the GroupContext account-identity-proof
profile away from "current", that carries an invalid 0x8009 proof on
any new or re-signed member leaf, or that replaces an existing member's
leaf with one bound to a different account identity. Pure and non-throwing
— returns a CommitLegalityOutcome rather than throwing or returning
undefined.
Three checks, in order:
(a) Profile drift (D-01a). Both parentState and resultingState must
classify as the current profile (getGroupProfileSupport). Both
are checked — not just the resulting one — so a commit can never
"fix" an already-drifted parent into passing; the profile must already
have been, and remain, current.
(b) Changed-leaf proof validity (D-01b, D-02, D-03). Every entry
diffChangedLeaves reports between the two ratchet trees — every
non-blank leaf that is new (Add) or re-signed (Update proposal, or the
committer's own update-path leaf) — is validated with
validateLeafAccountIdentityProof against the RESULTING epoch's
ciphersuite. Unchanged leaves are trusted and never re-validated (D-01).
This runs BEFORE bucket classification for every changed leaf, so
UPD-02/UPD-03 keep reporting their existing proof reasons regardless of
which bucket the leaf falls into.
(c) Replacement-leaf identity binding (UPD-01, D-01/D-02/D-03). Each
changed leaf is classified with classifyChangedLeaf against
args.classification (when supplied):
- add — a new member in a freed slot legitimately carries a
different identity than whoever occupied the slot before removal;
no prior-identity comparison runs (D-01).
- update-proposal / committer-update-path — a genuine replacement
of an existing member's leaf. Its ChangedLeaf.parentLeaf MUST
be defined (a replacement always has a prior occupant); if it is
not, or if getCredentialPubkey throws for either leaf, this is a
fail-closed violation. Otherwise the replacement leaf's account
identity is compared against the prior leaf's; a mismatch is a
terminal member-identity-changed violation (UPD-01) — per
account-identity-proof-v2.md, a change of account identity is not a
self-update.
- unattributable — the changed leaf matches no Add, no Update
sender, and is not the committer's own leaf, with full
classification information available: fail closed as
unattributable-leaf (D-02).
- undecidable — classification information was incomplete (no
args.classification, or an undefined committerLeafIndex with no
matching proposal). This does NOT return immediately: the loop
continues, because a definite violation elsewhere in the commit
must always outrank an undecidable leaf (D-03) — otherwise a
provably illegal commit could be pooled and retried until it ages
out instead of being rejected. The first undecidable leaf's detail
is remembered and returned only if the whole loop completes with no
violation.
Every thrown AccountIdentityProofError (or any other unexpected throw) is
caught and mapped to a typed violation, never left to escape — fork-recovery
and tree-fed convergence call validateCommitLegality unwrapped.
Every detail string this function builds names only the numeric
leafIndex and a reason literal — never credential bytes, account
identity, pubkey hex, or err.message (D-06, diagnostics-privacy rule).
Ported from
validate_staged_commit_account_identity_proofs(D-01, D-02, D-03), extended in Phase 9 (UPD-01) with replacement-leaf identity binding. Rejects a commit that drifts the GroupContext account-identity-proof profile away from"current", that carries an invalid0x8009proof on any new or re-signed member leaf, or that replaces an existing member's leaf with one bound to a different account identity. Pure and non-throwing — returns a CommitLegalityOutcome rather than throwing or returningundefined.Three checks, in order: (a) Profile drift (D-01a). Both
parentStateandresultingStatemust classify as the current profile (getGroupProfileSupport). Both are checked — not just the resulting one — so a commit can never "fix" an already-drifted parent into passing; the profile must already have been, and remain, current. (b) Changed-leaf proof validity (D-01b, D-02, D-03). Every entry diffChangedLeaves reports between the two ratchet trees — every non-blank leaf that is new (Add) or re-signed (Update proposal, or the committer's own update-path leaf) — is validated with validateLeafAccountIdentityProof against the RESULTING epoch's ciphersuite. Unchanged leaves are trusted and never re-validated (D-01). This runs BEFORE bucket classification for every changed leaf, so UPD-02/UPD-03 keep reporting their existing proof reasons regardless of which bucket the leaf falls into. (c) Replacement-leaf identity binding (UPD-01, D-01/D-02/D-03). Each changed leaf is classified with classifyChangedLeaf againstargs.classification(when supplied): -add— a new member in a freed slot legitimately carries a different identity than whoever occupied the slot before removal; no prior-identity comparison runs (D-01). -update-proposal/committer-update-path— a genuine replacement of an existing member's leaf. Its ChangedLeaf.parentLeaf MUST be defined (a replacement always has a prior occupant); if it is not, or ifgetCredentialPubkeythrows for either leaf, this is a fail-closed violation. Otherwise the replacement leaf's account identity is compared against the prior leaf's; a mismatch is a terminalmember-identity-changedviolation (UPD-01) — per account-identity-proof-v2.md, a change of account identity is not a self-update. -unattributable— the changed leaf matches no Add, no Update sender, and is not the committer's own leaf, with full classification information available: fail closed asunattributable-leaf(D-02). -undecidable— classification information was incomplete (noargs.classification, or an undefinedcommitterLeafIndexwith no matching proposal). This does NOT return immediately: the loop continues, because a definite violation elsewhere in the commit must always outrank an undecidable leaf (D-03) — otherwise a provably illegal commit could be pooled and retried until it ages out instead of being rejected. The first undecidable leaf's detail is remembered and returned only if the whole loop completes with no violation.Every thrown
AccountIdentityProofError(or any other unexpected throw) is caught and mapped to a typed violation, never left to escape — fork-recovery and tree-fed convergence call validateCommitLegality unwrapped. Everydetailstring this function builds names only the numericleafIndexand a reason literal — never credential bytes, account identity, pubkey hex, orerr.message(D-06, diagnostics-privacy rule).