anticsdecoded|Portfolio
SEC|2026
/audits / MULTIVERSX.go
cat /audits/MULTIVERSX.go
NODE: PRIVATE

L1 (data trie / consensus)

// engagement
protocolMultiversX
categoryL1 (data trie / consensus)
kindprivate · Private
date2026-10
duration3 weeks
languageGo
chainsMultiversX
nSLOC2,800
// findingsNODE: 1 FND
critical1
high0
medium0
low0
info0
gas0
// summary

Private disclosure for mx-chain-go, the MultiversX Go full node, covering the account data-trie layer's lazy AutoBalanceDataTries migration path, where old-style raw-key leaves and new hashed-key leaves are told apart by lookup order instead of by verifying the leaf. Confirmed on a real 8-process local network built from master (real libp2p gossip and BLS multisignature consensus), in addition to a unit PoC against the real trie and in-repo chain-simulator tests: a committed, publicly queryable storage value is silently deleted by an unrelated later write, and the committed state root of a transaction becomes nondeterministic.

// methodology
  • Manual review
  • PoC development
  • Chain-simulator testing
// scope
pathnsloc
state/trackableDataTrie/trackableDataTrie.go640
trie/patriciaMerkleTrie.go520
trie/leafNode.go260
process/smartContract/processorV2/vmOutputAccountsProcessor.go480
process/common.go900
total2,800
// findings
C-01criticalacknowledged

Data-trie key collision in the AutoBalance migration path silently deletes committed state and makes transaction state roots nondeterministic — externally triggered consensus halt

The raw-key fallback in `retrieveValueFromTrie` classifies a leaf as legacy by lookup order alone, and the version-blind trie `Get` matches on key bytes (`bytes.Equal` in `leafNode`), so when a contract writes storage key K2 = blake2b(K1) the fallback for K2 lands on K1's committed AutoBalance leaf at physical position blake2b(K1), mislabels it `NotSpecified`, and `deleteOldEntryIfMigrated` deletes it — silently and permanently destroying K1's committed value. The same mislabel makes the committed state root of a transaction a function of Go map iteration order: `updateTrie` flushes `dirtyData` unsorted, so one transaction writing both K1 and K2 commits two different tries across executions, and in a full consensus group the block can never be notarized — one cheap transaction kept in the mempool halts the chain, the same failure category as the September 2026 mainnet incident. The AutoBalanceDataTries flag has been active on mainnet since epoch 1265 (network now at epoch 2255), so any deployed contract can trigger this with a plain transaction; journal-revert resurrection of written values and cross-key reads follow from the same root cause, while protected `0x00`-prefixed system keys are out of reach. Verified at three levels on unmodified master (497f0f57): unit PoC, passing chain-simulator tests, and the live local network, where committed rounds show both INTACT and DESTROYED outcomes for byte-identical transactions. Suggested fix: verify the original key embedded in `TrieLeafData` on raw-position hits before classifying a leaf as legacy, and flush `dirtyData` in sorted key order.