Skip to content

perf(validators): replace tr46 with a generated UTS-46 table - #117

Merged
yeboster merged 1 commit into
mainfrom
perf/native-idna
Aug 28, 2026
Merged

perf(validators): replace tr46 with a generated UTS-46 table#117
yeboster merged 1 commit into
mainfrom
perf/native-idna

Conversation

@yeboster

Copy link
Copy Markdown
Contributor

What

Removes the tr46 runtime dependency and replaces it with a small generated table plus the Unicode operations the runtime already provides.

tr46 ships an explicit mapping target for every Unicode code point (~230 KB in a consumer's bundle). The SDK uses exactly one entry point from it: toUnicode(name, { useSTD3ASCIIRules: true }) in DomainValidator#normalize.

Almost all of those mapping targets are reproducible from NFKC + context-free case folding. So src/validators/idna derives the common case natively and consults a generated table only for what cannot be derived:

Data Size
DISALLOWED_RANGES — 776 ranges run-length + base36 delta encoded
LEADING_MARK_RANGES — 309 ranges run-length + base36 delta encoded
MAPPING_EXCEPTIONS — 526 code points literal pairs
src/validators/idna/table.ts 11.7 KB (vs 225 KB)

Plus an inline RFC 3492 punycode decoder for xn-- labels.

Bundle impact

esbuild --bundle --minify --platform=node on the published entry point:

bytes
main 1,642,792
this branch 1,425,317
delta −217,475 (−13.2%)

Own code grows 43,890 → 58,741 bytes (the table); the dependency side drops ~230 KB.

Note: a second copy of tr46 (260 KB) is still pulled in transitively by node-fetch. That one disappears with the perf/native-fetch PR, which removes node-fetch entirely.

Correctness

This is a security-relevant path — IDNA mapping decides which names are considered equal — so it is verified rather than spot-checked.

scripts/verify-idna.js runs the new implementation against tr46 over:

  • every Unicode code point (1,112,064)
  • 8,000 multi-code-point label strings built from an adversarial character set
  • the non-ASCII names actually registered on mainnet, plus the repo's fixtures
  • 2,000,000 seeded (reproducible) fuzz strings
total: 3120097 inputs, 0 mismatches

test/domain/idna.test.ts is the committed fast regression suite; it pins the registry-relevant cases so a break fails CI without needing tr46 installed.

Three subtleties, each a real bug caught during development and now covered by tests:

  1. Case folding is per code point. 'ΣΣ'.toLowerCase() is 'σς' (Greek final-sigma context); UTS-46 maps Σ to σ unconditionally.
  2. Code-point status must be probed in context. A bare combining mark fails the label-level "must not begin with a combining mark" rule, which belongs to the label, not the code point — deriving from standalone probes would reject "Á".
  3. The leading-mark rule applies to the mapped label. UTS-46 ignores variation selectors and the soft hyphen, so a mark following one only becomes label-leading after mapping ("︆̈" must be rejected).

Also: , and are UTS-46 label separators and are folded to . before splitting.

Regenerating

tr46 is no longer a dependency, so both scripts install it ad hoc:

yarn add --dev tr46 @types/tr46
node scripts/generate-idna-table.js   # rewrites src/validators/idna/table.ts
node scripts/verify-idna.js           # exhaustive differential check (minutes)

Worth re-running on a Unicode version bump.

Test status

npx tsc --noEmit clean, npx eslint src clean, npx jest -i21 suites / 269 tests, all passing.

(Under parallel jest, the live-testnet mint suites flake independently of this change; they are green serially, and were flaky on unmodified main too.)

Note on tsconfig.json

The exclude list gains coverage, dist, docs, scriptsallowJs: true otherwise pulls scripts/*.js and stale coverage output into the program. The same line is touched by the chore/security-hardening PR; the resulting content is identical, so the merge is clean either way.

https://claude.ai/code/session_01GceWCwGXu66D1xEBDxZWBb

@github-actions

Copy link
Copy Markdown

Total Coverage: 87.97%

Coverage Report
File Branch Funcs Lines Uncovered Lines
src
   actions.ts 100% 100% 100%
   index.ts 100% 100% 100%
   interface.ts 100% 100% 100%
   meta-names-sdk.ts 75% 75% 95.65% 22, 44, 45
   partisia-name-system.ts 58% 87.50% 84.07% 106, 114, 12, 139, 152, 160, 164, 169, 170, 171, 178, 180, 188, 189, 19, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 22, 220, 221, 222, 29, 33, 42, 49, 54, 55, 7, 78, 82
   version.ts 100% 100% 100%
src/actions
   domain.ts 64.29% 100% 100% 35, 44, 56, 71, 74
   record.ts 77.78% 100% 100% 14, 48
src/models
   domain.ts 100% 83.33% 95.95% 45, 46, 47
   index.ts 100% 100% 100%
src/models/helpers
   domain.ts 100% 100% 100%
src/providers
   config.ts 66.67% 100% 98.11% 40, 49, 50
   index.ts 100% 100% 100%
   secrets.ts 45.45% 62.50% 78.57% 19, 23, 24, 25, 26, 27, 29, 30, 44, 50, 51, 52, 53, 56, 57, 58, 59, 62, 63, 64, 65
src/providers/config
   mainnet.ts 100% 100% 100%
   testnet.ts 100% 100% 100%
src/repositories
   contract-repository.ts 75.81% 100% 98.19% 103, 114, 123, 124, 126, 127, 129, 130, 132, 133, 142, 158, 180, 196, 34, 46, 47, 57, 63
   domain-repository.ts 60% 100% 100% 102, 113, 117, 118, 133, 142, 150, 176, 220, 225, 240, 256, 262, 269, 34, 53, 54, 96, 98, 99
   index.ts 100% 75% 100%
   record-repository.ts 71.43% 100% 100% 24, 34, 50, 60
src/repositories/contracts
   meta-names-contract-repository.ts 84.62% 88.89% 88.66% 58, 59, 60, 61, 62, 63, 64, 65, 77, 78, 79, 80, 90
src/repositories/helpers
   avl-client.ts 80% 77.78% 61.62% 16, 22, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 95
   client.ts 88.89% 100% 100% 31
   contract.ts 75% 100% 92.45% 41, 42, 43, 44, 45, 50
src/transactions
   helper.ts 76.92% 85.71% 93.18% 51, 52, 53, 54, 55, 56, 57, 79, 79
   index.ts 50% 25% 31.72% 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 13, 132, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 93, 94, 95, 96, 97, 98, 99
   ledger.ts 100% 0% 43.62% 100, 101, 102, 103, 104, 105, 106, 11, 116, 117, 118, 119, 12, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 13, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 14, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 15, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 16, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 17, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 18, 180, 181, 182, 183, 184, 185, 186, 187, 188, 19, 20, 21, 26, 27, 28, 29, 30, 83, 84, 85, 86, 87, 88, 89, 97, 98, 99
src/validators
   base-validator.ts 100% 100% 100%
   domain-validator.ts 87.50% 100% 100% 29, 30, 31
   index.ts 100% 100% 100%
   record-validator.ts 100% 100% 100%
src/validators/idna
   index.ts 81.54% 100% 95.93% 108, 110, 110, 111, 118, 119, 120, 121, 122, 123, 124, 125, 136, 138, 139, 144, 151, 155, 168, 210, 46
   table.ts 100% 100% 100%
src/validators/records
   default-validator.ts 100% 100% 100%
   discord-validator.ts 100% 100% 100%
   email-validator.ts 100% 100% 100%
   main-validator.ts 100% 100% 100%
   price-validator.ts 100% 100% 100%
   regex-validator.ts 100% 100% 100%
   twitter-validator.ts 100% 100% 100%
   uri-validator.ts 100% 100% 100%
   wallet-validator.ts 100% 100% 100%
test
test/helpers
   config.ts 0% 100% 100% 7
   helper.ts 100% 60% 68% 25, 26, 27, 28, 29, 30, 31, 32, 35, 36, 37, 38, 39, 40, 41, 42
   index.ts 100% 100% 100%

@github-actions

Copy link
Copy Markdown

Total Coverage: 87.88%

Coverage Report
File Branch Funcs Lines Uncovered Lines
src
   actions.ts 100% 100% 100%
   index.ts 100% 100% 100%
   interface.ts 100% 100% 100%
   meta-names-sdk.ts 75% 75% 95.65% 22, 44, 45
   partisia-name-system.ts 58% 87.50% 84.07% 106, 114, 12, 139, 152, 160, 164, 169, 170, 171, 178, 180, 188, 189, 19, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 22, 220, 221, 222, 29, 33, 42, 49, 54, 55, 7, 78, 82
   version.ts 100% 100% 100%
src/actions
   domain.ts 64.29% 100% 100% 35, 44, 56, 71, 74
   record.ts 77.78% 100% 100% 14, 48
src/models
   domain.ts 100% 83.33% 95.95% 45, 46, 47
   index.ts 100% 100% 100%
src/models/helpers
   domain.ts 100% 100% 100%
src/providers
   config.ts 66.67% 100% 98.11% 40, 49, 50
   index.ts 100% 100% 100%
   secrets.ts 45.45% 62.50% 78.57% 19, 23, 24, 25, 26, 27, 29, 30, 44, 50, 51, 52, 53, 56, 57, 58, 59, 62, 63, 64, 65
src/providers/config
   mainnet.ts 100% 100% 100%
   testnet.ts 100% 100% 100%
src/repositories
   contract-repository.ts 75.81% 100% 98.19% 103, 114, 123, 124, 126, 127, 129, 130, 132, 133, 142, 158, 180, 196, 34, 46, 47, 57, 63
   domain-repository.ts 60% 100% 100% 102, 113, 117, 118, 133, 142, 150, 176, 220, 225, 240, 256, 262, 269, 34, 53, 54, 96, 98, 99
   index.ts 100% 75% 100%
   record-repository.ts 71.43% 100% 100% 24, 34, 50, 60
src/repositories/contracts
   meta-names-contract-repository.ts 84.62% 88.89% 88.66% 58, 59, 60, 61, 62, 63, 64, 65, 77, 78, 79, 80, 90
src/repositories/helpers
   avl-client.ts 80% 77.78% 61.62% 16, 22, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 95
   client.ts 88.89% 100% 100% 31
   contract.ts 75% 100% 92.45% 41, 42, 43, 44, 45, 50
src/transactions
   helper.ts 76.92% 85.71% 93.18% 51, 52, 53, 54, 55, 56, 57, 79, 79
   index.ts 20% 25% 37.35% 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 13, 14, 142, 147, 15, 16, 160, 161, 162, 164, 164, 165, 166, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 93, 94, 95, 96, 97, 98, 99
   ledger.ts 100% 0% 43.62% 100, 101, 102, 103, 104, 105, 106, 11, 116, 117, 118, 119, 12, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 13, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 14, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 15, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 16, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 17, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 18, 180, 181, 182, 183, 184, 185, 186, 187, 188, 19, 20, 21, 26, 27, 28, 29, 30, 83, 84, 85, 86, 87, 88, 89, 97, 98, 99
src/validators
   base-validator.ts 100% 100% 100%
   domain-validator.ts 87.50% 100% 100% 29, 30, 31
   index.ts 100% 100% 100%
   record-validator.ts 100% 100% 100%
src/validators/idna
   index.ts 81.54% 100% 95.93% 108, 110, 110, 111, 118, 119, 120, 121, 122, 123, 124, 125, 136, 138, 139, 144, 151, 155, 168, 210, 46
   table.ts 100% 100% 100%
src/validators/records
   default-validator.ts 100% 100% 100%
   discord-validator.ts 100% 100% 100%
   email-validator.ts 100% 100% 100%
   main-validator.ts 100% 100% 100%
   price-validator.ts 100% 100% 100%
   regex-validator.ts 100% 100% 100%
   twitter-validator.ts 100% 100% 100%
   uri-validator.ts 100% 100% 100%
   wallet-validator.ts 100% 100% 100%
test
test/helpers
   config.ts 0% 100% 100% 7
   helper.ts 100% 60% 68% 25, 26, 27, 28, 29, 30, 31, 32, 35, 36, 37, 38, 39, 40, 41, 42
   index.ts 100% 100% 100%

tr46 ships an explicit mapping target for every Unicode code point, which
costs ~230 KB in a consumer's bundle. The SDK uses exactly one of its
entry points, `toUnicode(name, { useSTD3ASCIIRules: true })`, to normalize
domain names.

Almost every one of those mapping targets is reproducible from NFKC plus
context-free case folding, both of which the runtime already implements
natively. So rather than shipping the table, generate and ship only what
cannot be derived:

  - run-length encoded ranges of code points UTS-46 disallows
  - run-length encoded ranges of combining marks, which UTS-46 allows
    inside a label but not at its start
  - 526 code points whose mapping differs from NFKC + case folding

That is 11.7 KB of source instead of 225 KB, plus an inline RFC 3492
punycode decoder for `xn--` labels.

Three details are worth calling out, because each was a real bug during
development:

  - Case folding must be applied one code point at a time. JavaScript's
    `String#toLowerCase` applies the Greek final-sigma rule, so 'ΣΣ'
    becomes 'σς'; UTS-46 maps Σ to σ unconditionally.
  - Code-point status must be probed in context. Testing a bare combining
    mark reports an error that belongs to the label rule, not to the code
    point, which would wrongly reject names like "Á".
  - The leading-mark rule applies to the mapped label. UTS-46 ignores
    variation selectors and the soft hyphen, so a mark that followed one
    only becomes label-leading after mapping.

`scripts/generate-idna-table.js` regenerates the table from tr46;
`scripts/verify-idna.js` checks the two implementations against each
other over 3,120,097 inputs (every code point, a multi-label corpus, the
non-ASCII names actually registered on mainnet, and 2,000,000 seeded
fuzz strings). Current result: 0 mismatches. `test/domain/idna.test.ts`
pins the cases that matter for the registry so a regression fails fast
without needing tr46 installed.

Bundled with esbuild (--bundle --minify --platform=node), the published
entry point drops from 1,642,792 to 1,425,317 bytes.

Claude-Session: https://claude.ai/code/session_01GceWCwGXu66D1xEBDxZWBb
@github-actions

Copy link
Copy Markdown

Total Coverage: 87.85%

Coverage Report
File Branch Funcs Lines Uncovered Lines
src
   actions.ts 100% 100% 100%
   index.ts 100% 100% 100%
   interface.ts 100% 100% 100%
   meta-names-sdk.ts 75% 75% 95.65% 22, 44, 45
   partisia-name-system.ts 58% 87.50% 84.07% 106, 114, 12, 139, 152, 160, 164, 169, 170, 171, 178, 180, 188, 189, 19, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 22, 220, 221, 222, 29, 33, 42, 49, 54, 55, 7, 78, 82
   version.ts 100% 100% 100%
src/actions
   domain.ts 64.29% 100% 100% 35, 44, 56, 71, 74
   record.ts 77.78% 100% 100% 14, 48
src/models
   domain.ts 100% 83.33% 95.95% 45, 46, 47
   index.ts 100% 100% 100%
src/models/helpers
   domain.ts 100% 100% 100%
src/providers
   config.ts 66.67% 100% 98.11% 40, 49, 50
   index.ts 100% 100% 100%
   secrets.ts 45.45% 62.50% 78.57% 19, 23, 24, 25, 26, 27, 29, 30, 44, 50, 51, 52, 53, 56, 57, 58, 59, 62, 63, 64, 65
src/providers/config
   mainnet.ts 100% 100% 100%
   testnet.ts 100% 100% 100%
src/repositories
   contract-repository.ts 75.81% 100% 98.19% 103, 114, 123, 124, 126, 127, 129, 130, 132, 133, 142, 158, 180, 196, 34, 46, 47, 57, 63
   domain-repository.ts 60% 100% 100% 102, 113, 117, 118, 133, 142, 150, 176, 220, 225, 240, 256, 262, 269, 34, 53, 54, 96, 98, 99
   index.ts 100% 75% 100%
   record-repository.ts 71.43% 100% 100% 24, 34, 50, 60
src/repositories/contracts
   meta-names-contract-repository.ts 84.62% 88.89% 86.14% 57, 58, 59, 60, 61, 62, 63, 64, 78, 79, 80, 81, 82, 83, 84, 94
src/repositories/helpers
   avl-client.ts 80% 77.78% 61.62% 16, 22, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 95
   client.ts 90% 100% 100% 44
   contract.ts 75% 100% 92.45% 41, 42, 43, 44, 45, 50
src/transactions
   helper.ts 76.92% 85.71% 93.18% 51, 52, 53, 54, 55, 56, 57, 79, 79
   index.ts 20% 25% 37.35% 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 13, 14, 142, 147, 15, 16, 160, 161, 162, 164, 164, 165, 166, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 93, 94, 95, 96, 97, 98, 99
   ledger.ts 100% 0% 43.62% 100, 101, 102, 103, 104, 105, 106, 11, 116, 117, 118, 119, 12, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 13, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 14, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 15, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 16, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 17, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 18, 180, 181, 182, 183, 184, 185, 186, 187, 188, 19, 20, 21, 26, 27, 28, 29, 30, 83, 84, 85, 86, 87, 88, 89, 97, 98, 99
src/validators
   base-validator.ts 100% 100% 100%
   domain-validator.ts 87.50% 100% 100% 29, 30, 31
   index.ts 100% 100% 100%
   record-validator.ts 100% 100% 100%
src/validators/idna
   index.ts 81.54% 100% 95.93% 108, 110, 110, 111, 118, 119, 120, 121, 122, 123, 124, 125, 136, 138, 139, 144, 151, 155, 168, 210, 46
   table.ts 100% 100% 100%
src/validators/records
   default-validator.ts 100% 100% 100%
   discord-validator.ts 100% 100% 100%
   email-validator.ts 100% 100% 100%
   main-validator.ts 100% 100% 100%
   price-validator.ts 100% 100% 100%
   regex-validator.ts 100% 100% 100%
   twitter-validator.ts 100% 100% 100%
   uri-validator.ts 100% 100% 100%
   wallet-validator.ts 100% 100% 100%
test
test/helpers
   config.ts 0% 100% 100% 7
   helper.ts 100% 60% 68% 25, 26, 27, 28, 29, 30, 31, 32, 35, 36, 37, 38, 39, 40, 41, 42
   index.ts 100% 100% 100%

@yeboster
yeboster merged commit 633e7b6 into main Aug 28, 2026
8 checks passed
@yeboster
yeboster deleted the perf/native-idna branch August 28, 2026 20:45
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