Skip to content

fix(delegation): no depth ceiling for an absent maxDepth, refuse minting without a ceiling - #25

Merged
aeoess merged 4 commits into
mainfrom
fix/legacy-maxdepth-absent
Oct 11, 2026
Merged

aeoess merged 4 commits into
mainfrom
fix/legacy-maxdepth-absent

Conversation

@aeoess

@aeoess aeoess commented Oct 11, 2026

Copy link
Copy Markdown
Member

What problem this solves

verify_delegation read an absent maxDepth as 0, so every signed legacy record without the member failed at currentDepth 1 or more, while a present "maxDepth": None skipped the check. The canonical form drops None members, so those two forms sign to the same bytes, and a holder could add the None member to turn an invalid record valid without the key. sub_delegate raised KeyError on a parent without maxDepth and TypeError on a non-numeric one.

Why this change was made

docs/SPEC-v1.1.md section 4.1 says an absent max_depth has no ceiling on verification, and the TypeScript verifyDelegation applies none. Reading 0 matched neither. Verification has to give both signed-identical forms one verdict, and minting under a parent with no ceiling is refused, as in the TypeScript SDK.

User impact

  • Signed records without maxDepth at depth 1 or more, which verify_delegation rejected, now verify. A present numeric maxDepth is compared as before. A present value that is not a number is still not compared.
  • sub_delegate raises ValueError when the parent's maxDepth or currentDepth is not a non-negative integer value. Integer-valued floats such as 2.0 are accepted, because they sign to the same bytes as 2 and the TypeScript SDK accepts them. Booleans, fractions, negatives and values beyond 2**53 - 1 are refused.
  • The Auditability check (F-005 in policy.py) still reads an absent maxDepth as 1. It is a separate acceptance policy and is not changed here, so a record can pass verify_delegation and fail F-005.

Evidence

  • tests/test_legacy_maxdepth_absent.py, 11 tests, including the vendored TypeScript vector delegation-without-max-depth-at-depth-64 and a paired None and absent test with equal canonical bytes. The verification, refusal and integer-valued tests fail before the change.
  • pytest: 2750 passed, 6 skipped.

Boundaries

  • The written clarification of sections 3.4 and 4.1 is errata item 3 in the TypeScript repository, in its own PR.
  • Treating a present non-numeric maxDepth as malformed is not in this PR. Tracked separately.

…maxDepth

verify_delegation read an absent maxDepth as 0, so every signed legacy record without
the member failed at currentDepth 1 or more. docs/SPEC-v1.1.md section 4.1 says an
absent max_depth has no ceiling on verification and v1.1 verifiers enforce the limit
when the field is present, and the TypeScript verifyDelegation applies none. Absence
now skips the comparison. A present maxDepth is compared as before, including present
values that are not numbers, which are still not compared. Writers are unchanged and
still emit maxDepth 1 when the caller omits it.

Regression: the vendored TypeScript case delegation-without-max-depth-at-depth-64 and a
locally signed record at currentDepth 3 fail before this change. Explicit limits are
pinned as controls.

Signed-off-by: Tymofii Pidlisnyi <tima@aeoess.com>
…epth

Separate from the verification change before it. Accepting a signed record without
maxDepth on verification is not permission to mint a child under it: with no ceiling on
the parent there is no signed value for the child to inherit. sub_delegate raised
KeyError on an absent maxDepth and TypeError on a non-numeric one. It now raises
ValueError naming the missing ceiling. Parents with an explicit maxDepth are unchanged.

Signed-off-by: Tymofii Pidlisnyi <tima@aeoess.com>
… depths to mint

The canonical form drops None members, so "maxDepth": None signs to the same bytes as
an absent member. A paired test pins that both get the same verdict. sub_delegate now
requires a non-negative integer maxDepth and currentDepth on the parent, as the
TypeScript subDelegate does.

Signed-off-by: Tymofii Pidlisnyi <tima@aeoess.com>
…unsafe ones

2.0 and 0.0 canonicalize to the same signed bytes as 2 and 0, and the TypeScript
subDelegate accepts them, but the integer guard refused them here. The guard now accepts
finite integer-valued numbers and refuses booleans, fractions, negatives and values
beyond 2**53 - 1, which the TypeScript SDK cannot represent exactly. Tests for both
sides and for currentDepth.

Signed-off-by: Tymofii Pidlisnyi <tima@aeoess.com>
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.

1 participant