anticsdecoded|Portfolio
SEC|2026
/audits / PARTICL.cpp
cat /audits/PARTICL.cpp
NODE: PRIVATE

Privacy L1 (RingCT)

// engagement
protocolParticl
categoryPrivacy L1 (RingCT)
kindprivate · Private
date2026-09
duration1 week
languageC++
chainsParticl
nSLOC3,020
// findingsNODE: 1 FND
critical0
high1
medium0
low0
info0
gas0
// summary

Private disclosure to the Particl Core team for particld/particl-qt, a Bitcoin-derived privacy L1 with RingCT (anon) transactions, covering an out-of-bounds read in consensus transaction-input validation (Consensus::CheckTxInputs) reachable by an unauthenticated peer through the default-on anon relay path.

// methodology
  • Manual review
  • PoC development
  • Mempool/consensus analysis
// scope
pathnsloc
src/consensus/tx_verify.cpp680
src/anon.cpp520
src/validation.cpp1,200
src/primitives/transaction.h320
src/policy/policy.cpp300
total3,020
// findings
H-01highfixed

Empty anon-input witness stack triggers an out-of-bounds read that crashes `CheckTxInputs` on a synced node

`Consensus::CheckTxInputs` binds a reference to `stack[0]` of a RingCT (anon) input's `scriptWitness.stack` (and `scriptData.stack`) with no non-empty check, so a serialized anon input carrying a valid 33-byte `scriptData` key-image blob but an empty `scriptWitness.stack` survives the wire format and passes every earlier gate — `CheckTransaction`, `IsStandardTx`, and `CheckAnonInputMempoolConflicts`, which guards only `scriptData` — before the ring loop dereferences the garbage reference and the node dies with SIGSEGV. Anon relay is on by default, so any unauthenticated peer can crash a synced mainnet node via the mempool path (`PreChecks`), and a staker can embed the same tx in a valid PoS block to crash every node that validates it (`ConnectBlock`). The matching guard (`bad-anonin-wstack-size`) already existed in `VerifyMLSAG` but ran after `CheckTxInputs`; fixed by hoisting the stack-size guards to the top of the anon branch, before any `stack[0]` read.