Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3cfe46a
Update Rust to 1.70.0 with ARM64 macOS support
VictorLux Dec 8, 2025
de5cf5e
Merge PR #103: Onion v3 local address fix + tests
VictorLux Dec 8, 2025
479efd2
Merge PR #100: Disable debug.log by default + add config option
VictorLux Dec 8, 2025
1e85e57
Add ZipherX branding to client version
VictorLux Dec 9, 2025
9704c64
Merge remote-tracking branch 'upstream/master'
VictorLux Jun 5, 2026
1c772fe
Enable ZIP-209 turnstile on mainnet; tag subversion ZIP209
VictorLux Jun 5, 2026
27cb436
CR-01: run contextual checks during IBD (close validation bypass)
VictorLux Jun 5, 2026
7780ab7
Add read-only chain auditor + ZIP-209/CR-01 tester checklist
VictorLux Jun 6, 2026
acce246
Fix NULL-pointer crash on genesis block during -reindex / import
VictorLux Jun 6, 2026
19745ab
Fix -reindex stall: generous tx-size bound in non-contextual CheckTra…
VictorLux Jun 7, 2026
bbea317
harden ZIP-209 pool accounting: checked CAmount delta arithmetic
VictorLux Jun 7, 2026
b2da5f6
docs: add Transparent-Value Shielded Pool (TVSP) proposal (v2)
VictorLux Jun 7, 2026
fb585fd
docs: add TVSP proposal (v2) Markdown source
VictorLux Jun 7, 2026
dcbdf14
deep-reorg parking: skip during IBD/reindex (fix from-genesis reindex…
VictorLux Jun 8, 2026
28382a7
Fix use-after-free in ConnectBlock check-queue lifetime (CVE-2024-529…
VictorLux Jun 9, 2026
4a6dce0
docs: security hardening June 2026 (UAF fix, parking IBD-gate review,…
VictorLux Jun 9, 2026
033f4fa
Merge feature/parkdeepreorg-ibd-gate: ConnectBlock UAF fix + parking …
VictorLux Jun 9, 2026
6b53a69
Enforce checkpoint hash at header acceptance (audit #2)
VictorLux Jun 9, 2026
48dabbf
docs: mark audit #2 (checkpoint hash enforcement) done in security-ha…
VictorLux Jun 9, 2026
9bf04ef
Merge feature/checkpoint-hash-enforcement: enforce checkpoint hash at…
VictorLux Jun 9, 2026
ec066e7
docs: correct drifted line numbers in security-hardening doc; add new…
VictorLux Jun 9, 2026
132068a
Merge docs/security-docs-accuracy: fix drifted line numbers + bring R…
VictorLux Jun 9, 2026
b41f715
Rebrand network client identity ZipherX -> ZClassic
VictorLux Jun 9, 2026
0819c2e
Merge chore/client-identity-zclassic: subversion + README now identif…
VictorLux Jun 9, 2026
127ed92
docs(README): drop remaining ZipherX references after client rebrand
VictorLux Jun 9, 2026
755b93b
docs: drop remaining ZipherX references (rebrand to ZClassic)
VictorLux Jun 9, 2026
a7162ad
security: apply 2026-06 source-review fixes (consensus, memory, crypt…
VictorLux Jun 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# ZClassic 2.1.2-beta6
# ZClassic 2.1.2-ZIP209-beta6

ZClassic is an Equihash-based proof-of-work implementation of the Zerocash protocol. It offers privacy through shielded transactions using zero-knowledge proofs that preserve transaction confidentiality. Based on Bitcoin's code and derived from Zcash, ZClassic builds three main binaries: **zclassicd** (daemon), **zclassic-cli** (RPC client), and **zclassic-tx** (transaction utility).

Expand All @@ -16,6 +16,23 @@ This software is the ZClassic client. It synchronizes the entire blockchain hist

---

## Consensus Hardening (ZIP209 builds)

These builds advertise the network subversion `/ZClassic:2.1.2-ZIP209-beta6/` and carry the following consensus-validation and reliability fixes on top of upstream ZClassic:

- **ZIP-209 shielded turnstile (mainnet).** A block that would drive the Sprout or Sapling shielded value-pool balance negative is rejected as invalid. This bounds the damage of any shielded soundness bug: value forged inside a pool cannot be withdrawn past the pool boundary undetected. Enforcement starts from a hardcoded Sprout value-pool checkpoint — see [doc/zip209-mainnet-reactivation.md](doc/zip209-mainnet-reactivation.md).
- **CR-01 — contextual checks during initial block download.** `ContextualCheckTransaction()` no longer returns early while the node is in initial block download / import / reindex. Transaction version and network-upgrade activation enforcement, JoinSplit Ed25519 signature verification, and all Sapling spend/output/binding checks now run in every node state, closing a node-state-dependent validation bypass. DoS ban scores stay reduced while syncing; only the checks are no longer skipped.
- **Reindex genesis-block crash fix.** `AcceptBlockHeader()` no longer dereferences a NULL `pindexPrev` for the genesis block (which has no predecessor), fixing a segmentation fault that aborted every `-reindex` / block import at startup. This is a reliability fix, not a consensus change, and it is what makes the CR-01 re-validation reindex above actually completable. The bug was latent because fresh nodes use anchor-pinned fast-sync rather than a from-genesis reindex.
- **ConnectBlock check-queue use-after-free (CVE-2024-52911 parity).** Queued script checks held a raw pointer into a local `txdata` vector that `~CCheckQueueControl()` could outlive on an early `ConnectBlock` return, dereferencing freed memory while draining still-queued checks — a remotely-triggerable crash (DoS) reachable with parallel script verification (`-par>=2`) when a late-failing valid-PoW block is connected. Fixed by declaring `txdata` before the check-queue controller so its lifetime always outlives the controller's `Wait()`. Memory-safety / reliability fix, not a consensus change.
- **Deep-reorg parking skipped during IBD/reindex.** The deep-reorg park in `AcceptBlock` fired spuriously on the out-of-height-order block loads of `-reindex`, parking large swaths of the chain and stalling a from-genesis reindex. It is now gated behind `!IsInitialBlockDownload()`. At-tip behaviour is unchanged — the latch in `IsInitialBlockDownload()` keeps parking active once the node has synced — and the skip window stays backstopped by depth-10 auto-finalization and the 99-block reorg cap. Reliability fix.
- **Checkpoint hash lock-in at header acceptance.** A header presented at a hardcoded-checkpoint height with the wrong block hash is now rejected in `ContextualCheckBlockHeader`, independent of current chain state — closing an eclipse / bootstrap gap where a fresh node could follow a forged chain that does not pass through the compiled checkpoints. Rule-tightening (soft-fork class); honest peers are unaffected.

Details and the full security write-up for the last three items are in [doc/security-hardening-2026-06.md](doc/security-hardening-2026-06.md).

**Deployment notes.** ZIP-209, CR-01, and the checkpoint hash lock-in are rule-tightenings (soft-fork class) and want a coordinated upgrade; the UAF and parking fixes are reliability fixes and are safe to deploy independently. Because the CR-01 fix changes how blocks are validated during sync, run a `-reindex` on a CR-01-fixed binary to re-validate existing chain state (a reindex on an *un*fixed binary does not re-validate, since reindex keeps the node in IBD). Full validation is correspondingly slower; fresh nodes can still use anchor-pinned fast-sync.

---

## Quick Start

### Release Binaries
Expand Down
10 changes: 5 additions & 5 deletions depends/packages/rust.mk
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package=rust
$(package)_version=1.32.0
$(package)_version=1.70.0
$(package)_download_path=https://static.rust-lang.org/dist
$(package)_file_name_linux=rust-$($(package)_version)-x86_64-unknown-linux-gnu.tar.gz
$(package)_sha256_hash_linux=e024698320d76b74daf0e6e71be3681a1e7923122e3ebd03673fcac3ecc23810
$(package)_file_name_darwin=rust-$($(package)_version)-x86_64-apple-darwin.tar.gz
$(package)_sha256_hash_darwin=f0dfba507192f9b5c330b5984ba71d57d434475f3d62bd44a39201e36fa76304
$(package)_sha256_hash_linux=8499c0b034dd881cd9a880c44021632422a28dc23d7a81ca0a97b04652245982
$(package)_file_name_darwin=rust-$($(package)_version)-aarch64-apple-darwin.tar.gz
$(package)_sha256_hash_darwin=75cbc356a06c9b2daf6b9249febda0f0c46df2a427f7cc8467c7edbd44636e53
$(package)_file_name_mingw32=rust-$($(package)_version)-x86_64-pc-windows-gnu.tar.gz
$(package)_sha256_hash_mingw32=358e1435347c67dbf33aa9cad6fe501a833d6633ed5d5aa1863d5dffa0349be9
$(package)_sha256_hash_mingw32=52945bf6ab861d05be100e88a95766760d2daff1a0c0a2eff32a7fd8071495bd

ifeq ($(host_os),mingw32)
$(package)_build_subdir=buildos
Expand Down
189 changes: 189 additions & 0 deletions doc/security-hardening-2026-06.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# Security hardening — June 2026

This document records the security work integrated via the merge of
`feature/parkdeepreorg-ibd-gate` into `master`. It covers one fix, one
reviewed behaviour change, one hardening commit, and the status of the
remaining items from the June-2026 upstream-parity audit.

> Line numbers are accurate as of `master` `9bf04ef32` and drift with later
> edits. Every reference also names its symbol/function — if a number looks
> off, grep the named symbol (e.g. `GENEROUS_TX_SIZE_LIMIT`) rather than trust
> the line.

Scope of the merge (commits, newest first):

| Commit | Summary |
|--------|---------|
| `28382a7cd` | Fix use-after-free in `ConnectBlock` check-queue lifetime (CVE-2024-52911 parity) |
| `dcbdf14e5` | Deep-reorg parking: skip during IBD/reindex (fixes from-genesis reindex stall) |
| `fb585fd29` | docs: TVSP proposal (v2) Markdown source |
| `b2da5f69f` | docs: Transparent-Value Shielded Pool (TVSP) proposal (v2) |
| `bbea3177e` | Harden ZIP-209 pool accounting: checked `CAmount` delta arithmetic |

---

## 1. ConnectBlock use-after-free — FIXED (`28382a7cd`)

**Class:** memory-safety use-after-free. Bitcoin **CVE-2024-52911** /
Zcash **GHSA-fqr9-fxpx-rfpf** parity.

### Root cause
In `ConnectBlock` (`src/main.cpp`):

- Queued script checks (`CScriptCheck`) hold a **raw**
`PrecomputedTransactionData*` into a local `txdata` vector
(`CScriptCheck::txdata`, `src/main.h`).
- `~CCheckQueueControl()` calls `Wait()` on **every** return — including
early returns — and `Wait()` drains any still-queued checks **on the
calling thread** (the "master" in `CCheckQueue::Loop`).
- `control` was declared **before** `txdata`. C++ destroys locals in reverse
declaration order, so on an early return `txdata` was destroyed **first**,
then `~control`'s `Wait()` ran the queued checks that dereferenced the
freed `txdata` → **heap-use-after-free**.

### Reachability and impact
Reachable in normal operation: `-par>=2` (default) and a block above the last
checkpoint (i.e. live blocks near tip). An attacker mines a valid-PoW block
crafted to fail **late** — e.g. coinbase overpay (`main.cpp:~2765`) or bad
Sapling root (`main.cpp:~2754`) — after script checks are queued but before the
explicit `control.Wait()`. The control destructor then drains those checks
against freed memory. **Impact: remote DoS (node crash).** RCE is theoretically
in the UAF class but not demonstrated.

### Fix
Declare `txdata` (and its `reserve()`) **before** `control`, so reverse-order
destruction always runs `~control`'s `Wait()` before `txdata` is destroyed, on
every return path. One-line move; `reserve()` still precedes the first
`emplace_back`, preserving pointer stability. See the inline comment at the
declaration site in `ConnectBlock`.

### Verification
- **gtest** (`src/gtest/test_validation.cpp`):
`Validation.CheckQueueControlDrainsQueuedCheckBeforeTxdataDestroyed`.
Deterministic (no worker threads → the queued check runs inside `~control`'s
`Wait()` on the calling thread). `EXPECT_TRUE(ran)` guards the
drain-before-destroy contract without needing a sanitizer; under
AddressSanitizer the buggy declaration order reports `heap-use-after-free`.
Compiles and passes.
- **AddressSanitizer proof** (standalone reproducer, throwaway, using the real
`CCheckQueue`/`CCheckQueueControl`): buggy declaration order →
`heap-use-after-free` in `~CCheckQueueControl → Wait → Loop → check()`;
fixed order → clean exit. Both observed.
Note: `--enable-asan` is unusable under Apple clang (`-static-libasan`
unsupported); the proof used `clang -fsanitize=address` directly.

---

## 2. Deep-reorg parking IBD gate — REVIEWED, SAFE (`dcbdf14e5`)

### What it changes
Gates the deep-reorg park in `AcceptBlock` behind `!IsInitialBlockDownload()`:

```cpp
if (GetBoolArg("-parkdeepreorg", true) && !IsInitialBlockDownload()) { ... }
```

### Why
During `-reindex`, blocks are loaded from `blk*.dat` out of height order, so
`chainActive.FindFork(pindex)` sees spurious deep forks (`fork depth > 1`) and
parks large swaths of the chain — the stall that forced `-parkdeepreorg=0` on a
from-genesis reindex (observed at height ~478543/478544).

### Review conclusion: safe
The "Deep Reorg Protection" feature (`d57bf7a5e`) has **three** layers; this
commit relaxes only the softest, and the two harder layers remain active during
the skip window:

| Layer | Behaviour | During the IBD skip window |
|-------|-----------|----------------------------|
| Parking (this commit) | Preemptively parks deep-fork blocks; unparks at 2× work | **Skipped during IBD** |
| Auto-finalization, depth `-maxreorgdepth` (default 10) | Finalizes a block 10 deep; conflicting forks rejected (`bad-fork-prior-finalized`, `main.cpp:3089`) | **Still active** (`main.cpp:~3253`) |
| `MAX_REORG_LENGTH = COINBASE_MATURITY-1 = 99` | Node shuts down on any reorg > 99 blocks (`main.cpp:3667`) | **Always active** |

Key safety property: `IsInitialBlockDownload()` **latches to false permanently**
after first catch-up. So after a node first reaches tip, parking is **always
on** again and cannot be forced off by an attacker staling the tip
(eclipse/partition). At-tip behaviour is therefore unchanged — the 51%/deep-reorg
defense is intact at tip. Parking is skipped only during `fReindex`/`fImporting`
or a node's genuine first sync.

### Residual nuance (informational, not a blocker)
The latch resets on process restart. A node restarted after being offline
> `nMaxTipAge` (~24h) re-enters `IBD=true` until it catches the backlog, so
parking is skipped during that catch-up. Finalization (depth 10) and the 99-block
cap still apply, and a visibly-catching-up node should not be trusted for
confirmations. Exposure is bounded and acceptable.

---

## 3. ZIP-209 checked arithmetic — included (`bbea3177e`)

Overflow-safe `CAmount` delta arithmetic for the shielded-pool value tracking
(`CheckedAdd`/`CheckedAddTo`). Hardening of pool accounting; no behavioural
change for in-range values.

---

## 4. Checkpoint hash enforcement at header acceptance — FIXED (audit #2, `6b53a6916`)

Added in a follow-up merge after the items above.

### Gap
`ContextualCheckBlockHeader` rejected only forks strictly **below** the last
checkpoint *present in `mapBlockIndex`* (`GetLastCheckpoint` +
`nHeight < pcheckpoint->nHeight`). It never rejected a header presented **at** a
checkpoint height with the wrong hash, and it depended on chain state — so a
fresh or eclipsed node could accept a forged chain that does not pass through the
compiled checkpoint hashes. Upstream's `CheckIndexAgainstCheckpoint` was absent.

### Fix
New `Checkpoints::CheckBlock(data, nHeight, hash)` (mirrors upstream) returns
false only when there is a checkpoint at `nHeight` and the hash differs. Called
in `ContextualCheckBlockHeader` under `fCheckpointsEnabled`: a mismatch is
rejected with `DoS(100)` / `REJECT_CHECKPOINT` / `"bad-fork-checkpoint"`. It
reads the **hardcoded** checkpoint map directly, so it is independent of
`mapBlockIndex` state and protects a fresh/eclipsed node during bootstrap.

### Safety
Pure tightening: canonical headers at checkpoint heights match by definition, so
honest peers are never rejected; an empty checkpoint map (regtest) is a no-op. No
activation height, no coordination. Unit test
`Checkpoints_tests/checkpoint_hash_lockin` covers no-checkpoint / match /
mismatch; all `Checkpoints_tests` pass and the UAF gtest still passes.

---

## Audit items still OPEN (not yet implemented)

From the June-2026 upstream-parity audit. Priority order (#2 now done — see §4):

1. **#3 — reindex size band-aids loosen live consensus (High).**
`GENEROUS_BLOCK_SIZE_LIMIT = 2MB` (`main.cpp:4370`) vs `MAX_BLOCK_SIZE =
200000` (`consensus.h:22`); `GENEROUS_TX_SIZE_LIMIT = 2MB` non-contextual
(`main.cpp:1230`) with the tight `MAX_TX_SIZE_AFTER_SAPLING = 102000`
(`consensus.h:27`) enforced only inside the **pre-Sapling** `!saplingActive`
branch (`main.cpp:1040`) → post-Sapling tx size effectively
unbounded to 2MB for live blocks. These are local patches, not an intentional
hardfork. **Load-bearing for reindex** (confirmed: real canonical tx at height
478544 is 125,811 B; another 122,415 B at 478596 — both exceed 102000). Fix:
keep the non-contextual bounds generous (so historical reindex passes) and add
the tight limits in `ContextualCheckTransaction`/`ContextualCheckBlock` gated
on a future activation height (coordinated soft fork).

2. **#4 — header-DoS hardening gap (Medium, unproven).** No modern
`nMinimumChainWork`/headers-presync staging. Has `MAX_HEADERS_RESULTS=160` +
checkpoint fork-rejection. Hardening gap, not a demonstrated exploit.

3. **#5 — timestamp adjustment (Low).** Raw `nTime - GetTime()` (`main.cpp:6225`).
Self-limiting (200-sample freeze; protective per the in-code issue-#4521
note). Signed-overflow hardening only.

### Confirmed sound (no action)
Value conservation / no-inflation path: coinbase overpay rejected
(`main.cpp:~2765`), input/value conservation (`main.cpp:~2141`), ZIP-209
negative-pool checks (`main.cpp:~2597`), Sapling/JoinSplit verification
(`main.cpp:~951`). Duplicate-input protection (`1342`), CVE-2012-2459 merkle
malleability (`4357`), Overwinter non-overwintered-tx rejection (`1027`). No
NU5/Orchard code in this fork. Structural block checks **do** run at
`AcceptBlock` (`CheckBlock` default `fCheckSizeLimits=true`, `main.cpp:4604`) —
only the redundant `ConnectBlock` re-check skips them below checkpoint.
Loading
Loading