Repository navigation
mhda v1.2.0
A behavioral mirror of go-mhda v1.2.0. The URN format stays bit-for-bit compatible between the two implementations: both suites run the same conformance tables, and a differential run gave byte-identical results for 500 000 mutated URNs (lenient, strict, NSS and chain key) and 600 000 derivation paths. This release changes the ABI of derivation_path and address; rebuild dependants.
URNs and values accepted by 1.1 are refused now: see Changed. Code that
builds addresses gets stricter constructors and setters. Mirrors go-mhda
1.2.0.
Changed
- A derivation-path level index of 2^31 or more is refused with
parse_error(invalid_derivation_path), at every level of every
derivation type (bip32,bip44,bip49,bip54,bip74,bip84,
bip86,cip11,cip1852,zip32,slip10), hardened or not, by
parse_urn,parse_urn_strict,parse_nss,derivation_path::parse
andvalidate_derivation_path. 1.1 accepted any value up to 2^32-1. A
BIP-32 child number keeps the hardened flag in its top bit (n'is child
number 2^31+n), so such an index aliased another key: a consumer
computinghardened ? 0x80000000 | index : indexderives the key of
m/44'/60'/0'/0/0form/44'/60'/2147483648'/0/0, and an unhardened
m/44'/60'/0'/0/2147483648asks for a hardened child number through the
public-key formula. No longer parse: anydpwith a level of
2147483648to4294967295, with or without a hardened marker, e.g.
urn:mhda:nt:evm:ci:1:dt:bip44:dp:m/44'/60'/2147483648'/0/0and
urn:mhda:nt:evm:ci:1:dt:bip44:dp:m/44'/60'/0'/0/4294967295. The largest
index is2147483647. Thectcomponent is not a path level and keeps
its 32-bit range. - Fixed path levels are spelled exactly, as in go-mhda. The purpose,
the fixed coin ofcip11/cip1852/zip32and the0/1charge of
bip32and the BIP-44 family are literals in the Go reference's grammar;
the port compared their values only and so accepted a leading zero that
Go refuses (m/044'/60'/0'/0/0,m/44'/60'/0'/00/0,
m/1852'/01815'/0'/0/0). These now throw
parse_error(invalid_derivation_path). Leading zeros in a variable level
(m/44'/060'/0'/0/0) are still accepted, as in Go, and dropped in the
canonical form. - Programmatic values are validated like parsed input. The network
type, the chain id and the derivation type are written verbatim into
every URN, and nothing checked them when set in code: a chain id
1:dt:bip44:dp:m/44'/60'/0'/0/666on a root address produced a URN that
re-parsed as a bip44 path. Thechainconstructor,set_networkand
set_chain_idnow require a registered network type and a non-empty
chain id of printable ASCII without:,?or#(ASCII-trimmed) and
throwparse_error(invalid_network_type,missing_chain_id,
invalid_value) otherwise, leaving the chain unchanged. The
derivation_pathconstructor,from_levelsandset_typethrow
parse_error(invalid_derivation_type)on an unregistered type. Mirrors
go-mhda. - A derivation path without a derivation type, or under
dt:root, is
refused withparse_error(invalid_derivation_path). Both used to be
dropped silently:urn:mhda:nt:evm:ci:1:dp:m/44'/60'/0'/0/0and
urn:mhda:nt:evm:ci:1:dt:root:dp:m/0parsed as the root address
urn:mhda:nt:evm:ci:1, naming the root key instead of the path they
spell out.address::set_derivation_pathon a root address accepts only
an empty path. - The NSS is parsed strictly as
key:valuepairs. An unknown key was
skipped as a single token, so its value was read as the next key and a
key with the wrong case vanished:...:ci:1:DT:bip44:DP:m/44'/60'/0'/0/5
parsed as the root addressurn:mhda:nt:evm:ci:1, and
...:ci:1:ext:wi:dt:bip44:...readdtas the wallet id. An unknown
component is now skipped together with its value; a key that differs from
a known key only by case, a dangling token, a trailing:and an empty
key throwparse_error(invalid_nss).chain::from_keyreports a
dangling token asinvalid_chain_key, as before. parse_nss,chain::from_nssandchain::from_keyrefuse?and
#withparse_error(invalid_nss).parse_urnstrips the RFC 8141
r/q/f components before parsing, but an NSS given on its own kept the
byte in a value, so the URN emitted from it was truncated on the next
parse (nt:evm:ci:1:x#y:zwas accepted). The NSS fuzz test now checks
that the emitted URN re-parses to itself.- A
slip10path has at most 255 levels; a deeper one throws
parse_error(invalid_derivation_path). BIP-32 serialises a key's depth
in one byte, so no wallet can represent a deeper key. - Strict validation refuses curve, purpose and format combinations no
wallet can derive.validatechecked the algorithm, the format and the
derivation type each on its own, so it accepted SLIP-10 ed25519 paths
with soft levels (nt:solana:...:dp:m/44'/501'/0/0; ed25519 has no soft
derivation),bip44with ed25519 on XRPL, NEAR and Aptos, Suibip54
with ed25519 or secp256r1, a Bitcoinbip84path withaf:p2pkh, and
Cosmosbip44with coin118', which is thecip11path again.
parse_urn_strict/validatenow throwparse_error(incompatible)
for: an ed25519 path with an unhardened level (exceptcip1852, which
is BIP32-Ed25519); a derivation type with another curve than its own on
Sui (slip10ed25519,bip54secp256k1,bip74secp256r1), Aptos and
NEAR (slip10ed25519,bip44secp256k1); an explicit Bitcoin format
that does not match the purpose (bip44p2pkh,bip49p2sh,bip84
p2wpkh or bech32,bip86p2tr or bech32m); and Cosmosbip44with coin
118'. A Bitcoin URN withoutafstays valid; SPEC.md no longer claims
strict mode requires it. Lenient parsing is unchanged. Mirrors go-mhda. - No whitespace inside the NSS. Each value was trimmed, so
urn:mhda:nt:evm:ci: 1 :dt: bip44 :...parsed like the URN without the
spaces, against SPEC §6.1. ASCII whitespace is now trimmed only around the
whole URN, around an NSS or chain key parsed on its own, and before a
stripped r/q/f component (ci:0 #fragstill parses); around a key or
value it throwsparse_error(invalid_nss). Mirrors go-mhda. - NSS bytes follow RFC 3986. Keys and values accepted any printable
ASCII, so",<,>,\,^,`,{,|,},[,]and a
raw%passed and were emitted in URNs that RFC 8141 does not allow. A
key or value is now made of letters, digits and-._~!$&'()*+,;=@/
only (RFC 3986 pchar and/, without:and%); anything else throws
invalid_nsswhen parsed andinvalid_valuefrom the setters and the
chainconstructor.%is refused because percent-encoding is not
supported and%41would be a second spelling ofA. Mirrors go-mhda.
Fixed
charge_typeis 32 bits wide (wasstd::uint8_t). The CIP-11 charge
and the CIP-1852 role accept any level index, but the parsed value was
truncated to a byte:m/1852'/1815'/0'/256/0parsed as role 0, so it
named the role-0 key, re-serialised asm/1852'/1815'/0'/0/0, and
levels()returned the truncated value. The full value is now kept in
charge(),levels()andstr(). This changes the ABI of
derivation_path(its constructor,charge()and its layout); rebuild
dependants.- Static initialisation and destruction order. The named constants
(network_type::bitcoin,algorithm::secp256k1,format::hex,
derivation_type::root, ...) were defined in the library's.cppfiles
and built at start-up in an unspecified order relative to the consumer's
globals, and the lookup tables copied them on first use. A consumer
global that used one (a built-in chain table, a URN validated up front)
could run first: it saw empty values and left the tables wrong for the
whole process (parse_urn_strictthen refused every network as
unknown). A global destructor that parsed at exit read destroyed tables
and crashed. The constants are now inline variables defined in the
public headers, so they are initialised before any global defined after
the include, and the lookup tables are allocated once and never
destroyed. - No stale or mixed derivation state.
set_typeand
address::set_derivation_typechanged the type and kept the old path, so
a bip44 address switched to zip32 serialised as
dt:zip32:dp:m/32'/133'/3'/9, a valid URN naming a key nobody gave, and
a SLIP-10 path parsed after it kept the BIP-44 coin, account and charge.
set_derivation_typefollowed by a failingset_derivation_pathleft
that mixed address behind, and abad_allocinsideparse_pathleft
str()andlevels()disagreeing. A new type now clears the path;
until one is set the URN carriesdtwithoutdp(and does not parse),
andvalidate/marshal_textthrowinvalid_derivation_path.
parse_pathparses into a fresh path and replaces the old one only on
success. The newaddress::set_derivation(dt, dp)sets both at once and
changes nothing on error; the URN parser uses it. Mirrors go-mhda. levels()andstr()cannot disagree.from_levelskept any
levels it was given whilestr()printed the type's template: a BIP-44
path built from49'/60'/0/0/0printedm/44'/60'/0'/0/0but returned
purpose 49' and an unhardened account fromlevels(), which is what a
wallet derives from. Too few levels (or none for SLIP-10) or an index of
2^31 produced a URN that does not parse, and the five-argument
constructor accepted a charge of 7 or an account of 2^31. Both now
return exactly the pathparse()gives back for their ownstr(), and
throwparse_error(invalid_derivation_path)otherwise. Two boundary
tests that pinned the old results now expect the refusal. Mirrors
go-mhda.- Error messages escape the input they quote.
what()copied the
caller's bytes verbatim, so a newline or an ANSI escape in a
client-supplied value (set_wallet_id("x\n[INFO] ok")) forged or
coloured lines in any log that recorded the exception. Quoted input is
now escaped like Go's%q(\",\\,\n,\r,\t,\xNNfor
other control and non-ASCII bytes), sowhat()is one line of printable
ASCII. - The headers compile under
<windows.h>. Itsnearmacro (empty, from
minwindef.h) turnedcoins::nearinto a syntax error in any
translation unit that included<windows.h>first.coin_type.hppnow
sets the macro aside for its list and restores it, and the new alias
coins::near_protocol(afternetwork_type::near_protocol) stays usable
while the macro is in effect. - Embedding mhda leaves the parent build alone. Added with
add_subdirectoryor FetchContent, mhda forcedCMAKE_BUILD_TYPE=Release
into the cache of a parent that had none (so the parent's own code was
built with-O3 -DNDEBUGand its asserts off), built its tests and
examples into the parent and added its install rules to the parent's
install. These now apply only to a top-level build;MHDA_BUILD_TESTS,
MHDA_BUILD_EXAMPLESand the newMHDA_INSTALLdefault to it. BUILD_SHARED_LIBS=ONproduces a usable library. The hidden-symbol
preset, without export macros, leftlibmhda.sowith no mhda symbols
and every consumer failed to link. A shared mhda now exports its symbols
(WINDOWS_EXPORT_ALL_SYMBOLSfor a DLL); a static one keeps them hidden.- The installed package is complete.
mhda::mhdanow requires
cxx_std_17publicly, so a consumer asking for C++14 is raised to C++17
instead of failing onstd::optional, and the install include path
followsCMAKE_INSTALL_INCLUDEDIR.
Documentation
- SPEC.md states that an explicit
dt:rootis folded away (root is the
default and has no path, so a root address has one canonical form and
one hash); the Algorand and TON notes no longer calldt:rootthe
canonical form. It also lists ZIP-32 as a known limitation:zip32
parses, but no network registers it, so strict parsing refuses it. Both
behaviours are unchanged and now pinned by tests. - SPEC.md notes that an EVM chain id is opaque:
ci:1,ci:0x1and
ci:01are three chain keys for one chain, and a producer must keep to
one spelling.
Tests
tests/data/dp_conformance.txt: a derivation-path conformance table
shared verbatim with go-mhda (testdata/dp_conformance.txt) and run by
both suites, standalone and inside a URN, so a path one implementation
refuses, the other refuses too. The boundary test and the fuzz seed that
pinned4294967295now sit at the new bound.tests/data/urn_conformance.txt: a URN-level table shared verbatim with
go-mhda, run throughparse_urn/parse_nsswith the expected error
code, for the parser changes above.tests/test_static_init.cppuses the library from a global constructor
and a global destructor,tests/test_state.cppcovers construction,
re-parsing and type changes, andtests/test_windows_macros.cppcompiles
the headers under the<windows.h>macros; hash digests are pinned at
the SHA block and padding boundaries (55 to 1000 bytes).- CI adds an ASan/UBSan job and a
-Werrorjob with the README's warning
set, a shared-library job, and a packaging job that builds the
find_packageandadd_subdirectoryconsumers intests/cmake/; it
runs with read-only permissions. 160 test cases total.