All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
The project ships specifications (docs/standards/MXD-*) and a reference C
implementation. Wire-format and consensus rules follow their own per-spec
versioning (see each MXD-XX document's change log). Repository-level
versions track the C library and tooling as a whole.
letsgo-wsl.bat— one-click Windows launcher that auto-installs WSL2 + Ubuntu-22.04 if missing (first run requires admin + reboot), then clones the repo inside WSL and runs./letsgo testnet|mainnet. On a machine that already has WSL2, total bring-up is ~30 seconds — same as the native Linux path.- README "Running on Windows" section — documents the WSL2 and MSYS2 paths side-by-side with a clear recommendation (WSL2 for almost everyone, MSYS2 only if WSL is unavailable).
-
integration-testjob finally green. Three compounding bugs found and fixed via the v0.2.5 → v0.2.7 release smoke arc:- The job rebuilt the full Dockerfile from scratch on every run
(~5+ min) because
docker-build'smxdlib:latestimage lived on a different runner. Nowdocker-buildsaves the image as a workflow artifact andintegration-testdownloads +docker loads it (~30 s instead of ~5 min). - The container CMD didn't pass
--http-api 8080, sohttp.require_auth=1+ emptyapi_tokencaused the HTTP API to refuse to start. The compose now overrides the CMD to enable HTTP API explicitly (same as whatletsgodoes on testnet). - Host curl to
localhost:8080/statusfailed with CURLE_RECV_ERROR (exit 56) due to an IPv6/IPv4 bridge quirk on GitHub Actions runners. The verify step now usesdocker execto query/statusfrom inside each container — same path as the healthcheck, no bridge involved.
- The job rebuilt the full Dockerfile from scratch on every run
(~5+ min) because
-
New
docker-compose.ci.yml(mxdlib-private, not shipped via the public manifest). Minimal 2-node compose usingimage: mxdlib:latestwith nobuild:directive, no monitoring stack, tight healthchecks asserting/statusreturns JSON with a"height"field. Operator- facingdocker-compose.test.ymlremains in the public repo for local "spin up the full stack with monitoring" use cases.
- No functional changes to libmxd, chain consensus, or wire format. Same binary as v0.2.7.
- The Windows launcher works against this release tag and any
future tag —
letsgo-wsl.batitself doesn't hard-code a version; it delegates to the Linuxletsgoscript which resolves the latest release dynamically.
The end-to-end smoke of v0.2.5 on a fresh Ubuntu 24.04 VM crashed on the first block store with:
./db/c.cc:532: bool SaveError(char**, const rocksdb::Status&):
Assertion `errptr != nullptr' failed.
v0.2.6 fixed the symptom at the distribution layer (pin build to
22.04 + bundle librocksdb 6.11). v0.2.7 fixes the underlying chain-
code bug so letsgo --from-source paths on any rocksdb 7.x/8.x
distro also work cleanly.
src/mxd_blockchain_db.c— 7 sites converted fromrocksdb_put/get(..., NULL)to the standardchar *err = NULL; rocksdb_put(..., &err); if (err) { log; free; }pattern. All are best-effort housekeeping ops around thecurrent_heightandlatest_stored_heightkeys:store_block_unconditional—latest_stored_heightputstore_block_unconditional— gap-fill probe (get)store_block_unconditional— gap-fillcurrent_heightputreorg_to_candidate— post-rollbackcurrent_heightputmxd_block_exists_at_height— existence probe (get)mxd_advance_height_pointer— probe (get)mxd_advance_height_pointer—current_heightput Behavior on success path is unchanged. Behavior on failure path goes from "silent error swallow" (rocksdb 6.x) or "process abort" (rocksdb 8.x) to "log WARN + continue".
- CI green on both gcc + clang (37/37 tests, sanitizers clean).
- Coordinated deploy on 6-node testnet, 8-minute soak: NRestarts=0, consensus tick normal, zero rocksdb assertion lines in journals.
- Mainnet validators still run the v0.2.0 binary (no symptom there — rocksdb 6.11 tolerates the NULL). Mainnet rollout of this fix will happen at the next coordinated v0.3.x release with a staggered/canary deploy pattern.
- Other rocksdb-heavy files (
mxd_utxo.c,mxd_contracts_db.c,mxd_transaction.c) already used the correct&errpattern; no changes needed.
End-to-end smoke test of letsgo testnet on a clean Ubuntu 24.04 VM
exposed two distribution bugs:
letsgoapt prereq was hardcodedlibrocksdb6.11(the 22.04 package name). Ubuntu 24.04 shipslibrocksdb8.9, Debian 12 shipslibrocksdb7.8, etc. Apt install failed withUnable to locate package librocksdb6.11.mxd_nodecrashed with a rocksdb assertion (SaveError: errptr != nullptr) on Ubuntu 24.04. libmxd's chain code passesNULLfor the rocksdbchar** errptrparameter, which rocksdb 6.x tolerates (mainnet runs 22.04/6.11) but rocksdb 8.x asserts-aborts.
v0.2.6 fixes the distribution surface so both problems go away without touching the chain code. The libmxd-side NULL-errptr issue is real and worth fixing eventually, but it doesn't affect operators using these bundles.
- Build matrix pinned to
ubuntu-22.04(andubuntu-22.04-arm). glibc 2.35 baseline — forward-compatible with 24.04, Debian 12, Fedora 36+, Rocky 9+, Arch (rolling), Alpine 3.18+ (with gcompat). Also matches the mainnet validators' OS so the shipped binary is byte-compatible with production. - Bundle is now self-contained via recursive
lddtraversal in theStage release bundlestep. Every dynamic dependency ofmxd_nodeandlibmxd.sois bundled intolib/— includinglibssl,libcrypto,libsodium,librocksdb(6.11 matching mainnet, no 8.x assert issue),libcurland its sub-deps (libnghttp2,libidn2,librtmp,libssh,libpsl,libgssapi_krb5,libldap,liblber),libminiupnpc,libmicrohttpd,libgmp,libcjson,libzstd, on top of the source-builtlibuv,libuvwasi,libm3,liboqs. Glibc components (libc,libm,libpthread,libdl,librt,ld-linux, vdso) are intentionally excluded — they're kernel- coupled and must come from the host. - SONAME symlinks reconstructed in the bundle.
cp -Lflattens the symlink chain, so the bundle step rebuildslibfoo.so → libfoo.so.X → libfoo.so.X.Y.Zso loaders find each lib by its SONAME at runtime. letsgono longer runsapt-get install. The bundle has every .so it needs; the wrapper'sLD_LIBRARY_PATH=bundle/libwins over the system's/usr/lib/.../. Removes the entire class of "apt package name doesn't exist on this distro" bugs.- Bundle README simplified to a 2-step quick-start (no apt line).
- Tarball size grows from ~3 MB → estimated ~30–50 MB compressed (~15 extra .so files). Trade-off accepted for distro portability.
- Operators on Ubuntu 22.04 will see the bundled libs slightly duplicated against system libs, but the bundle ones win — no conflict, just a few MB of disk.
- Mainnet validators continue to use the source-built path (via
install_dependencies.sh+letsgo --from-sourceor direct cmake) and are unaffected by the bundle changes.
docker-buildfailed on v0.2.4 at "Generate SBOM for source" withreceived HTTP status=404 for url='https://get.anchore.io/syft/v0.103.1/install.sh'. The pinnedanchore/sbom-action@v0.15.8is bundled with a syft version that Anchore has since deleted/moved upstream. Bumped all three usage sites (source SBOM indocker-build, image SBOM indocker-build, image SBOM indocker-publish-sign) to@v0(the moving major-tag pointer Anchore maintains as stable).- SBOM steps now non-blocking via
continue-on-error: true. CycloneDX SBOMs are supply-chain-transparency metadata, not gating on Docker image functionality — if the SBOM action breaks again upstream, Docker push should still complete. The Upload SBOM step is gated onhashFiles(...)so it skips cleanly when the SBOM file wasn't produced.
- v0.2.4 binary tarballs landed correctly and are still the
recommended
letsgo testnettarget until v0.2.5 supersedes them. - Docker image was never produced for v0.2.2 / v0.2.3 / v0.2.4. v0.2.5
should be the first version with
ghcr.io/alanruno/mxd:vX.Y.Zlive.
v0.2.3's tag CI got further than v0.2.2's — build-binary-release
produced both x86_64 and arm64 tarballs as workflow artifacts — but
two issues then prevented the GitHub Release from being created:
- GHCR push failed with "repository name must be lowercase" —
the workflow used
${{ github.repository }}(=AlanRuno/mxd) directly in the image tag, but OCI registries reject uppercase. Added alcstep that lowercases the repo once and reuses the output in the build-push + cosign + SBOM steps. publish-releasewas skipped because itneeds: [build-binary-release, docker-publish-sign]and Docker failed. Decoupled: nowneeds: [build-binary-release]only. Docker publishing is independent of the GitHub Release creation; if GHCR has issues the tarballs still land soletsgoprebuilt path works.integration-testfailed withdocker-compose.test.yml: no such file or directory— the file lives in mxdlib root but wasn't in the public manifest. Now added.
- Image will publish as
ghcr.io/alanruno/mxd:vX.Y.Z(lowercase). - v0.2.3 tarballs do exist as workflow artifacts on
https://github.com/AlanRuno/mxd/actions but were never promoted
to a GitHub Release. v0.2.4 should be the first to land actual
Release assets that
letsgocan download.
Patch on top of v0.2.2 to make the new prebuilt-release workflow
actually produce artifacts. v0.2.2 shipped but its tag-triggered CI
failed before reaching build-binary-release, so no tarballs landed
on the GitHub Release. v0.2.3 ships the same code as v0.2.2 plus the
two release-pipeline fixes below.
- Executable bit on shell scripts —
install_dependencies*.sh,letsgo, andswitch_network.shwere stored in git with mode100644(non-executable) because the Windows-sidecp -rduring release-tree sync silently strips the exec bit (core.filemode = falseon Windows git installs). The Linux CI runner then refused to run them with exit 126 ("Permission denied"). Now stamped to100755in the git tree viagit update-index --chmod=+x. Bug present since v0.1.0; CI on public has been red on every release tag until now. wasm3.pc.inmissing from public release tree —install_dependencies_linux.shreferences${SCRIPT_DIR}/wasm3.pc.in(a pkg-config template at repo root) before compiling wasm3 from source. The manifest didn't include it, so even after the exec-bit fix the wasm3 build step failed withcp: cannot stat .../wasm3.pc.in. Now listed inscripts/release/mxd-public-manifest.txt.RELEASE_PROCESS.md— codified the post-syncgit update-index --chmod=+xstep for the six known-executable scripts, so this doesn't recur on the next release.
- v0.2.2 tarballs do NOT exist on the GitHub Release;
letsgo testnetwould have returned a 404. v0.2.3 is the first version that should actually produce downloadable artifacts when the tag CI completes.
Operator onboarding goes from ~10-20 min ("compile everything from source") to ~30 seconds ("download + extract + run") for the common case. No functional changes to libmxd, chain consensus, or wire format.
.github/workflows/ci.yml— two new jobs on tag pushes:build-binary-release— matrix-builds release tarballs natively onubuntu-24.04(x86_64) andubuntu-24.04-arm(arm64). Each bundle containsbin/mxd_node+bin/mxd_node_runwrapper +lib/{libmxd, libuv, libuvwasi, libm3, liboqs}.so*+ bundledconfig/default_config.json+ the README + apt prereq line for runtime-only system libs. Each tarball is SHA256-summed and cosign-signed (keyless / OIDC).publish-release— collects the per-arch bundles, generates a combinedSHA256SUMS, extracts release notes fromCHANGELOG.md, and usessoftprops/action-gh-release@v2to create the GitHub Release with all assets attached.
- Multi-arch Docker image — the existing
docker-publish-signjob now buildslinux/amd64,linux/arm64viabuildx+ QEMU and pushes a single multi-arch manifest toghcr.io/AlanRuno/mxd:vX.Y.Z+:latest. Image is cosign-signed (keyless) and ships with a CycloneDX SBOM. letsgo— rewritten to be prebuilt-first with three modes:- default:
curlthe latest release tarball for$(uname -m), verify SHA256, extract under./mxd-prebuilt/, run viamxd_node_run(which setsLD_LIBRARY_PATHso the bundled libuv/libuvwasi/libm3/liboqs are found without a global install). Detects + auto-installs missing runtime system libs (libssl3,libsodium23,librocksdb6.11, etc.) via apt on Debian/Ubuntu. --docker:docker pull ghcr.io/AlanRuno/mxd:latestand run with the chain data dir bind-mounted.--from-source: legacy path, kept verbatim for auditors and contributors who want to compile.- Environment overrides:
MXD_RELEASE_TAG(pin a version),MXD_INSTALL_DIR(where to extract),MXD_REPO(release source, defaultAlanRuno/mxd).
- default:
letsgo.bat— Windows wrapper now forwards extra args (--docker,--from-source,--reset) through to the bash script under MSYS2.
- The mxdlib working tree's mainnet
v8_activation_height = 100(operator deploy value, commitc38110b) is not shipped to public; the staged release tree keeps theUINT32_MAXplaceholder so downstream operators must still make a deliberate choice. - The
release-ymlchicken-and-egg: this release is itself the first one to actually exercise the new workflow. The v0.2.2 tag push will produce the initial set of binary tarballs + Docker manifest.
No functional changes to libmxd, chain consensus, or wire format. All diffs are CI/test maintenance against the v0.2.0 baseline.
include/common/mxd_metrics_types.h— bumpedmxd_node_stake_t::node_idfromchar[64]tochar[65]. The field holds a 64-hex-char encoding of the v6 addr32 plus a null terminator (65 bytes); the previous[64]size caused both thesnprintfloop and the explicitnode_id[64] = '\0'inmxd_rsc.c::mxd_apply_membership_deltasto write one byte past the end, clobbering the low byte of the adjacentstake_amount. Benign in practice (the field wasmemset-zeroed and reassigned moments later bymxd_get_balance), but UB and flagged bycppcheckasarrayIndexOutOfBounds. The in-memory struct layout shift is safe because the struct is never serialized to disk or wire.src/blockchain/mxd_fork_choice.c— removed a duplicateif (cur_h == 0) break;check infind_common_ancestor's walk-back loop. The first check at line 133 already breaks early andcur_his not modified between the two checks, making the second one unreachable.include/mxd_blockchain_sync.h— declaredmxd_sign_and_broadcast_block()in the public header. It was defined inmxd_blockchain_sync.cwith a file-local forward declaration only, and called from three sites inmxd_validation_handler.cwithout any declaration in scope. Clang's-Werror=implicit-function-declarationcaught this; gcc had been silently letting it pass.
contracts/— bumpedaxiosfrom^1.13.5to^1.15.5and regeneratedpackage-lock.jsonto absorb security patches for transitive Hardhat dev dependencies. Cleared the critical Handlebars RCE (GHSA-…) and five high-severity issues against axios (prototype pollution, header injection, SSRF). Remaining Dependabot alerts are all transitive dev-dependencies gated behind a futurehardhat-toolbox v4 → v6major bump and don't affect production validators (which never link any npm code).
tests/test_smart_contracts.c— passed the requireddeployer[20]argument tomxd_deploy_contractat all five call sites (was using the legacy 3-arg signature), added<sys/stat.h>+<unistd.h>andmkdir(data_dir, 0755)in the test-setup helper somxd_contracts_db_init()can create its RocksDB instance in CI's working directory, and correctedmxd_init_contracts() == -1→== 0in the disabled-by-default test to match the source comment "FIX: Disabled is a valid state, not an error".tests/test_blockchain.c::test_block_validation— removed the positivemxd_validate_block(&block) == 0assertion. The full validation path needs whole-chain context (prev block in RocksDB, per-height required protocol version, computed contracts/scores roots) that can't reasonably be set up in a synthetic-block unit test. The negative path (block->version=0→ rejected) still runs.tests/test_enhanced_consensus.c—mxd_init_node_metricsnow intentionally setslast_update = mxd_now_ms()so newly- joined validators appear active immediately; updated the test to assert!= 0instead of the stale== 0. Also bumped the "invalid response time" test value past both the header constant (5000) and the in-source override (mxd_rsc.credefinesMXD_MAX_RESPONSE_TIMEto 120000).tests/test_validator_management.c— converted all remaining 20-byte stack-array addresses to v6 addr32 (32-byte), derived the join-request address from the test pubkey viamxd_derive_addressso the addr↔pubkey binding check insidemxd_validate_join_requestactually passes, extended the liveness-tracking loop to enough heights for at least one validator to crossMXD_MAX_CONSECUTIVE_MISSESwith three validators in round-robin, freed therequestsarray allocated bymxd_get_pending_join_requests(LeakSanitizer caught the 7280-byte leak), and added the missing<stdlib.h>forcalloc/free.src/mxd_wasm_validator.c— added// cppcheck-suppress oppositeInnerConditioncomment to silence a false-positive on the defensiveif (ptr >= body_end) break;insidewhile (ptr < body_end). The check is kept as defense against future loop-body edits that advanceptrmid-iteration.
.github/workflows/ci.yml— added--inline-supprto thecppcheckinvocation so the new in-sourcecppcheck-suppresscomment is honoured, and excluded thenode_network_testsintegration test (which needs a live daemon) from the standardctest --output-on-failurerun.
- CI now passes 100% on both gcc and clang lanes across all 37 tests, with valgrind (gcc) and address/leak sanitizers (clang) clean.
- Protocol version 8 with per-network
v8_activation_heightgate. Mainnet defaults toUINT32_MAX(no activation); operators MUST set a coordinated future activation height before deploying the v8 binary. - Peer-driven validator EVICT — an active validator that observes a
peer's on-chain balance has fallen below the §4.4 stake threshold
signs an MXD-VAL-V1 EVICT (84-byte canonical bytes: domain || 0x02 ||
target_addr32 || evictor_addr32 || timestamp_be) and broadcasts it on
the new
MXD_MSG_VALIDATOR_EVICT_REQUEST(P2P type 20) gossip channel. The 173-byte Ed25519 / ~7.3 KB Dilithium5 wire format carries the evictor's pubkey inline for self-contained verifiability. - Auto-EVICT trigger integrated into the existing
auto_join_thread_func(no new thread). When the local node is in the rapid_table AND v8 is active, every 60-second poll scans OTHER validators for balances below the threshold and signs an EVICT for each. - Block field
rapid_eviction_entries[](v8+ blocks). Serialized alongsiderapid_membership_entries[]; pre-v8 blocks omit the field for backwards compatibility. - Validator membership-management apply paths lifted from the
formerly-dead
mxd_process_validation_chaininto the three real block-storage paths (mxd_blockchain_sync.cunsolicited + bulk-sync,mxd_rsc.cproposer's own block). Both JOIN and EVICT now take effect on every peer's in-memory rapid_table immediately after block finalize, with no process restart required. - MXD-CONS-01 v1.2.0 — promotes
op_type=0x02EVICT from Reserved to Active. Adds §4.0.2 (84-byte canonical bytes), §4.2.2 (verification + 5-min grace period for newly-joined validators), §4.3.1 (173-byte gossip wire format), §4.4.2 (auto-trigger integration), §4.5.2 (proposer drain with dedup-by-target and 4-validator floor for K=⅔ quorum preservation), §4.6.2 + §4.6.3 (eviction apply + ordering rule — membership first, scoring middle, evictions last), §4.7.2 (storage-path EVICT signature verification). MXD-CONS-01-validator-management-test-vectors.json— deterministic test vectors covering JOIN positive, EXIT (deprecated) reference, EVICT positive (84-byte canonical + 173-byte wire payload), and cross-replay negatives proving theop_typebyte defends against JOIN↔EXIT and JOIN↔EVICT signature reuse.vendor/ml-dsa-pqcrystals/— the FIPS 204 ML-DSA-87 reference implementation that the chain links against. Previously missing from v0.1.0 despite being referenced byCMakeLists.txt; v0.2.0 includes it so the build is self-contained.- Release tooling documented in the repository README.
- Use-after-free of
g_active_val_blockin the v5 skip-timeout path. The proposer's validation-tracking globals (g_active_val_ctx,g_active_val_block,g_active_val_table) were not cleared beforemxd_stop_block_proposal()freed the block, allowing the next consensus tick's skip-timeout scan to read freed memory. Latent v5-era race that EVICT activity exposed by extending the time between block-close and first signature. - JOIN/EVICT request pool clearing after block-apply.
mxd_clear_processed_requestsexisted in source since the original Phase 1 plumbing but (a) had no EVICT logic and (b) was never called from any production path. Without it, the per-(evictor, target) dedup inmxd_submit_validator_evict_with_pubkeyretained stale pool entries after the proposer drained them, silently no-op'ing every subsequent EVICT for the same target. v0.2.0 extends the clear-helper to cover EVICT and invokes it at all three apply call sites, so JOIN↔EVICT cycles for the same validator address can repeat indefinitely without process restart.
mxd_node_stake_t::added_at_block_time_ms(new field) tracks when each validator was last added to the rapid_table. Input to the §4.2.2 grace-period rule on EVICT requests.- Dockerfile +
tools/mxd_sign.cbuild comments use/opt/mxdpaths consistently (previously referenced/opt/mxdlib, the internal working-tree path).
- Two consecutive smoke batteries on a 6-node testnet (~50 min each)
exercised the full JOIN → idle-soak → EVICT → idle-soak → liveness
cycle. Zero crashes, repeatable JOIN↔EVICT cycles for the same
validator address without restart, memory flat within ±0.3 MB over
30-minute idle windows.
scripts/release/smoke_evict_cycle.shin the private working tree is the re-runnable regression suite. - libmxd.so SHA
cee5ed5e68526646is the validated v0.2.0 build.
0.1.0 — Initial public release
- Chain core C library (
libmxd) and validator binary (mxd_node). - BSC-side bridge contracts:
MXDBridgeV3.sol(one-way BSC → MXD, K-of-N Dilithium5 oracle attestation), plus theBNBMXDtoken and the historicalMXDBridge/TestBNBMXDreferences. - Protocol specifications:
- MXD-00 — index of standards
- MXD-01 — address format (addr32, dual-algorithm Ed25519 + Dilithium5)
- MXD-02 — mnemonic and HD derivation (BIP-39 + per-algo paths)
- MXD-03 — signing and verification (Ed25519 + Dilithium5)
- MXD-04 — transaction format and sighash
- MXD-05 — wallet-at-rest encryption
- MXD-06 — P2P handshake
- MXD-API-01 — bridge oracle attestation (K-of-N canonical message)
- MXD-CONS-01 — validator consensus signatures
- MXD-CONS-02 — fork choice and reorganization
- MXD-PQ-00 — post-quantum-ready wallet profile
- JSON test vectors for every spec, suitable for cross-implementation conformance testing.
- Operator/implementer utilities in
tools/:mxd_sign.c— standalone Dilithium5 (FIPS 204 ML-DSA-87) sign/verify CLI.gen_test_vectors.c— regenerate theMXD-*-test-vectors.jsonfixtures from the reference implementation.generate_node_key.py— generate a freshnode_keys.v2validator identity.
- Mainnet oracle set published in
docs/MAINNET_ORACLE_SET.md(5 Dilithium5 public keys + derived addresses). - C unit test suite (
tests/) with fuzzing and sanitizer harnesses. - Docker build (
Dockerfile) and platform-specific dependency installers.
- Bridge mint pipeline ships with defense-in-depth across the oracle DB
(atomic row claim,
dest_tx_hashexclusion filter, stuck-processing recovery), the libmxd queue (source_tx_hashdedup), the consensus validator (bridge_tx:<source_tx_hash>replay guard), and the API surface (audit-trailbridge_datain/block/NJSON responses). Any single-layer bypass is structurally caught by the next layer. - Admin operations (
AUTHORIZE_BRIDGE,REVOKE_BRIDGE,UPDATE_ORACLE_SET) require a 3-of-5 Dilithium5 oracle quorum over a canonical message format that prevents cross-operation replay.
AGPL-3.0-only.