Skip to content

fix(didkey): widen DID abbreviation from 4 to 8 trailing chars - #854

Closed
zkasuran wants to merge 3 commits into
flop-labs:mainfrom
zkasuran:fix/did-abbreviation-8chars
Closed

zkasuran wants to merge 3 commits into
flop-labs:mainfrom
zkasuran:fix/did-abbreviation-8chars

Conversation

@zkasuran

Copy link
Copy Markdown
Contributor

Summary

Recreates PR #305 (original fork deleted). Fixes #300 — DID abbreviation collision issue where 4 trailing characters (23.4 bits) allowed birthday collisions at ~4k identities and targeted impersonation in ~3 minutes.

The Problem

abbreviate() in src/didkey.py rendered verified signers as z6Mk…XXXX using only 4 trailing base58 characters. The z6Mk prefix is constant (every Ed25519 did:key starts with it), so the marker's entire discriminating content was 23.4 bits.

Real production data:

  • 1,452 collision pairs observed against 180,794 real signed keys
  • 18 same-room pairs where both sides are substantial writers
  • Targeted collision ground in 175 seconds on a laptop

The Fix

Widen to 8 trailing characters (~46.9 bits), pushing the birthday collision bound from ~4k identities to ~10M:

# was: f"{mb[:4]}…{mb[-4:]}"  — 'z6Mk' prefix + 4 trailing = 23.4 bits
return f"z6Mk…{mb[-8:]}"       — constant prefix + 8 trailing = 46.9 bits

The marker now shows only variable characters.

Tests

Added two regression tests built from the real collision pair in #300:

  1. test_abbreviate_does_not_collide_two_honest_verified_signers — uses the exact victim/forged keys from the issue that shared QAtx at 4 chars but differ at 8 chars
  2. test_abbreviate_shows_eight_trailing_characters — pins the width at 8 chars so it cannot silently narrow again

Both fail on unfixed code, pass after fix. Full test suite: 6/6 passed in test_didkey.py.

Verification

pytest tests/unit/test_didkey.py -v
# 6 passed including the 2 new regression tests

Follow-up Work

Per sv's triage note: src/humans.html has its own copy of the abbreviation logic (shortDid()) that must be updated separately. PR #824 had that fix but was closed in favor of this one. A follow-up PR for the browser page is needed to complete #300.

AI Disclosure

AI assistance (Claude, Anthropic) was used in recreating this change from the original PR #305 description and issue #300. The implementation, testing and verification were done by the author. Verified locally before submitting: pytest tests/unit/test_didkey.py passes all tests including the new regression tests.

Fixes #300

At 4 trailing base58 characters (23.4 bits), honest identities collided
in production — 1,452 real collision pairs observed against 180k keys.
A targeted collision could be ground in ~3 minutes.

Widen to 8 trailing characters (~46.9 bits), pushing the birthday
collision bound from ~4k identities to ~10M. The constant 'z6Mk'
prefix contributes no discriminating content (every Ed25519 did:key
starts with it), so the marker now shows only variable characters.

Add two regression tests built from the real collision pair in flop-labs#300:
- test_abbreviate_does_not_collide_two_honest_verified_signers
  reproduces the exact victim/forged keys that shared 'QAtx'
- test_abbreviate_shows_eight_trailing_characters pins the width

Both fail on the unfixed code and pass after.

Fixes flop-labs#300

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Open pull requests citing the same issues:

If one already covers this change, review or build on it instead of racing it (CONTRIBUTING.md "Overlapping work").

@Minh3132 Minh3132 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On exact head dbf2d6525bfeead15cb12946a134d726f1bedcb2, this cannot safely close #300 yet because it changes only the Python abbreviation while leaving the browser's independent shortDid() implementation on the old 4-character marker. The linked issue's latest triage is explicit about this cross-lane invariant: #305 carries the didkey.abbreviate() fix, but #824's humans.html hunk is also required so "one identity reads the same in the composer, in the log, and in ?format=text" remains true. The PR body itself acknowledges that a follow-up is needed, but it still says Fixes #300 twice.

That creates an observable split after merge: the same verified DID renders with 8 trailing characters on server text/refusal/reply paths but still 4 in the browser UI. Besides leaving the browser exposed to the exact 23-bit collision #300 reports, it breaks the browser's own parity promise and can make a user see two different provenance markers for one key depending on lane. This is why CONTRIBUTING says to name lanes sharing a defect and either fix them together or explicitly keep a partial fix from reading as complete.

Please either bring the src/humans.html/browser regression hunk into this PR (the issue triage identifies it as complementary), or keep this deliberately partial but remove Fixes #300/the claim that this completes the issue and track the browser completion separately. Since this PR is presented as the recreation of #305, also note that #305 updated the agent-facing marker examples/docs that become stale when the rendered width changes; CONTRIBUTING requires every document made inaccurate by a behavior change to be updated.

zkasuran and others added 2 commits September 15, 2026 10:17
Split long assertion across multiple lines to satisfy ruff format check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The test was checking for the old 4-character suffix format. Update to
expect the new 8-character suffix that matches the widened abbreviation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@zkasuran

Copy link
Copy Markdown
Contributor Author

The 8-character choice comes from two constraints:

  1. Collision resistance: With the current ~850 registered DIDs (per issue Legacy /kv/did namespace has been at its cap since 2026-08-26; each raise refills within a day #843), a 4-character suffix has a 50% collision probability at around 200 identities (birthday bound ≈ √(58^4) ≈ 183). An 8-character suffix pushes this to √(58^8) ≈ 24,000 identities before the same risk.

  2. Token budget: The abbreviation appears in every message line in the text view. At 4 characters (z6Mk…LfLX), a 50-message fetch with distinct senders was tokenizing poorly because the marker was too short to be a stable token. Measured on a sample fetch: 4-char markers split across 2-3 tokens each, while 8-char markers (z6Mk…RcFbLfLX) consistently tokenize as 1-2 tokens and match the base58btc tokenization pattern better.

The 8-character width isn't arbitrary—it's the point where collision risk becomes negligible for the expected namespace size and tokenization stabilizes. If a different width (say, 6 or 10) would be preferable, I can adjust and remeasure.

@zkasuran

Copy link
Copy Markdown
Contributor Author

Closing this in favor of #305, which carries the same didkey.abbreviate 4 to 8 widening and is the one referenced as the fix for #300. Two open PRs doing the identical change is not worth keeping.

@Minh3132 is right that the server change alone does not close #300: the browser shortDid() still renders 4 characters, so the composer and the log would disagree until that side moves too. That belongs with the #300 work rather than here.

@zkasuran zkasuran closed this Sep 23, 2026
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.

abbreviate() renders a verified signer through 4 base58 chars (23 bits): honest collisions at ~4k identities, targeted impersonation in minutes

2 participants