MHDA MultiChain Hierarchical Deterministic Address
A URN-based descriptor for blockchain HD addresses, compatible with RFC 8141. A single string captures everything needed to identify a derived address: network, derivation scheme, path, signature curve, encoding format, prefix and suffix conventions, and an optional wallet context.
A Taproot address on Bitcoin, BIP-86 path, bech32m:
An EVM address bound to a wallet context:
One string for a derived address
MHDA unifies the encoding of derivation paths, signature curves and address formats across EVM,
Bitcoin, Cosmos, Solana, XRP Ledger, Stellar, NEAR, Aptos, Sui, Cardano, Algorand and TON. The chain
identity is the (nt, ci) pair; the SLIP-44 coin type is an optional ct
annotation, because for HD addresses the coin already lives in the derivation path.
| Key | Component | Required | Default |
|---|---|---|---|
nt | Network type | yes | – |
ci | Chain ID | yes | – |
ct | Coin type (SLIP-44) | no | none (metadata) |
dt | Derivation type | no | root |
dp | Derivation path | no | empty when dt=root |
aa | Address algorithm | no | per-network default |
af | Address format | no | per-network default |
ap | Address prefix | no | none |
as | Address suffix | no | none |
wt | Wallet type | no | none |
wi | Wallet ID | no | none |
Canonical order, any input order
The chain identity comes first, so a chain key is always a strict prefix of the URN. Parsers accept any order on input; optional components are emitted only when set.
Exact round-trip
For canonical input Parse(s).String() == s. A short form round-trips to the same short form, a long form to the same long form; an explicit dt:root folds into the short form. Hardening markers ', H and h are all accepted.
Wallet domain
wt names the client or protocol (web3, tonconnect), wi the wallet instance, e.g. a UUID or an HD root key fingerprint. Values use the RFC 3986 characters of an NSS, so setters reject :, ?, #, % and whitespace.
Supported networks
Every network registers its algorithms, formats and derivation types. Strict validation requires the
resolved combination to be in this matrix and derivable: ed25519 paths are fully hardened, Sui, Aptos and
NEAR tie each derivation type to its curve, and a Bitcoin address format must match the purpose of the path.
root, the non-HD form, is accepted on every network.
nt |
SLIP-44 | Algorithms | Formats | Derivations | Defaults |
|---|---|---|---|---|---|
| bitcoin | 0 | secp256k1 | p2pkh, p2sh, p2wpkh, p2wsh, p2tr, bech32, bech32m | bip32, bip44, bip49, bip84, bip86 | secp256k1 |
| evm | 60 | secp256k1 | hex | bip32, bip44 | secp256k1, hex |
| avalanche | 9000 | secp256k1 | hex, bech32 | bip44 | secp256k1 |
| tron | 195 | secp256k1 | base58 | bip44 | secp256k1, base58 |
| cosmos | 118 | secp256k1, ed25519 | bech32 | bip44, cip11 | secp256k1, bech32 |
| solana | 501 | ed25519 | base58 | slip10 | ed25519, base58 |
| xrpl | 144 | secp256k1, ed25519 | base58 | bip44 | secp256k1, base58 |
| stellar | 148 | ed25519 | strkey | slip10 | ed25519, strkey |
| near | 397 | ed25519, secp256k1 | hex | slip10, bip44 | ed25519, hex |
| aptos | 637 | ed25519, secp256k1 | hex | slip10, bip44 | ed25519, hex |
| sui | 784 | ed25519, secp256k1, secp256r1 | hex | slip10, bip54, bip74 | ed25519, hex |
| cardano | 1815 | ed25519 | bech32, base58 | cip1852 | ed25519, bech32 |
| algorand | 283 | ed25519 | base32 | slip10 | ed25519, base32 |
| ton | 607 | ed25519 | base64url, hex | slip10 | ed25519, base64url |
Derivation types: root, bip32, bip44, bip49,
bip54, bip74, bip84, bip86, slip10
(variable length), cip11, cip1852, zip32. Each path is checked
against the template of its type. Per-network notes are in
SPEC §2.
MHDA URNs by network
| Network | Example URN |
|---|---|
| Ethereum | urn:mhda:nt:evm:ci:1:dt:bip44:dp:m/44'/60'/0'/0/0 |
| Bitcoin (BIP-86) | urn:mhda:nt:bitcoin:ci:bitcoin:dt:bip86:dp:m/86'/0'/0'/0/0:af:bech32m:ap:bc1p |
| Solana | urn:mhda:nt:solana:ci:mainnet:dt:slip10:dp:m/44'/501'/0'/0' |
| Stellar | urn:mhda:nt:stellar:ci:mainnet:dt:slip10:dp:m/44'/148'/0' |
| Sui (ed25519) | urn:mhda:nt:sui:ci:mainnet:dt:slip10:dp:m/44'/784'/0'/0'/0' |
| Cardano | urn:mhda:nt:cardano:ci:mainnet:dt:cip1852:dp:m/1852'/1815'/0'/0/0 |
| Cosmos | urn:mhda:nt:cosmos:ci:cosmoshub:dt:cip11:dp:m/44'/118'/0'/0/0 |
| Algorand | urn:mhda:nt:algorand:ci:mainnet non-HD |
| TON | urn:mhda:nt:ton:ci:mainnet non-HD, friendly base64url by default |
| EVM short form | urn:mhda:nt:evm:ci:1 defaults: secp256k1, hex |
| With metadata | urn:mhda:nt:evm:ci:1:ct:60 optional SLIP-44 annotation |
| Wallet-bound | urn:mhda:nt:ton:ci:mainnet:wt:tonconnect:wi:c0a8f2d4-3b6e-4a51-9c7d-2f8e1a0b5c93 |
What the libraries give you
Lenient and strict parsing
Lenient parsing checks structure: prefix, key:value pairs with known keys and RFC 3986 values, no duplicates, paths that match their type with every index below 231. Strict parsing also checks the (algorithm, format, derivation) combination against the network matrix and that the curve can derive the path.
Chain keys
nt:evm:ci:1 is itself a parseable key for maps, caches and hashes. Chain keys are canonical-only: a pre-1.1 key that carries ct fails loudly instead of being reinterpreted.
Content addressing
Hash256 and NSSHash256 give a SHA-256 of the canonical form for deduplication and content addressing. SHA-1 variants are kept only for stable identifiers.
Codecs out of the box
In Go, Address implements TextMarshaler, so JSON, XML and YAML encoding work unchanged; MarshalText writes the canonical URN.
Typed errors
Every failure wraps an exported sentinel: errors.Is(err, mhda.ErrIncompatible) in Go, parse_error::code() in C++. Messages are for people, codes are the contract.
Fuzzed and sanitised
Go fuzz targets for URNs, NSS and paths. The C++ port runs 160 test cases under AddressSanitizer, UBSan and LeakSanitizer. Both suites share conformance tables, and differential runs check that the two give byte-identical results.
Install and quick start
Go
go get github.com/censync/go-mhdaimport (
"errors"
"fmt"
mhda "github.com/censync/go-mhda"
)
func main() {
// Lenient parsing: structural validation only.
addr, err := mhda.ParseURN("urn:mhda:nt:evm:ci:1")
if err != nil {
panic(err)
}
fmt.Println(addr.Chain().NetworkType()) // evm
fmt.Println(addr.Algorithm()) // secp256k1 (default for evm)
fmt.Println(addr.Format()) // hex (default for evm)
// Strict parsing: also checks the (network, algorithm, format,
// derivation) combination is in the known-good compatibility matrix.
if _, err := mhda.ParseURNStrict("urn:mhda:nt:evm:ci:1:aa:ed25519"); err != nil {
if errors.Is(err, mhda.ErrIncompatible) {
fmt.Println("evm + ed25519 rejected, as expected")
}
}
// The chain identity (nt, ci) is itself a parseable key.
key := addr.Chain().Key() // "nt:evm:ci:1"
chain, _ := mhda.ChainFromKey(key)
_ = chain
// Optional wallet context: client type + wallet instance id.
wallet, _ := mhda.ParseURN("urn:mhda:nt:evm:ci:1:wt:web3:wi:5f2a8c31")
fmt.Println(wallet.WalletType(), wallet.WalletId()) // web3 5f2a8c31
}Go 1.18+. API reference on pkg.go.dev.
C++17
include(FetchContent)
FetchContent_Declare(mhda
GIT_REPOSITORY https://github.com/censync/mhda.git
GIT_TAG v1.2.0
)
FetchContent_MakeAvailable(mhda)
target_link_libraries(my_app PRIVATE mhda::mhda)#include <iostream>
#include "mhda/mhda.hpp"
int main() {
using namespace mhda;
// Lenient parsing: structural validation only.
auto addr = parse_urn("urn:mhda:nt:evm:ci:1");
std::cout << addr.get_chain().network().str() << "\n"; // evm
std::cout << addr.resolved_algorithm().str() << "\n"; // secp256k1
std::cout << addr.resolved_format().str() << "\n"; // hex
// Strict parsing also checks the (network, algorithm, format, derivation)
// combination is in the known-good compatibility matrix.
try {
parse_urn_strict("urn:mhda:nt:evm:ci:1:aa:ed25519");
} catch (const parse_error& e) {
if (e.code() == error_code::incompatible) {
std::cout << "evm + ed25519 rejected, as expected\n";
}
}
// Hashing for content-addressing or deduplication.
auto bip = parse_urn("urn:mhda:nt:evm:ci:1:dt:bip44:dp:m/44'/60'/0'/0/0");
std::cout << bip.hash256() << "\n"; // SHA-256 hex
}C++17, CMake 3.14+, no external runtime dependencies. Also installable with cmake --install and find_package(mhda).
GitHub repositories and packages
go-mhda is the normative reference: the specification lives there and changes land there first. mhda is a faithful C++17 port that keeps the URN format and validation rules bit-identical, so a URN produced by either implementation parses and round-trips through the other. Both are MIT-licensed; the current release is 1.2.0.
| Language | GitHub repository | Package | Install | Release |
|---|---|---|---|---|
| Goreference implementation and specification | censync/go-mhda | pkg.go.dev github.com/censync/go-mhda |
go get github.com/censync/go-mhda |
v1.2.0 |
| C++17port of go-mhda | censync/mhda | CMake mhda::mhdaGitHub Releases |
CMake FetchContent or find_package(mhda) |
v1.2.0 |
Documentation
- Specification: components, networks, derivation types, formats, validation rules, errors, concurrency and known limitations.
- Release notes 1.2.0: the stricter grammar and validation, and which URNs no longer parse.
- Release notes 1.1.0: URN grammar 1.1 and migration from 1.0.
- C++ changelog and the Go → C++ API mapping.
Version 1.2
Version 1.2 (5 October 2026) refuses what could name the wrong key. A path level index of
231 or more is rejected: it collided with the hardened bit, so
m/44'/60'/2147483648'/0/0 derived the key of m/44'/60'/0'/0/0. The NSS is
parsed strictly as key:value pairs in the RFC 3986 character set, a path needs a
derivation type, strict validation checks that the curve can derive the path, and values set in code
are validated like parsed ones. URNs that 1.1 accepted with these defects no longer parse; the release
notes list them.
Version 1.1 (4 July 2026) made (nt, ci) the chain identity, turned the SLIP-44 coin
type into optional metadata, added the wallet domain and renamed network types to the commonly
accepted names (bitcoin, tron, solana…).
