Skip to content

feat(walletapi): decoy selection — batch RPC + activity sampler + D6 bins (fixes #88) - #89

Open
liqdmetal wants to merge 3 commits into
DEROFDN:community-devfrom
liqdmetal:feature/decoy-batch-rpc
Open

feat(walletapi): decoy selection — batch RPC + activity sampler + D6 bins (fixes #88)#89
liqdmetal wants to merge 3 commits into
DEROFDN:community-devfrom
liqdmetal:feature/decoy-batch-rpc

Conversation

@liqdmetal

@liqdmetal liqdmetal commented Aug 22, 2026

Copy link
Copy Markdown

Summary

Non-consensus fix (wallet↔daemon protocol only, ships in a normal release): kills the two decoy-selection privacy leaks from the transaction-relation analysis, and publishes the activity model that makes selection track real participants.

The two leaks

K1 — active-account narrowing. DERO.GetRandomAddress samples the balance tree but skips any account whose ciphertext changed in the last 5 blocks. Since decoys are guaranteed untouched for 5 blocks, any ring member that was touched is, by construction, sender or receiver. An observer computing per-block balance-tree diffs can read this signal off public state.

K2 — daemon-ring-leak. The wallet fetches the encrypted balance of every decoy candidate via DERO.GetEncryptedBalance with the address in plaintext. A daemon sees: the wallet's own address, the receiver, and every decoy re-queried milliseconds after serving it — and can reconstruct the ring, often inferring sender/receiver from query order and timing.

The fix

Node side = raw material only. New RPC DERO.GetRandomAddressBatch returns up to 512 real registered accounts WITH their encrypted balances in one response, sampled from the balance tree:

  • the 5-block filter is removed
  • ghost/zero-balance accounts are rejected server-side

Wallet side = selection. The wallet verifies the batch, then picks the final ring client-side with its own CSPRNG (Fisher-Yates draw, crypto/rand). The daemon's posterior over the true ring after serving a batch of size B for a ring of size R is 1/C(B,R) — its information advantage is destroyed.

Activity-matched sampler + published model (D6, folded in)

Uniform sampling fixes sender-selection statistics but not activity-distribution matching. This revision adds:

  • walletapi/decoy_sampler.goDecoyModel (weighted "blocks since last appearance" bins), candidateRecency (NonceHeight from the embedded NonceBalance), SelectDecoys (weighted draw without replacement).
  • walletapi/decoy_model_published.goPublishedMainnetDecoyModel / DefaultDecoyModel(): the D2 direct-estimator bins (13.6% in 0–5 blocks, 24.7% in 5–10, then 10–50 / 50–200 / 200–1000 / 1000+). Source: ringsize-2 members are BOTH real participants (zero decoys), so the participant density is read straight from their recency histogram. Starting table, not a live feed.

The posterior over ring members is uniform iff decoys are drawn from the same distribution as real participants; this closes that gap.

Changes

File Change
rpc/daemon_rpc.go GetRandomAddressBatch_Params/Result + Candidate structs
cmd/derod/rpc/rpc_dero_getrandomaddress.go GetRandomAddressBatch handler (cap 512)
cmd/derod/rpc/websocket_server.go register getrandomaddressbatch
walletapi/daemon_communication.go Random_ring_members_batch() + filterBatchCandidates() (fail-closed)
walletapi/wallet_transfer.go ring assembly uses batch + CSPRNG; falls back to legacy if daemon lacks RPC
walletapi/decoy_sampler.go activity-matched weighted selection
walletapi/decoy_model_published.go published mainnet bins + DefaultDecoyModel()

Security argument

  1. No active-account signal (K1 fixed) — decoys drawn from the full registered set including recently-active accounts.
  2. Daemon cannot reconstruct ring (K2 fixed) — one batch request, offline selection.
  3. No ghost injection (K3 fixed) — wallet verifies registered + balance for every candidate (fail-closed).
  4. Activity matching (D5/D6) — decoy distribution tracks real participant density; posterior uniform.
  5. No consensus impact — decoy selection is not consensus-enforced; ships without a fork.

Honest limits

  • The published model is a starting table from first-run measurement, not a live per-epoch feed (refresh is a follow-up).
  • Timing/network metadata (first-seen, IP) is orthogonal.
  • Bounded ring (max 128) unchanged.

Branch: feature/decoy-batch-rpc in the fork liqdmetal/derohe-improvements-by-liqdmetal. Carries the build-manifest fix (go.mod/go.sum) for fresh-clone builds. Flat on community-dev.

liqdmetal added a commit to liqdmetal/derohe-improvements-by-liqdmetal that referenced this pull request Aug 23, 2026
… fix

Implements spec/decoy-activity-distribution.md D5: decoy sampling that
matches the REAL participant activity distribution, so an observer cannot
distinguish real sender/receiver from decoys by on-chain activity alone
(the OSPEAD analog for DERO's account model).

walletapi/decoy_sampler.go:
- DecoyModel: published bin table (blocks-since-last-appearance x weight)
  from the D2 direct estimator (participant density p(x) recovered from
  ringsize-2 members — no deconvolution noise)
- candidateRecency: extracts blocks-since-last from the candidate's
  embedded NonceBalance (uvarint NonceHeight + ElGamal)
- SelectDecoys: weighted Fisher-Yates-style draws without replacement,
  probability proportional to bin weight; zero-weight candidates never
  drawn

walletapi/wallet_transfer.go: fix the batch-path decode — the tree value
is NonceBalance (varint NonceHeight + 66B ElGamal), NOT a bare ElGamal;
NonceBalance.Unmarshal panics on malformed input, so it must be
recover-guarded (a wallet must never panic on a daemon's malformed batch).

Tests (walletapi/decoy_sampler_test.go):
- TestCandidateRecency: recency extraction + malformed -> 0
- TestSelectDecoysMatchesModel: sampled distribution matches model
  weights (recent share ~0.67 vs expected 0.714 on a 10:1 model, within
  finite-population tolerance); zero-weight candidates excluded

Posterior math (spec §3.1): Pr[s=m_i | ring] is uniform iff decoys are
drawn from the same distribution as real participants; this module
implements that match. Pure client logic — no consensus impact.

Builds on the batch RPC (PR DEROFDN#89) — the wallet already has the candidate
features (NonceHeight embedded in each candidate's balance).
@liqdmetal
liqdmetal force-pushed the feature/decoy-batch-rpc branch from 362a598 to 7b18a77 Compare August 23, 2026 03:26
…dor noise)

Daemon GetRandomAddressBatch (<=512 real accounts with encrypted balances,
one RPC call, 5-block filter removed); wallet Random_ring_members_batch()
with CSPRNG Fisher-Yates draw. Effect: no per-decoy round-trips; daemon
posterior collapses to 1/C(B,R).
@liqdmetal
liqdmetal force-pushed the feature/decoy-batch-rpc branch from 2a18dfb to aef5bf9 Compare August 25, 2026 01:30
…er (one package)

Batch RPC: GetRandomAddressBatch (<=512 real accounts, one call, 5-block
filter removed) + client-side CSPRNG Fisher-Yates draw. Activity sampler:
DecoyModel/DecoyBin quantile model, recency from NonceHeight, SelectDecoys
weighted draw without replacement. Effect: decoy distribution tracks
observed mainnet activity, daemon posterior collapses to 1/C(B,R).
D2 ringsize-2 direct-estimator table as DefaultDecoyModel(). Starting
artifact, not a live feed. Sampler already consumes DecoyModel.
@liqdmetal liqdmetal changed the title Decoy batch RPC + client-side ring selection (fixes #88) feat(walletapi): decoy selection — batch RPC + activity sampler + D6 bins (fixes #88) Aug 25, 2026
@liqdmetal

Copy link
Copy Markdown
Author

Folded local maturity: PublishedMainnetDecoyModel / DefaultDecoyModel() — D2 ringsize-2 direct-estimator bins as the public starting table the sampler consumes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant