Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 84 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,94 @@ All notable changes to this project will be documented here. The format
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.0] — 2026-04-27
## [1.1.0] — 2026-07-04

URN grammar 1.1, mirroring the go-mhda reference. The chain identity is now
the `(nt, ci)` pair; the SLIP-44 coin type is optional metadata. Existing
pre-1.1 URNs still parse (any component order is accepted on input), but
pre-1.1 chain keys are rejected loudly and must be regenerated.

### Changed

- **Chain identity is `(nt, ci)`.** `chain::str()` / `chain::key()` return
`nt:<network>:ci:<chain_id>` — the coin type never appears in the key —
and `chain::operator==` compares network and chain id only. The constructor
is now `chain(network_type, chain_id)`; the old three-argument form
(with a coin type) is gone.
- **`ct` is optional metadata.** `chain::coin()` returns
`std::optional<coin_type>` (`set_coin` / `clear_coin` manage it);
`address::set_coin_type("")` clears it. Parsers accept a URN/NSS without
`ct`; when present it must still be a valid uint32 (decimal or 0x-hex) and
is re-emitted in decimal.
- **Canonical NSS order** is now `nt:ci[:ct][:dt:dp][:aa][:af][:ap][:as]
[:wt][:wi]` — the chain key is a strict prefix of every NSS. Input order
remains free.
- **`chain::from_key` is canonical-only.** A chain key must BE the canonical
identity string `nt:<network>:ci:<chain_id>`: an input with `ct` throws the
new `error_code::coin_type_in_chain_key` (pre-1.1 keys fail loudly instead
of being silently reinterpreted); any other known non-identity component,
unknown tokens, reordering and non-canonical spelling throw the new
`error_code::invalid_chain_key`. Surrounding ASCII whitespace is trimmed
and tolerated. `chain::from_nss` stays lenient and still extracts `nt`,
`ci` and the optional `ct` from any NSS.
- **Strict `ct` grammar.** Coin-type values parse as plain decimal or
`0x`/`0X`-prefixed hex only: `0o`/`0b` prefixes, digit-group underscores,
signs and a bare `0x` are rejected, and a leading zero is plain decimal
(`060` == 60, never octal).
- **Printable-ASCII values.** Every NSS value must consist of printable
ASCII (0x21–0x7E) after ASCII trimming: control bytes, interior whitespace
and non-ASCII bytes (incl. Unicode spaces) throw
`parse_error(invalid_nss)` instead of being silently normalised.
- **Validated free-form setters.** `set_address_prefix` / `set_address_suffix`
/ `set_wallet_type` / `set_wallet_id` reject values containing `:`, `?`,
`#` or anything outside printable ASCII with the new
`error_code::invalid_value` (empty still resets). The
`address(chain, path, aa, af, ap, as)` constructor routes its params
through the same setters, so invalid constructor input throws too.
- **Network-type values renamed** to the commonly accepted network names
(constant identifiers unchanged): `bitcoin` (was `btc`), `avalanche`
(was `avm`), `tron` (was `tvm`), `solana` (was `sol`), `xrpl` (was `xrp`),
`stellar` (was `xlm`), `aptos` (was `apt`), `cardano` (was `ada`),
`algorand` (was `algo`). `evm`, `cosmos`, `near`, `sui`, `ton` are
unchanged. There are no aliases: the old short names are invalid.
- `coins::atom` fixed to 118 (was 168, which SLIP-44 assigns to
Helleniccoin); 118 also matches the coin level of CIP-11 paths.

### Added

- **Wallet domain** on `address`: free-form `wt` (wallet type, e.g. `web3`,
`tonconnect`) and `wi` (wallet instance id) components, each independently
optional, emitted last in the canonical NSS and orthogonal to strict
validation. API: `wallet_type()` / `wallet_id()` accessors and
`set_wallet_type` / `set_wallet_id` setters (empty string resets).
- Coin-type registry extended with 34 SLIP-44 entries (etc, bch, eos, icp,
ckb, zil, luna, dot, ksm, kava, fil, cspr, egld, scrt, flow, vet, rune,
ftm, one, xtz, hype, hbar, move, stx, bera, xch, strk, mina, wax, kas,
osmo, sei, inj, mon); the list is ordered ascending by index.
- `error_code::invalid_value` — raised by the free-form component setters
(ap/as/wt/wi) and the address constructor on NSS-corrupting values.

### Removed

- `error_code::missing_coin_type` — `ct` is never required anymore.

### Documentation / tests

- SPEC.md and README brought in lockstep with the Go reference (grammar 1.1,
wallet domain, chain API, charset and value-validation rules, error table).
- Test corpus mirrors the Go fixtures: new wallet-domain suite, strict
chain-key suite, optional-ct semantics, updated hash reference vectors for
the new canonical form, and the post-review hardening suite (canonical-only
chain keys, ct spellings, printable-ASCII enforcement, setter validation,
case-preservation, coin-registry spot checks); 139 test cases total.

## [1.0.0] — 2026-04-27

Initial public release. C++17 port of the
[go-mhda](https://github.com/censync/go-mhda) reference implementation,
mirroring its parser, validator, derivation-path support and hash surface.
Shipped as tag v1.0.0; the in-tree version markers of that tree still read
0.1.0.

### Added

Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.14)

project(mhda
VERSION 0.1.0
VERSION 1.1.0
DESCRIPTION "MultiChain Hierarchical Deterministic Address (MHDA) — C++ port of go-mhda"
LANGUAGES CXX
)
Expand Down
77 changes: 50 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,37 @@ MHDA is a URN-based descriptor for blockchain HD addresses, with
[RFC 8141](https://datatracker.ietf.org/doc/rfc8141/) compatibility.

A single string captures everything needed to identify a derived address:
network, derivation scheme, path, signature curve, encoding format and any
prefix/suffix conventions.
network, derivation scheme, path, signature curve, encoding format, any
prefix/suffix conventions, and an optional wallet context.

```
urn:mhda:nt:btc:ct:0:ci:bitcoin:dt:bip86:dp:m/86'/0'/0'/0/0:af:bech32m:ap:bc1p
urn:mhda:nt:bitcoin:ci:bitcoin:dt:bip86:dp:m/86'/0'/0'/0/0:af:bech32m:ap:bc1p
```

Supported networks: Bitcoin, EVM, Avalanche, Tron, Cosmos, Solana, XRP,
Supported networks: Bitcoin, EVM, Avalanche, Tron, Cosmos, Solana, XRP Ledger,
Stellar, NEAR, Aptos, Sui, Cardano, Algorand, TON.

The chain identity is the `(nt, ci)` pair; the SLIP-44 coin type is an
optional `ct` metadata component (for HD addresses the coin already lives in
the derivation path). The optional wallet domain (`wt`/`wi`) binds an address
to a client type and a wallet instance:

```
urn:mhda:nt:evm:ci:1:dt:bip44:dp:m/44'/60'/0'/0/0:wt:web3:wi:5f2a8c31
```

## Status

- Version: **1.0.0**
- Version: **1.1.0**
- Standard: **C++17**, no external runtime dependencies
- Tests: **71** unit + fuzz-equivalent stress cases (≈11 000 randomised
- Tests: **139** unit + fuzz-equivalent stress cases (≈11 000 randomised
iterations), passing under `-fsanitize=address,undefined,leak`
- Compilers verified: GCC 11.4 (Ubuntu 22.04), Clang 14 (when libstdc++ is
available); the CI matrix runs Linux + macOS, Release + Debug
- Warning policy: clean under `-Wall -Wextra -Wpedantic -Wshadow -Wconversion
-Wsign-conversion -Werror`
- API surface frozen at **0.1.0**; binary stability is not yet guaranteed
across pre-1.0 minor versions
- Binary stability is not yet guaranteed across minor versions; 1.1.0
changes the URN grammar (see [CHANGELOG.md](./CHANGELOG.md))

## Building

Expand Down Expand Up @@ -69,7 +78,7 @@ target_link_libraries(my_app PRIVATE mhda::mhda)
include(FetchContent)
FetchContent_Declare(mhda
GIT_REPOSITORY https://github.com/censync/mhda.git
GIT_TAG v1.0.0
GIT_TAG v1.1.0
)
FetchContent_MakeAvailable(mhda)
target_link_libraries(my_app PRIVATE mhda::mhda)
Expand All @@ -85,34 +94,40 @@ int main() {
using namespace mhda;

// Lenient parsing: structural validation only.
auto addr = parse_urn("urn:mhda:nt:evm:ct:60:ci:1");
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:ct:60:ci:1:aa:ed25519");
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";
}
}

// Type-agnostic level-by-level path inspection.
auto bip = parse_urn("urn:mhda:nt:evm:ct:60:ci:1:dt:bip44:dp:m/44'/60'/0'/0/0");
auto bip = parse_urn("urn:mhda:nt:evm:ci:1:dt:bip44:dp:m/44'/60'/0'/0/0");
for (const auto& lvl : bip.path()->levels()) {
std::cout << " " << lvl.index << (lvl.is_hardened ? "'" : "") << "\n";
}

// Hashing for content-addressing or deduplication.
std::cout << bip.hash256() << "\n"; // SHA-256 hex

// The chain-domain triple (nt, ct, ci) is itself a parseable key.
auto key = bip.get_chain().key(); // "nt:evm:ct:60:ci:1"
// The chain identity (nt, ci) is itself a parseable key. Keys never carry
// the optional ct metadata; a pre-1.1 key with ct fails loudly with
// error_code::coin_type_in_chain_key.
auto key = bip.get_chain().key(); // "nt:evm:ci:1"
auto parsed = chain::from_key(key);
(void)parsed;

// Optional wallet context: client type + wallet instance id.
auto wallet = parse_urn("urn:mhda:nt:evm:ci:1:wt:web3:wi:5f2a8c31");
std::cout << wallet.wallet_type() << " " << wallet.wallet_id() << "\n";
}
```

Expand All @@ -122,16 +137,18 @@ A runnable version is in [`examples/basic.cpp`](./examples/basic.cpp).

| Network | Example URN |
|----------------|------------------------------------------------------------------------------------------------------|
| Ethereum | `urn:mhda:nt:evm:ct:60:ci:1:dt:bip44:dp:m/44'/60'/0'/0/0` |
| Bitcoin (BIP86)| `urn:mhda:nt:btc:ct:0:ci:bitcoin:dt:bip86:dp:m/86'/0'/0'/0/0:af:bech32m:ap:bc1p` |
| Solana | `urn:mhda:nt:sol:ct:501:ci:mainnet:dt:slip10:dp:m/44'/501'/0'/0'` |
| Stellar | `urn:mhda:nt:xlm:ct:148:ci:mainnet:dt:slip10:dp:m/44'/148'/0'` |
| Sui (ed25519) | `urn:mhda:nt:sui:ct:784:ci:mainnet:dt:slip10:dp:m/44'/784'/0'/0'/0'` |
| Cardano | `urn:mhda:nt:ada:ct:1815:ci:mainnet:dt:cip1852:dp:m/1852'/1815'/0'/0/0` |
| Algorand | `urn:mhda:nt:algo:ct:283:ci:mainnet` (non-HD) |
| TON | `urn:mhda:nt:ton:ct:607:ci:mainnet` (non-HD, friendly base64url default) |
| Cosmos | `urn:mhda:nt:cosmos:ct:118:ci:cosmoshub:dt:cip11:dp:m/44'/118'/0'/0/0` |
| EVM short form | `urn:mhda:nt:evm:ct:60:ci:1` (defaults: bip44, secp256k1, hex) |
| Ethereum | `urn:mhda:nt:evm:ci:1:dt:bip44:dp:m/44'/60'/0'/0/0` |
| Bitcoin (BIP86)| `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` |
| Algorand | `urn:mhda:nt:algorand:ci:mainnet` (non-HD) |
| TON | `urn:mhda:nt:ton:ci:mainnet` (non-HD, friendly base64url default) |
| Cosmos | `urn:mhda:nt:cosmos:ci:cosmoshub:dt:cip11:dp:m/44'/118'/0'/0/0` |
| 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` |

## API mapping (Go → C++)

Expand All @@ -141,10 +158,14 @@ A runnable version is in [`examples/basic.cpp`](./examples/basic.cpp).
| `mhda.ParseURNStrict` | `mhda::parse_urn_strict` |
| `mhda.ParseNSS` | `mhda::parse_nss` |
| `mhda.ChainFromKey` / `FromNSS` | `mhda::chain::from_key` / `from_nss` |
| `mhda.NewChain(...)` | `mhda::chain{...}` |
| `mhda.NewChain(nt, ci)` | `mhda::chain{nt, ci}` |
| `Chain.SetCoinType` / `ClearCoinType` | `chain::set_coin` / `chain::clear_coin` |
| `Chain.CoinType` + `HasCoinType` | `chain::coin` (`std::optional<coin_type>`) |
| `mhda.ParseDerivationPath` | `mhda::derivation_path::parse` |
| `mhda.NewDerivationPathFromLevels`| `mhda::derivation_path::from_levels` |
| `Address.String()` / `NSS()` | `address::str` / `address::nss` |
| `Address.WalletType` / `WalletId` | `address::wallet_type` / `wallet_id` |
| `Address.SetWalletType` / `SetWalletId` | `address::set_wallet_type` / `set_wallet_id` |
| `Address.MarshalText` | `address::marshal_text` |
| `Address.UnmarshalText` | `address::unmarshal_text` |
| `Address.Hash` / `Hash256` | `address::hash` / `hash256` |
Expand All @@ -160,9 +181,11 @@ Sentinel constants:
| `ErrInvalidNSS` | `error_code::invalid_nss` |
| `ErrMissingNetworkType` | `error_code::missing_network_type` |
| `ErrInvalidNetworkType` | `error_code::invalid_network_type` |
| `ErrMissingCoinType` | `error_code::missing_coin_type` |
| `ErrInvalidCoinType` | `error_code::invalid_coin_type` |
| `ErrMissingChainID` | `error_code::missing_chain_id` |
| `ErrCoinTypeInChainKey` | `error_code::coin_type_in_chain_key` |
| `ErrInvalidChainKey` | `error_code::invalid_chain_key` |
| `ErrInvalidValue` | `error_code::invalid_value` |
| `ErrInvalidDerivationType` | `error_code::invalid_derivation_type` |
| `ErrInvalidDerivationPath` | `error_code::invalid_derivation_path` |
| `ErrInvalidAlgorithm` | `error_code::invalid_algorithm` |
Expand Down Expand Up @@ -191,7 +214,7 @@ Mirrors the [SPEC §8](./SPEC.md#8-concurrency) contract.

## Testing & validation

- 71 unit + fuzz-equivalent test cases.
- 139 unit + fuzz-equivalent test cases.
- Fuzz harness runs ≈11 000 randomised mutations of the historical Go-fuzz
seed corpus per execution (URN, NSS and derivation-path entry points).
Contracts verified: no exception other than `parse_error`/`std::invalid_argument`,
Expand Down
Loading
Loading