A3 Strategy 2: on-chain recipient-cohort delivery + balance-arm discovery (D1 executable)#1
Open
DHEBP wants to merge 83 commits into
Open
A3 Strategy 2: on-chain recipient-cohort delivery + balance-arm discovery (D1 executable)#1DHEBP wants to merge 83 commits into
DHEBP wants to merge 83 commits into
Conversation
This reverts commit 3824e67.
…oproject#138)" This commit reverts some of the changes made in commit c0d3a94.
(cherry picked from commit 715d9c5)
(cherry picked from commit 051505f)
…ttacks - Implements SHAKE256-based ephemeral key derivation using sender secret, roothash, ring inputs, and recipient public key. - Introduces 32-byte prefix in payloads for compressed ephemeral public key. - Adds randomized padding to obfuscate payload size and structure. - Updates wallet sender/receiver logic to support new ENCRYPTED_DEFAULT_PAYLOAD_CBOR_V2 format. - Prevents trial decryption and payload re-identification attacks. Co-authored-by: Dank <44716232+DankFC@users.noreply.github.com>
Merge from dev to harden payload encryption
Merge pull request DEROFDN#3 from civilware/dev-xswd
Harden payload encryption and add websockets support
Previously, the simulator hardcoded --testnet=true, preventing users from testing mainnet behavior. The simulator would always run in testnet mode regardless of user input. This change: - Removes the hardcoded --testnet=true assignment - Defaults to mainnet when --testnet flag is not provided - Properly respects the --testnet flag when explicitly set by user - Adds defensive nil checking in InitNetwork() to handle missing flag Fixes simulator network mode selection to match user expectations.
- Changed logger name from 'derod' to 'simulator' for clarity - Added genesis wallet address, seed, and restore command display - Helps developers access the funded genesis wallet (~1.12 DERO) - Other 22 wallets only get 0.002 DERO registration bonus in mainnet The genesis wallet is the only one with significant funds in mainnet simulator mode since the premine list is skipped for mainnet.
…k with parent timestamps
…erval into consideration Replaced supply calculation formular in daemon to utilize the new function instead
…sholds Security enhancement to prevent fake proof displays: - Reject amounts exceeding DERO hard cap (22M, ~5% buffer above 21M) - Reject amounts that would cause int64 wraparound (>2^63-1) - Use SetUint64 instead of SetInt64 to prevent signed overflow - Verify payload amount matches claimed proof amount - Add comprehensive test coverage for known fake proof amounts Thresholds aligned with HOLOGRAM implementation for consistency.
Complete UI refresh inspired by DERO Foundation's noir theme: - Dark glassmorphism design with jade green accents - Responsive layout optimized for mobile devices - AJAX-based proof form for smooth UX (no page reload) - Block age displayed as human-readable relative time - Fixed epoch-0 timestamp display for Block 0 - User-facing note that payload proofs are display-only - Branded header with DERO hex logo - Updated footer link to DEROFDN/derohe - favicon.svg and logo.png assets Includes explorerlib_test.go for formatBlockAge coverage.
- Replace deprecated go get with git clone + go build workflow - Update GitHub links from deroproject to DEROFDN/derohe - Update explorer link to explorer.derofoundation.org - Remove defunct web wallet link - Add Matrix community channel link - Fix typos and clarify build instructions
Fix for supply calculation
feat(explorer): proof validation security + modern UI refresh
The Connect() function sets Connected=true after a successful WebSocket dial, but returns directly from test_connectivity() without resetting it on failure. This causes consumers to observe Connected==true even when the daemon connection is invalid (e.g., mainnet/testnet mismatch). Downstream code then incorrectly proceeds with background tasks against a broken connection. Reset Connected=false explicitly before returning the error.
…over Two related arithmetic-correctness fixes on the proof verification path, both follow-ups to PR DEROFDN#14. proof/proof_validation.go: the hard-cap and reasonable-amount thresholds were computed as if 1 DERO = 1,000,000 atomic units, but DERO actually uses 100,000 atomic units per DERO (matches globals/globals.go:295 and the GetInfo RPC supply conversion at rpc_dero_getinfo.go:81). The constants were therefore 10x too high — the validator's hard-cap check effectively triggered at ~220M DERO instead of the documented 22M. Reorder the const block so atomicUnitsPerDero is defined first, then rewrite each threshold as X * atomicUnitsPerDero so the conversion factor is explicit at every use site. Same fix applied to the largeThreshold local variable inside DetectSuspiciousProofPatterns. proof/proof_validation_test.go: four test cases referenced the buggy constants by value (e.g., 21_000_000_000_000 // "21M DERO"). With the corrected constants those amounts now correspond to 210M DERO atomic units — outside the labelled intent. Update the four affected test amounts and tighten their labels. cryptography/crypto/proof_generate.go: mirror PR DEROFDN#14's verifier-side SetInt64(int64(amount)) -> SetUint64(amount) hardening to the prover side. The previous pattern is bounded in legitimate use (transfer amounts and balances cap well below 2^63) and only causes self-DoS if it ever fires, but the asymmetry between prover and verifier is forensic noise once readers compare them. Drop the misleading "this should be reduced" comments — values at this scale (bounded by 2^64) are far smaller than the bn256 group order (~2^254), so modular reduction would be a no-op. No behavioral change for legitimate transfers. The validator's hard-cap rejection now actually fires at 22M DERO as the comments claim, rather than at 220M.
Warning 4 used a hardcoded 1 trillion atomic literal, which at the correct 100k atomic/DERO conversion flagged only multiples of 10M DERO (just 10M and 20M in the valid range). Restore the original intent of flagging exact multiples of 1M DERO via 1_000_000 * atomicUnitsPerDero. Note: DetectSuspiciousProofPatterns has zero callers in community-dev so this has no user-visible effect in this tree today, but it aligns the internal arithmetic with the corrected constants and matches the matching change in HOLOGRAM (DHEBP/HOLOGRAM @ a8902d9).
Added PrunedHeight information to GetInfo result
…Height to dvm.SimulatorInitialize()
fix(proof): correct atomic-unit thresholds + mirror SetUint64 to prover
Added PrunedHeight field to GetInfo_Result type
Fixed discrepancies in GetGasEstimate() result
…anic The receiver decode paths used sender_idx <= RingSize against a 0-indexed Publickeylist of length RingSize, so a crafted byte equal to RingSize indexed one slot past the slice and panicked the receiving wallet during history processing. Tighten to < in both the CBOR and CBOR_V2 paths so valid indices are 0..RingSize-1.
Add SenderVerified and RingSize to rpc.Entry, populated in both receiver decode paths. SenderVerified is true only at ring size 2, where the receiver override pins attribution to the counterparty; for larger rings the sender chose the unauthenticated attribution byte, so entry.Sender must not be presented as trusted. RingSize is carried so consumers can render attribution confidence per entry.
Add an additive TransferPayload0WithOptions variant carrying a TransferOptions struct; the existing TransferPayload0 becomes a shim passing the zero value, so all callers keep today's behavior. The new AttributionAnonymous mode writes a decoy ring slot index (drawn from the anonymity set, never the sender or receiver slot) into the encrypted receiver payload, reducing the receiver to the ring's 1-of-N anonymity instead of being handed the sender's slot. Honest mode is unchanged and still writes the receiver slot. A source-level guard test locks honest attribution to witness_index[1] so a change to the sender slot fails loud.
Add a RingPreference (via TransferOptions.Ring) letting a caller supply preferred decoys for ring assembly; random members top up to ringsize. With no preference the candidate source is unchanged, so behavior is identical to today. Preferred decoys are validated parseable, non-self, distinct, and registered on the base balance tree before use. Registration is probed against the base tree (the tree consensus falls back to for ring membership) so a curated decoy cannot pass the wallet and then reject at consensus after signing. Strict mode hard-errors a bad decoy; otherwise it is skipped and random selection fills the slot.
Adds an on-chain gate for the curated-decoy ring-selection path: a transfer whose ring members are user-curated via RingPreference (not drawn from DERO.GetRandomAddress), carrying an action-less SCDATA body, is built at ring 8, accepted into the pool, mined, and read back from the persisted block store — proving the curated ring is consensus-valid, not merely wallet-valid, and that the registration-probe in curatedRingCandidates prevents the 'passes wallet, rejects at consensus' failure by construction. Asserts all ring-2 decoy slots are filled from the curated set (no random fallback) and the body reads back byte-equal. Falsifiable: disabling curation drops curated members from the ring and fails the gate. Single-node simulator scope: finalization = persisted into a mined block (0-conf; PoW/miniblock verify and the low-fee floor are sim-bypassed). Multi-node fork-choice and fee competition remain out of scope.
A 25-agent adversarial audit found the A2 proof SOUND (no false-green;
verified Verify_Transaction_NonCoinbase runs with skip_proof=false, so the
curated ring passes real bulletproof/ring verification, not just wallet
checks) but flagged honesty/falsifiability gaps. This closes them:
- Add Test_CuratedRing_NegativeControls_A2 making the registration-probe
and finalization branches PROVEN-RUN, not EXPECTED-BY-INSPECTION:
(a) Strict + unregistered decoy -> build hard-errors before signing;
(b) non-Strict + unregistered decoy -> dropped, random member substitutes,
only registered preferred decoys appear in the ring;
(c) never-mined carrier -> absent from Block_tx_store (finalization
assertion is non-vacuous).
- Bind curation to finalization: assert finalized_tx hash == submitted hash
(the ring is read off the built tx; the hash commits the serialized ring).
- Make the curation assertion positional/subset-equality (every non-sender/
non-recipient ring member is a preferred decoy) instead of a bare count.
- Correct the success log: drop the unproven fee-debit claim (sim bypasses
the fee floor), state proof verification is NOT bypassed, scope to base-SCID.
- Scope the header + engine comment: for a zero-SCID transfer the curation
registration-probe is redundant with the ring-assembly re-probe; it is the
sole wallet-side defense only on the non-zero-SCID path (not run here).
…rm discovery (A3 S2) Proves on a real DERO simulator that a carrier sealed to receive-identity R2 of a co-held cohort is delivered, finalized, and DISCOVERED by R2's wallet from the balance-increase arm ALONE (no in-body tag, no trial-decryption), while the other co-held member R1 does NOT mis-attribute it -- the on-chain half of the recipient-rotation claim (the crypto half was proven separately). - Carrier to R2 finalizes (persisted in a mined block, Block_tx_store.ReadTX). - R2 attributes it via the real daemon_communication.go balance arm (Incoming entry, balance +1, amount 1); R1's balance is unchanged and it does not attribute. - Positive-discrimination twin: a second carrier to R1 proves the balance arm attributes each carrier to exactly the targeted member in BOTH directions, so co-held disambiguation is directional and live, not dormant. - No-tag proven on the PERSISTED form (finalized tx carries body "B", no target-index field). Index/timing non-leak is a source property (per-tx ring shuffle + witness_index never serialized) noted in the header. - Negative controls: never-mined carrier (R2 doesn't attribute -> non-vacuous); wrong-wallet query. Also hardens the shared simulator test harness (blockchain_sim_test.go): each simulator_chain_start now gets a fresh ephemeral RPC port + a unique data dir, and simulator_chain_stop tears down and cleans up. The previous fixed port + fixed /tmp/dsimulator dir raced on a leaked RPC server / in-flight graviton commit, flaking the whole package (A2 ~40%, A3 S2 ~75-87%) with a daemon-side GetEncryptedBalance panic during wallet sync. After the fix the package is stable: A3 S2 pair 20/20 consecutive, full package 8/8. Adversarially audited (20-agent panel, A3S2-AUDIT-RECORD.md): caught the flake and a confounded disambiguation control; both fixed and re-proven before this lands. Scope: single-node sim / 0-conf, base-SCID plain ring>=4, R=2.
…(A3 S2, red-team-owed) Closes the R>2 generalization the 2-member A3 S2 test left EXPECTED-BY-INSPECTION (and that the red-team explicitly flagged owed). On-chain analog of A3 Strategy 1's TargetRotatesAcrossCohort (which ran R=3 in pure crypto), now run through the real daemon_communication.go path on a simulator. Test_RecipientCohort_OnChain_R3Rotation_A3S2: a 3-member co-held cohort (distinct registered wallets), target rotated across all three. For each member in turn, a carrier sealed to it is attributed by EXACTLY that member via the balance arm (Incoming, balance +1) and by NEITHER of the other two (balance unchanged, no Incoming entry). Proves balance-arm discovery + co-held disambiguation hold for R>2, not just the pairwise R=2 case. Disambiguation control mutation-verified green->RED (inverting the non-target assertion fires). 8/8 stable in-process (the shared-harness race fix holds). Scope: single-node sim / 0-conf, base-SCID plain ring>=4.
DHEBP
added a commit
that referenced
this pull request
Jun 24, 2026
…l scrub test Addresses the DEROFDN#21 six-perspective review: - Self-send sites set SenderVerified=true + RingSize (both decode arms): a wallet-authored tx has a certain sender at any ring size, so a contract- following consumer no longer distrusts the user's own sends. (review #2) - Add Test_Attribution_Scrub_Behavior: drives the real receiver decode on a sim chain and asserts the scrub effect (ring2 keeps Sender+Data[0]; ring>2 blanks Sender + zeroes Data[0]). Rebuilds until the receiver lands in a non-zero ring slot so the Data[0]==0 assertion has teeth. Mutation-checked. The source-grep guard is kept as a tripwire. (review DEROFDN#3) - Document the (RingSize, SenderVerified, Sender) renderer truth table on the field; drop the redundant exported_payload[:] slice. (review DEROFDN#6, DEROFDN#9) The <=->< bounds guard is sufficient: rpc.NewAddress panics on an undecompressable ring key before the Publickeylist rebuild, so len(Publickeylist)==RingSize holds and the index cannot go out of range. (review #1)
DHEBP
added a commit
that referenced
this pull request
Jun 24, 2026
…anon Addresses the DEROFDN#22 review: - curatedRingCandidates canonicalizes the sender, recipient, and every preferred decoy to its base address before the distinctness/dedup checks, and the ring carries the base form. A ring member is identified by its pubkey, not its address string, so an integrated/payment-id encoding of an account already in the ring (sender, recipient, or another decoy) no longer slips the string-keyed checks and lands a duplicate pubkey that consensus rejects AFTER signing. Test_CuratedRing_AltEncoding_NoDuplicate_A2 covers recipient / sender / decoy-vs-decoy / cross-network, each mutation-proven. (review #1) - Anonymous attribution now fails closed at ring size < 4 (no decoy slot) instead of silently falling through to honest attribution. (review DEROFDN#8) - Port the unverified-attribution export scrub + self-send flag into the decode arms (this stack predates it); behavioral scrub test + an honest-byte guard that asserts honest mode writes the receiver slot (closes the grep guard's reassignment blind spot). (review DEROFDN#3, DEROFDN#9) The bounds guard is sufficient for the same reason as DEROFDN#21: bad ring keys panic at NewAddress before the Publickeylist rebuild, so len==RingSize. (review DEROFDN#4)
DHEBP
added a commit
that referenced
this pull request
Jul 2, 2026
… (review #1) The ring loop collected distinct members through a persistent deduplicator with no bound on passes: a candidate pool smaller than the ring spins forever holding transfer_mutex — the balance probe can't error on non-zero SCIDs (unregistered accounts get synthesized zero balances), success needs a full ring, and the candidate stream stops yielding. 41+ curated decoys made it deterministic: the combined count passed the <=40 scarcity check, so the base-tree rescue never fired. Seven changes: - Tail-only threshold: the <=40 check measures len(members)-curated (curatedRingCandidates reports its validated count per pass) — curated decoys no longer mask a scarce tree; upstream semantics restored. - Stall rescue: <=40 is a size heuristic blind to the 41..ringsize-1 dead band (upstream also hung there). After 2 consecutive barren passes (no new distinct candidate) assembly switches to base-tree candidates — sticky, base-fetch-only per pass. - Pass bounds: rescue armed, 32 further consecutive barren passes or 8*ringsize total -> explicit retryable error; an empty final fetch reports daemon failure, not pool exhaustion (Random_ring_members swallows RPC errors into empty lists — this also ends upstream's latent spin on a persistently failing daemon). - Wall-clock bound: exponential barren backoff (250ms doubling to 4s; realizable window ~112s > the daemon's 90s 5-block activity filter, so a transiently filtered pool recovers before exhaustion is declared) plus a 150s per-transfer stall budget (barren_passes resets on any progress, so a trickling daemon could otherwise re-arm ~32 windows ≈ 60 min at ring 128). Per-transfer scope matches the per-transfer barren state. - MaxTransfersPerBuild = 256, checked before any mutex-held RPC: every per-transfer cost above multiplies by len(transfers), and the consensus 300KB tx limit can't bound that (enforced after assembly has paid). Not a new restriction: the smallest payload serializes ~1.7KB wire, above the 300*1024/256 = 1200-byte floor — longer arrays could never have broadcast. Floor pinned on a real wire-form tx. - 45s per-RPC deadline (CallWithTimeout) on every mutex-held daemon call: random members, balance fetches, and NameToAddress (pre-assembly, reachable at ring 2 — deadline-free it kept the hang reproducible with every assembly bound bypassed). Without it a daemon that accepts the socket but never replies parks the goroutine forever with IsDaemonOnline() still true. RPC count is bounded across the whole body: <=2 fee fetches; per transfer 1 probe, <=20 resolver fetches, <=1 name resolution, then pass-capped assembly. - 45s per-write websocket deadline (rwc.NewWithWriteTimeout): jrpc2 holds one client mutex across socket writes AND timeout delivery, so one write blocked on a half-open daemon freezes every call and its deadline for the kernel TCP retransmit timeout (~15+ min) — and the wallet's own deadline-free 5s connectivity pinger makes that writer a certainty. The blocked write now errors at 45s, gorilla poisons the connection, Keep_Connectivity reconnects. Daemon/explorer rwc users keep the historical constructor. Worst-case mutex hold: (min(len(transfers),256)+1) x (150s + count-bounded RPCs x 3x45s) — an absolute constant. Residual: wasm keeps the deadline-free nhooyr channel (browser websockets buffer in the JS runtime); rides the send-layer timeout follow-up. Tests, each red pre-fix on its target defect: ring_pool_exhaustion_test (41 decoys + empty token tree at ring 64: hang -> seconds via rescue); ring_scarce_band_test (~92-leaf token tree ON CHAIN at ring 128: tail-only alone errors "have 90 of 128", the rescue fills it); ring2_payload_floor_test (pins the 1200-byte floor on wire form); transfer_array_cap_test (offline: cap+1 fails closed pre-network, cap passes); rpc_call_timeout_test (never-answering daemon: probe, members, name resolution all return at the deadline); rwc_write_timeout_test (peer that never reads: the blocked write errors at the deadline, not the TCP retransmit timeout); ring_backoff_test (arithmetic pin: backoff window > 5x BLOCK_TIME filter window — the filter is disabled below topoheight 100, so no simulator run can exercise it). The exhaustion-error branch is unreachable on a healthy chain (the 1326-account genesis premine floors the base pool; the rescue always reaches it) — covered by inspection. Test_Payload_TX / Test_Creation_TX_morecheck fail identically at base (pre-existing).
DHEBP
added a commit
that referenced
this pull request
Jul 2, 2026
…etroactive scrub (review #1/DEROFDN#4/DEROFDN#5/DEROFDN#6) DEROFDN#6 — Entry.String() printed "Sender: %s" unconditionally: a scrubbed ring>2 transfer rendered a bare "Sender: " line that reads as a decode bug — the exact "looks broken" outcome the design meant to avoid, contradicting the truth table documented on SenderVerified in the same file. The Sender line now follows the table: verified -> the address; scrubbed (no payload error) -> "unknown (unverifiable, ring size N)"; decode failure -> a distinct "unknown (payload decode failed)". Blast radius: two CLI debug dumps (%+v in show_transfers error paths); JSON marshalling untouched. Pinned by rpc/wallet_rpc_test.go (offline, sub-second): all three arms, plus forbids the bare line and scrub/decode-failure conflation. DEROFDN#4/DEROFDN#5 — the non-retroactive caveat lived only in commit 4e9de56's message. Now in the docs consumers read: SenderVerified's doc comment states the scrub is decode-time only (pre-upgrade entries keep their guessed Sender and unscrubbed Data[0], served as-is by Get_Transfers; the guarantee holds only for blocks decoded post-upgrade), the upgrade is one-way for the privacy property, and pre-upgrade entries deserialize SenderVerified=false, RingSize=0 — indistinguishable from a scrub without re-decoding, so a migration MUST re-derive RingSize from chain data (a read-path scrub keyed on persisted fields would wrongly blank the wallet's own verified sends — why the rescan is a follow-up, not this commit). RingSize's doc gains the "0 = pre-upgrade/unknown, not a real ring" note; the GetTransfers handler doc points RPC consumers at the contract. #1 hardening — both decode arms bounded sender_idx against Statement.RingSize while indexing Statement.Publickeylist. Safe today (len(Publickeylist)==RingSize holds at that point), but the guard now bounds against the slice actually indexed, so it survives any future break of that invariant. Existing pins green: the scrub-behavior sim test and the source-grep guard (its regexes key on the scrub lines, untouched here).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Proves on a real DERO simulator that the recipient-cohort rotation's on-chain delivery + discovery half works (the crypto half was proven separately in A3 Strategy 1). Closes Decision D1 → Option C EXECUTABLE.
A carrier sealed to a co-held cohort member is delivered, finalized, and discovered by that member's wallet from the balance-increase arm ALONE — no in-body tag, no trial-decryption — while the other co-held members do NOT mis-attribute it.
Gates (all PROVEN-RUN, audited)
Test_RecipientCohort_OnChainDelivery_A3S2— carrier to R2 finalizes (Block_tx_store.ReadTX); R2 attributes via the realdaemon_communication.gobalance arm (Incoming, balance +1); R1 does not. Positive-discrimination twin (a 2nd carrier to R1) proves attribution is directional. No-tag re-asserted on the persisted tx.Test_RecipientCohort_OnChain_NegativeControls_A3S2— never-mined carrier (non-vacuity) + wrong-wallet query.Test_RecipientCohort_OnChain_R3Rotation_A3S2— R=3 target rotation: each member attributes only its own carrier, the other two disambiguate cleanly. Closes the R>2 generalization.Harness hardening (also in this PR)
blockchain_sim_test.go: eachsimulator_chain_startnow gets a fresh ephemeral RPC port + unique data dir (+ teardown cleanup). The previous fixed port + fixed/tmp/dsimulatordir raced on a leaked RPC server / in-flight graviton commit, flaking the whole package (A2 ~40%, A3 S2 ~75-87%) with a daemon-sideGetEncryptedBalancepanic during wallet sync. After the fix: A3 S2 pair 20/20 consecutive, full package 8/8.Adversarial review
20-agent red-team caught the flake + a confounded disambiguation control; both fixed and re-proven before this lands. Verdict: gates SURVIVED.
Scope (honest)
Single-node sim / 0-conf (persisted-in-one-mined-block, not multi-node finality); base-SCID plain ring≥4; cohort-birth conceded. Index/timing non-leak is PROVEN-SOURCE (per-tx ring shuffle,
witness_indexnever serialized).