Skip to content

Commit 75a15da

Browse files
authored
Merge pull request #1 from censync/feat/urn-1.1-chain-identity
URN 1.1: chain identity without coin type, optional wallet domain
2 parents 9b4a41d + 3d0219b commit 75a15da

32 files changed

Lines changed: 1293 additions & 457 deletions

‎CHANGELOG.md‎

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,94 @@ All notable changes to this project will be documented here. The format
44
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
55
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7-
## [0.1.0] — 2026-04-27
7+
## [1.1.0] — 2026-07-04
8+
9+
URN grammar 1.1, mirroring the go-mhda reference. The chain identity is now
10+
the `(nt, ci)` pair; the SLIP-44 coin type is optional metadata. Existing
11+
pre-1.1 URNs still parse (any component order is accepted on input), but
12+
pre-1.1 chain keys are rejected loudly and must be regenerated.
13+
14+
### Changed
15+
16+
- **Chain identity is `(nt, ci)`.** `chain::str()` / `chain::key()` return
17+
`nt:<network>:ci:<chain_id>` — the coin type never appears in the key —
18+
and `chain::operator==` compares network and chain id only. The constructor
19+
is now `chain(network_type, chain_id)`; the old three-argument form
20+
(with a coin type) is gone.
21+
- **`ct` is optional metadata.** `chain::coin()` returns
22+
`std::optional<coin_type>` (`set_coin` / `clear_coin` manage it);
23+
`address::set_coin_type("")` clears it. Parsers accept a URN/NSS without
24+
`ct`; when present it must still be a valid uint32 (decimal or 0x-hex) and
25+
is re-emitted in decimal.
26+
- **Canonical NSS order** is now `nt:ci[:ct][:dt:dp][:aa][:af][:ap][:as]
27+
[:wt][:wi]` — the chain key is a strict prefix of every NSS. Input order
28+
remains free.
29+
- **`chain::from_key` is canonical-only.** A chain key must BE the canonical
30+
identity string `nt:<network>:ci:<chain_id>`: an input with `ct` throws the
31+
new `error_code::coin_type_in_chain_key` (pre-1.1 keys fail loudly instead
32+
of being silently reinterpreted); any other known non-identity component,
33+
unknown tokens, reordering and non-canonical spelling throw the new
34+
`error_code::invalid_chain_key`. Surrounding ASCII whitespace is trimmed
35+
and tolerated. `chain::from_nss` stays lenient and still extracts `nt`,
36+
`ci` and the optional `ct` from any NSS.
37+
- **Strict `ct` grammar.** Coin-type values parse as plain decimal or
38+
`0x`/`0X`-prefixed hex only: `0o`/`0b` prefixes, digit-group underscores,
39+
signs and a bare `0x` are rejected, and a leading zero is plain decimal
40+
(`060` == 60, never octal).
41+
- **Printable-ASCII values.** Every NSS value must consist of printable
42+
ASCII (0x21–0x7E) after ASCII trimming: control bytes, interior whitespace
43+
and non-ASCII bytes (incl. Unicode spaces) throw
44+
`parse_error(invalid_nss)` instead of being silently normalised.
45+
- **Validated free-form setters.** `set_address_prefix` / `set_address_suffix`
46+
/ `set_wallet_type` / `set_wallet_id` reject values containing `:`, `?`,
47+
`#` or anything outside printable ASCII with the new
48+
`error_code::invalid_value` (empty still resets). The
49+
`address(chain, path, aa, af, ap, as)` constructor routes its params
50+
through the same setters, so invalid constructor input throws too.
51+
- **Network-type values renamed** to the commonly accepted network names
52+
(constant identifiers unchanged): `bitcoin` (was `btc`), `avalanche`
53+
(was `avm`), `tron` (was `tvm`), `solana` (was `sol`), `xrpl` (was `xrp`),
54+
`stellar` (was `xlm`), `aptos` (was `apt`), `cardano` (was `ada`),
55+
`algorand` (was `algo`). `evm`, `cosmos`, `near`, `sui`, `ton` are
56+
unchanged. There are no aliases: the old short names are invalid.
57+
- `coins::atom` fixed to 118 (was 168, which SLIP-44 assigns to
58+
Helleniccoin); 118 also matches the coin level of CIP-11 paths.
59+
60+
### Added
61+
62+
- **Wallet domain** on `address`: free-form `wt` (wallet type, e.g. `web3`,
63+
`tonconnect`) and `wi` (wallet instance id) components, each independently
64+
optional, emitted last in the canonical NSS and orthogonal to strict
65+
validation. API: `wallet_type()` / `wallet_id()` accessors and
66+
`set_wallet_type` / `set_wallet_id` setters (empty string resets).
67+
- Coin-type registry extended with 34 SLIP-44 entries (etc, bch, eos, icp,
68+
ckb, zil, luna, dot, ksm, kava, fil, cspr, egld, scrt, flow, vet, rune,
69+
ftm, one, xtz, hype, hbar, move, stx, bera, xch, strk, mina, wax, kas,
70+
osmo, sei, inj, mon); the list is ordered ascending by index.
71+
- `error_code::invalid_value` — raised by the free-form component setters
72+
(ap/as/wt/wi) and the address constructor on NSS-corrupting values.
73+
74+
### Removed
75+
76+
- `error_code::missing_coin_type` — `ct` is never required anymore.
77+
78+
### Documentation / tests
79+
80+
- SPEC.md and README brought in lockstep with the Go reference (grammar 1.1,
81+
wallet domain, chain API, charset and value-validation rules, error table).
82+
- Test corpus mirrors the Go fixtures: new wallet-domain suite, strict
83+
chain-key suite, optional-ct semantics, updated hash reference vectors for
84+
the new canonical form, and the post-review hardening suite (canonical-only
85+
chain keys, ct spellings, printable-ASCII enforcement, setter validation,
86+
case-preservation, coin-registry spot checks); 139 test cases total.
87+
88+
## [1.0.0] — 2026-04-27
889

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

1396
### Added
1497

‎CMakeLists.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
cmake_minimum_required(VERSION 3.14)
22

33
project(mhda
4-
VERSION 0.1.0
4+
VERSION 1.1.0
55
DESCRIPTION "MultiChain Hierarchical Deterministic Address (MHDA) — C++ port of go-mhda"
66
LANGUAGES CXX
77
)

‎README.md‎

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,37 @@ MHDA is a URN-based descriptor for blockchain HD addresses, with
1212
[RFC 8141](https://datatracker.ietf.org/doc/rfc8141/) compatibility.
1313

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

1818
```
19-
urn:mhda:nt:btc:ct:0:ci:bitcoin:dt:bip86:dp:m/86'/0'/0'/0/0:af:bech32m:ap:bc1p
19+
urn:mhda:nt:bitcoin:ci:bitcoin:dt:bip86:dp:m/86'/0'/0'/0/0:af:bech32m:ap:bc1p
2020
```
2121

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

25+
The chain identity is the `(nt, ci)` pair; the SLIP-44 coin type is an
26+
optional `ct` metadata component (for HD addresses the coin already lives in
27+
the derivation path). The optional wallet domain (`wt`/`wi`) binds an address
28+
to a client type and a wallet instance:
29+
30+
```
31+
urn:mhda:nt:evm:ci:1:dt:bip44:dp:m/44'/60'/0'/0/0:wt:web3:wi:5f2a8c31
32+
```
33+
2534
## Status
2635

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

3847
## Building
3948

@@ -69,7 +78,7 @@ target_link_libraries(my_app PRIVATE mhda::mhda)
6978
include(FetchContent)
7079
FetchContent_Declare(mhda
7180
GIT_REPOSITORY https://github.com/censync/mhda.git
72-
GIT_TAG v1.0.0
81+
GIT_TAG v1.1.0
7382
)
7483
FetchContent_MakeAvailable(mhda)
7584
target_link_libraries(my_app PRIVATE mhda::mhda)
@@ -85,34 +94,40 @@ int main() {
8594
using namespace mhda;
8695

8796
// Lenient parsing: structural validation only.
88-
auto addr = parse_urn("urn:mhda:nt:evm:ct:60:ci:1");
97+
auto addr = parse_urn("urn:mhda:nt:evm:ci:1");
8998
std::cout << addr.get_chain().network().str() << "\n"; // evm
9099
std::cout << addr.resolved_algorithm().str() << "\n"; // secp256k1
91100
std::cout << addr.resolved_format().str() << "\n"; // hex
92101

93102
// Strict parsing also checks the (network, algorithm, format, derivation)
94103
// combination is in the known-good compatibility matrix.
95104
try {
96-
parse_urn_strict("urn:mhda:nt:evm:ct:60:ci:1:aa:ed25519");
105+
parse_urn_strict("urn:mhda:nt:evm:ci:1:aa:ed25519");
97106
} catch (const parse_error& e) {
98107
if (e.code() == error_code::incompatible) {
99108
std::cout << "evm + ed25519 rejected, as expected\n";
100109
}
101110
}
102111

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

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

112-
// The chain-domain triple (nt, ct, ci) is itself a parseable key.
113-
auto key = bip.get_chain().key(); // "nt:evm:ct:60:ci:1"
121+
// The chain identity (nt, ci) is itself a parseable key. Keys never carry
122+
// the optional ct metadata; a pre-1.1 key with ct fails loudly with
123+
// error_code::coin_type_in_chain_key.
124+
auto key = bip.get_chain().key(); // "nt:evm:ci:1"
114125
auto parsed = chain::from_key(key);
115126
(void)parsed;
127+
128+
// Optional wallet context: client type + wallet instance id.
129+
auto wallet = parse_urn("urn:mhda:nt:evm:ci:1:wt:web3:wi:5f2a8c31");
130+
std::cout << wallet.wallet_type() << " " << wallet.wallet_id() << "\n";
116131
}
117132
```
118133

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

123138
| Network | Example URN |
124139
|----------------|------------------------------------------------------------------------------------------------------|
125-
| Ethereum | `urn:mhda:nt:evm:ct:60:ci:1:dt:bip44:dp:m/44'/60'/0'/0/0` |
126-
| Bitcoin (BIP86)| `urn:mhda:nt:btc:ct:0:ci:bitcoin:dt:bip86:dp:m/86'/0'/0'/0/0:af:bech32m:ap:bc1p` |
127-
| Solana | `urn:mhda:nt:sol:ct:501:ci:mainnet:dt:slip10:dp:m/44'/501'/0'/0'` |
128-
| Stellar | `urn:mhda:nt:xlm:ct:148:ci:mainnet:dt:slip10:dp:m/44'/148'/0'` |
129-
| Sui (ed25519) | `urn:mhda:nt:sui:ct:784:ci:mainnet:dt:slip10:dp:m/44'/784'/0'/0'/0'` |
130-
| Cardano | `urn:mhda:nt:ada:ct:1815:ci:mainnet:dt:cip1852:dp:m/1852'/1815'/0'/0/0` |
131-
| Algorand | `urn:mhda:nt:algo:ct:283:ci:mainnet` (non-HD) |
132-
| TON | `urn:mhda:nt:ton:ct:607:ci:mainnet` (non-HD, friendly base64url default) |
133-
| Cosmos | `urn:mhda:nt:cosmos:ct:118:ci:cosmoshub:dt:cip11:dp:m/44'/118'/0'/0/0` |
134-
| EVM short form | `urn:mhda:nt:evm:ct:60:ci:1` (defaults: bip44, secp256k1, hex) |
140+
| Ethereum | `urn:mhda:nt:evm:ci:1:dt:bip44:dp:m/44'/60'/0'/0/0` |
141+
| Bitcoin (BIP86)| `urn:mhda:nt:bitcoin:ci:bitcoin:dt:bip86:dp:m/86'/0'/0'/0/0:af:bech32m:ap:bc1p` |
142+
| Solana | `urn:mhda:nt:solana:ci:mainnet:dt:slip10:dp:m/44'/501'/0'/0'` |
143+
| Stellar | `urn:mhda:nt:stellar:ci:mainnet:dt:slip10:dp:m/44'/148'/0'` |
144+
| Sui (ed25519) | `urn:mhda:nt:sui:ci:mainnet:dt:slip10:dp:m/44'/784'/0'/0'/0'` |
145+
| Cardano | `urn:mhda:nt:cardano:ci:mainnet:dt:cip1852:dp:m/1852'/1815'/0'/0/0` |
146+
| Algorand | `urn:mhda:nt:algorand:ci:mainnet` (non-HD) |
147+
| TON | `urn:mhda:nt:ton:ci:mainnet` (non-HD, friendly base64url default) |
148+
| Cosmos | `urn:mhda:nt:cosmos:ci:cosmoshub:dt:cip11:dp:m/44'/118'/0'/0/0` |
149+
| EVM short form | `urn:mhda:nt:evm:ci:1` (defaults: secp256k1, hex) |
150+
| With metadata | `urn:mhda:nt:evm:ci:1:ct:60` (optional SLIP-44 annotation) |
151+
| Wallet-bound | `urn:mhda:nt:ton:ci:mainnet:wt:tonconnect:wi:c0a8f2d4-3b6e-4a51-9c7d-2f8e1a0b5c93` |
135152

136153
## API mapping (Go → C++)
137154

@@ -141,10 +158,14 @@ A runnable version is in [`examples/basic.cpp`](./examples/basic.cpp).
141158
| `mhda.ParseURNStrict` | `mhda::parse_urn_strict` |
142159
| `mhda.ParseNSS` | `mhda::parse_nss` |
143160
| `mhda.ChainFromKey` / `FromNSS` | `mhda::chain::from_key` / `from_nss` |
144-
| `mhda.NewChain(...)` | `mhda::chain{...}` |
161+
| `mhda.NewChain(nt, ci)` | `mhda::chain{nt, ci}` |
162+
| `Chain.SetCoinType` / `ClearCoinType` | `chain::set_coin` / `chain::clear_coin` |
163+
| `Chain.CoinType` + `HasCoinType` | `chain::coin` (`std::optional<coin_type>`) |
145164
| `mhda.ParseDerivationPath` | `mhda::derivation_path::parse` |
146165
| `mhda.NewDerivationPathFromLevels`| `mhda::derivation_path::from_levels` |
147166
| `Address.String()` / `NSS()` | `address::str` / `address::nss` |
167+
| `Address.WalletType` / `WalletId` | `address::wallet_type` / `wallet_id` |
168+
| `Address.SetWalletType` / `SetWalletId` | `address::set_wallet_type` / `set_wallet_id` |
148169
| `Address.MarshalText` | `address::marshal_text` |
149170
| `Address.UnmarshalText` | `address::unmarshal_text` |
150171
| `Address.Hash` / `Hash256` | `address::hash` / `hash256` |
@@ -160,9 +181,11 @@ Sentinel constants:
160181
| `ErrInvalidNSS` | `error_code::invalid_nss` |
161182
| `ErrMissingNetworkType` | `error_code::missing_network_type` |
162183
| `ErrInvalidNetworkType` | `error_code::invalid_network_type` |
163-
| `ErrMissingCoinType` | `error_code::missing_coin_type` |
164184
| `ErrInvalidCoinType` | `error_code::invalid_coin_type` |
165185
| `ErrMissingChainID` | `error_code::missing_chain_id` |
186+
| `ErrCoinTypeInChainKey` | `error_code::coin_type_in_chain_key` |
187+
| `ErrInvalidChainKey` | `error_code::invalid_chain_key` |
188+
| `ErrInvalidValue` | `error_code::invalid_value` |
166189
| `ErrInvalidDerivationType` | `error_code::invalid_derivation_type` |
167190
| `ErrInvalidDerivationPath` | `error_code::invalid_derivation_path` |
168191
| `ErrInvalidAlgorithm` | `error_code::invalid_algorithm` |
@@ -191,7 +214,7 @@ Mirrors the [SPEC §8](./SPEC.md#8-concurrency) contract.
191214

192215
## Testing & validation
193216

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

0 commit comments

Comments
 (0)