Skip to main content
A receipt that names a chain, an account or an asset carries a CAIP identifier. CAIP-2 names the chain (eip155:1), CAIP-10 names an account on that chain (eip155:1:0xAb16a9...), and CAIP-19 names an asset (eip155:1/slip44:60). When such an identifier is folded into a canonicalised, content-addressed record, it stops being a display string and becomes part of the hash preimage. Two verifiers must agree on its exact bytes or their digests diverge and the record stops recomputing. So the question is not merely whether an identifier is valid. It must be byte-canonical. That makes identifier validation a substrate concern rather than an application concern, and it is why it sits next to JCS canonicalisation in the same package.

The trailing-newline anchor trap

This is the failure that motivates the module, and it is a genuine cross-language divergence rather than a quirk of one runtime. Consider validating a CAIP-2 chain id with a regex anchored as ^...$, then hashing the string:
Whether that input is accepted depends on the language the verifier is written in: A producer in one language accepts the trailing newline and hashes the identifier with the newline. A verifier in another language rejects it, or accepts the clean identifier and hashes without it. Both parties believe they validated correctly, yet the digests differ by one byte and the record fails to recompute. This table is measured, not reasoned. The caip_edge_v1 conformance set runs 102 adversarial identifiers through eight implementations across the seven languages above. All eight validate 102/102 under the correct anchors; under a naive ^...$ the over-acceptance counts are Python 3, Java 8, PHP 3, Ruby 5, and Go, Rust and JavaScript 0. Opting into line anchors reverts the last three to the same defect, so the fault is the anchor rather than the language. Full matrix with engine versions: seven-engine attestation. The substrate therefore anchors with \A and \Z (Python) and without the multiline flag (TypeScript), which have no trailing-newline exception in any of the above:
Anchor discipline is load-bearing. require_caip2("eip155:1\n") raises rather than returning a value that would silently produce a non-reproducible digest. The strict require_* forms are the pre-hash gate: they fail closed before a non-canonical identifier ever reaches the digest.

Three validation tiers

The tiers are additive and opt-in. Chain-agnostic by default, strict where you ask for it.

1. Grammar

is_caip2 / isCaip2The CAIP grammar only. A future or not-yet-registered chain still validates, so the substrate does not become a gatekeeper on which chains may exist.

2. Registered namespace

is_registered_caip2 / isRegisteredCaip2Additionally requires a namespace registered in ChainAgnostic/namespaces, such as eip155, solana, cosmos, xrpl.

3. Reference format

is_valid_caip2 / isValidCaip2Strictest. Additionally requires the chain reference to be well formed for its namespace, so eip155:abc is rejected because eip155 references are decimal.
Each tier has a require_* counterpart (require_caip*, require_registered_caip*, require_valid_caip*) that returns the identifier unchanged or raises CaipError.
The namespace registry is vendored, not fetched at runtime, so validation stays offline and deterministic. Note that a registry directory name is not always the namespace it declares: the avalanche directory registers the namespace avax.

Install

Usage

Grammar

Verbatim from CAIP-2, CAIP-10 and CAIP-19:
The Python implementation uses only the standard library, so the tier adds no dependency to the substrate.

What is validated

The identifier validators ship with the substrate test suite. Every claim below is reproducible from the tagged source:

Cross-runtime

Full suite green on Python 3.10, 3.11, 3.12 and 3.13 and on Node 18, 20 and 22, run against the installed package rather than the source tree.

Differential

The regexes are checked against an independent regex-free oracle over a large fuzz corpus, so an anchor or character-class error surfaces as a disagreement rather than a silent pass.

Property-based

Structural generators build grammar-valid identifiers that must validate, and shrink any counterexample to a minimal failing input.

Non-catastrophic

Pathological inputs are checked for catastrophic backtracking, so a hostile identifier cannot stall a verifier.
The Python and TypeScript implementations hand-maintain the same grammar and the same namespace registry in separate files. A parity test parses the TypeScript source and asserts its constants match Python’s, so copy drift fails the suite rather than shipping as a silent cross-language disagreement.

Reproducible builds

The published artefacts are reproducible from the tagged source. An independent checkout of v0.5.0 rebuilds the wheel, source distribution and npm tarball byte for byte:
Line endings are pinned to LF in .gitattributes, so the build does not depend on the checkout platform.

Licence and attribution

Apache-2.0. Every module carries an SPDX-License-Identifier: Apache-2.0 header, and the NOTICE file ships inside the wheel, the source distribution and the npm tarball. When redistributing this work or derivative works, retain the NOTICE per Apache License, Version 2.0, Section 4(d). See Substrate licensing and attribution.

JCS Canonicalisation Substrate

The canonicalisation discipline these identifiers are hashed under.

Adopt the substrate

Anchor your own service or specification and be recorded in the registry.