Vault adversarial analysis: properties, bypass enumeration, operational limits

Vault properties, bypass enumeration and operational limits.

This page reproduces docs/VAULT-ADVERSARIAL.md from qanum-core v0.9.0 (release tag). Links to files that are not published on this site are shown as plain text until the source repository is public.

Contents

Status: canonical. Companion docs: THREAT-MODEL.md §4, WALLET-GUIDE.md §4.2, whitepaper §14. Why this document exists: a review before the first public release (not an independent audit) asked for a written, code-anchored demonstration that Qanum’s cryptographic diversity is a property of the spending policy, not “two algorithms present in the protocol”, and posed six specific questions (no-bypass proof; what is committed at spend initiation; who can cancel and how; behaviour under censorship, congestion and reorgs; real reaction time; required surveillance). This document answers all six against the tree, states what vaults deliberately do not solve, and pins every property to tests.


1. The state machine, as consensus enforces it

Vaults are not a wallet convention: they are output templates whose transitions are validated in consensus, in three parallel copies: the no-crypto validator (validator.rs, used under assumevalid), the full verifier path (validator.rs), and the block-connect path, all applying the same shape and height checks. Structural checking is priced in the consensus budget: VU_VAULT_STRUCTURAL_CHECK = 10 VU (constants.rs), against 100 VU per ML-DSA-44 verification. Height conditions are checked inside the input validator itself (validator.rs), so neither assumevalid nor the signature cache can skip them.

State 1: VAULT (0x05), 72-byte payload (validator.rs, parse_vault_payload and the output rules):

Field Consensus restriction
hot_suite_id ML-DSA-44 or ML-DSA-87 (KeyRole::VaultHot); SLH-DSA is never a hot suite
hot_key_id (32 B) Free; must differ from cold_key_id (identical keys = OutputPayload, invalid by consensus)
cold_suite_id ML-DSA-44, SLH-DSA-SHAKE-128s, ML-DSA-87 or SLH-DSA-SHAKE-256s (KeyRole::VaultCold)
cold_key_id (32 B) Free; must differ from hot_key_id
delay_blocks (u32) Bounded by consensus to 144..=5 256 000 blocks (validator.rs)

On a chain that retires the level-1/2 suites (the test network does so from genesis), a new vault uses ML-DSA-87 as the hot suite and ML-DSA-87 or SLH-DSA-SHAKE-256s as the cold suite. Consensus does not require the cold suite to differ from the hot suite. The suite diversity this document relies on holds when the owner picks an SLH-DSA cold key (hash-based, a different hardness assumption from the lattice-based hot key). With an ML-DSA cold key the vault still gives the reaction window against hot-key theft, but no protection against a break of ML-DSA.

State 2: VAULT_PENDING (0x06), 76-byte payload (validator.rs, parse_vault_pending_payload and the output rules): same hot/cold key IDs and suites, plus unlock_height (u64), with the rule that unlock_height == 0 is invalid: a disguised instant spend.

Transitions out of VAULT (validator.rs), exactly two:

Transitions out of VAULT_PENDING (validator.rs), exactly two:

Every transition is signed under the protocol’s single sighash (SIGHASH_ALL, constants.rs), which commits to all outputs of the signing transaction: nobody can sign “a spend” and redirect it afterwards.

Wallet/CLI expose all four operations as first-class commands (vault init/recover/finalize/cancel, src/bin/qanum_cli/custody.rs), with builders that pre-check invariants (wallet_manager.rs).

2. What each custody tool covers (property table)

Property Threat Covered by
Recovery from key loss Owner loses the hot key P2RKPQ (0x02) delayed recovery path; VAULT cold path
Reaction to key theft Attacker obtains a valid hot key VAULT: the hot branch must cross a cancellable window
Resistance to suite break Attacker can forge one suite’s signatures Window + cancellation signed by a key of the other suite family, when the cold key is SLH-DSA; a conditional mitigation, not a guarantee

P2RKPQ is an anti-loss tool, not anti-theft: its recovery path is as available to a key thief as to the owner (its delay bounds apply the same way, validator.rs). VAULT is the anti-theft tool with a reaction window: it does not stop the thief from initiating; it guarantees the initiation is visible, cancellable and non-instant. No template stops a spender who controls (or can forge) both keys; by design, the goal is that stealing/breaking one suite is not enough.

3. The reviewer’s six questions

3.1 Is there any bypass path?

Enumeration of every spend route of a VAULT / VAULT_PENDING output:

  1. Hot spend of VAULT -> can only produce the byte-fixed VAULT_PENDING and an optional identical VAULT change (validator.rs). No bypass.
  2. Cold spend of VAULT -> immediate spend. A feature (recovery), not a bypass; requires the cold key.
  3. Hot spend of VAULT_PENDING before unlock -> HeightCondition (validator.rs). Blocked.
  4. Hot spend of VAULT_PENDING at/after unlock -> finalize to free destinations. By design.
  5. Cold spend of VAULT_PENDING -> cancel at any height. By design.
  6. Identical hot and cold keys -> OutputPayload at creation, for both VAULT and VAULT_PENDING. Impossible by consensus to build a single-key vault.
  7. Suite confusion / wrong-length payloads -> parsers require exactly 72/76 bytes and whitelisted suites. Blocked.
  8. Route confusion across the three validators -> all three apply the same shape and height logic (height lives in the input validator itself), so assumevalid and the signature cache cannot skip it.

Conclusion: no bypass of the window exists for the hot branch. The only fast door is the cold key, a stated property (§5.1).

3.2 What is committed when a spend is initiated?

No destinations. Neither finalize nor cancel destinations are committed at init: VAULT_PENDING fixes keys, unlock_height and the value assigned by the init; final destinations are chosen when the finalize or cancel is transmitted. This is stated plainly because it defines what the window buys: the window buys reaction time, not destination freezing. Value is fixed in two steps: the hot init splits the vault’s value between VAULT_PENDING (vout[0]) and optional VAULT change (vout[1]); each output then keeps its value until it exits its own state.

Operational consequence: a defender cancelling does not need to guess where the money was going; detecting the VAULT_PENDING is enough. The attacker cannot shrink the window either: delay_blocks lives in the original VAULT payload and the init must reproduce it; the attacker only chooses when the window starts.

3.3 Who can cancel, under what conditions?

Only the cold key, via VAULT_PENDING path 1: no height condition, no extra shape constraints, normal fees. The hot key cannot cancel: by construction, path 0 requires current_height >= unlock_height. This asymmetry is the vault’s semantics: cancellation authority lives outside the key set a hot-key thief may hold. The uncomfortable symmetry must also be stated: whoever controls the cold key can spend immediately through the same route. Vaults protect against hot-key theft, not cold-key theft (§5.1).

Practical conditions: the cancel is a normal transaction; it needs a sufficient feerate for relay (min_fee_rate, default 1 joma/byte) and, if it conflicts with a queued finalize, must satisfy the replacement rules (pay replaced_sum + min_fee_rate × size and a feerate strictly higher than the best evicted tx; mempool.rs replacement checks). See §3.4 and the congestion note below for the operational detail.

3.4 Censorship, congestion, reorganizations

Reorgs: immune by construction. unlock_height is an absolute height, not a relative lock (Qanum has no relative locktimes; windows use absolute heights, consistent with the channel design in whitepaper §4.3). A reorg only moves current_height: a finalize keeps waiting, a cancel stays valid at every height. If the reorg un-confirms the init itself, the vault simply returns to VAULT state, benign; a later re-init recomputes unlock_height = height + delay at the new confirmation height (validator.rs), never shorter. Mempool rebuild after reorg uses the live policy and re-validates every candidate at the new height (mempool_reorg_invariants.rs, node_mempool_reorg.rs).

Congestion: the real flank, measured and pinned. Cancel and finalize are competing transactions for the same outpoint: until one confirms, the higher feerate wins (always-RBF). Three operational facts:

Still open: package relay on the wire is not implemented (whitepaper §13.3); efficient propagation of a parent+child package under sustained congestion is not guaranteed. Tracked as mid-term hardening, not as a vault-specific defect.

Active censorship. A miner (or coalition) can refuse the cancel while including the finalize; the structural defence is that the cancel is valid at every height, so censorship must be continuous until the unlock. Short of a majority attacker, any honest miner includes it. No UTXO protocol can offer more against a censoring majority; that is the general PoW threat model, not a vault weakness.

3.5 How much real reaction time does the user have?

The reaction budget is delay − (detection + propagation + confirmation):

Example: a 1 152-block vault, mempool surveillance, moderate congestion -> the effective reaction is roughly 23 of the 24 window hours. A 144-block vault without mempool surveillance and under congestion -> the real margin can shrink to ~2 hours. Operational rule: the effective window is the window minus expected congestion time; choose delay accordingly.

3.6 What surveillance infrastructure is needed?

4. Test anchors

Every property above is pinned in the suite:

Property Tests
Init shape, byte-identical pending payload, bypass shapes rejected tests/block_validation.rs (block_valid_with_vault_init, TV-005 bypass cases)
Delay bounds, key ordering, pre-check order wallet_manager.rs unit tests (delay checked before keys)
Cold cancel crosses a saturated pool; eviction by descendant-package feerate tests/mempool_congestion_vault_cancel.rs (5 tests)
RBF of the cancel: incremental fee + strictly-higher feerate; MAX_REPLACEMENT_CANDIDATES; honest MempoolFull same file
Confirmed cancel beats attacker finalize; HeightCondition at tip+1 same file
Mempool rebuild/reorg keeps live policy tests/mempool_reorg_invariants.rs, tests/node_mempool_reorg.rs

5. What vaults do not solve

An honest analysis ends with the out-of-scope list:

  1. Cold-key theft or compromise -> immediate spend via path 1. Mitigation: the cold key lives offline (hardware / air-gap); the design deliberately roots authority in the cold key. (Alternative: a delay on cancel too, rejected for now because it sacrifices instant recovery.)
  2. Simultaneous forgery of both suites -> no template helps; that is the suite-migration scenario (whitepaper §6.5), not daily custody.
  3. Malware on the hot machine -> can steal the hot key and start the window at will; the window still gives the owner the chance to cancel, unless the cold key is handled on the same compromised machine.
  4. Physical coercion of any key holder.
  5. A confirmed finalize is irreversible: the race is played in the mempool; once the finalize mines, the cancel has no input left to spend. The entire value of the vault is surveillance during the window.

6. Findings raised by this analysis and how they are handled

Severity Finding Status
Low (docs) Finalize/cancel destinations are not committed at init, and the whitepaper did not describe the state machine. Covered by whitepaper §14.3 and this document.
Info (design) Cancel symmetry: the cold key also spends instantly. Vaults protect hot-key theft, not cold-key theft. Stated here (§5.1) and in whitepaper §14.3.
Medium (ops) The window is only worth what the mempool is worth under congestion (a small default pool; no package relay). Fixed: 32 MB / 40 000 entries + maxmempoolentries, profiles in STORAGE.md, urgent-cancel scenario pinned by mempool_congestion_vault_cancel.rs. Package relay remains open as mid-term hardening.
Low (ops) No dedicated surveillance RPC (listvaultspending, alerts); detection needs an external job over getrawmempool / UTXO listing. Open: candidate minor RPC AIP; USER-GUIDE item meanwhile.
Low (practice) Pre-signed cold cancel was undocumented as an operational pattern despite being free under the current machine. Documented here (§3.6) and linked from WALLET-GUIDE §4.2.

7. Bottom line

The reviewer’s request (“cryptographic diversity must be a demonstrated property of the spending policy”) is satisfied by the consensus VAULT machine: the compromised (hot) branch cannot spend without crossing a cancellable window whose cancellation authority lives in another key, of another suite family when the cold key is SLH-DSA, with no height condition, and no bypass of that window exists. What was missing was the demonstration itself; this document, together with the pinned tests, supplies it, including the parts the design deliberately does not cover.