Zcash activated its NU7 network upgrade on November 6, 2026, at block 3,543,405. Two changes got most of the attention. The target time between blocks dropped from 75 seconds to 25, and ZIP 2003 made nodes reject version 4 transactions. Version 4 is the only format that can spend from Sprout, Zcash's first shielded pool, so the roughly 12,224 ZEC still sitting there (per ZecStats, as reported by Cointelegraph and Decrypt) can no longer be moved.
I wanted to check the block-time claim on the chain itself, without running a node and without trusting an explorer's summary. This post shows how to pull a raw block, find the timestamp in the header bytes, and compare the average interval before and after activation.
What ZIP 2003 actually changes
The consensus change is one line in the protocol spec. Before NU7:
The transaction version number MUST be 4 or 5 or 6.
From NU7 on:
The transaction version number MUST be 5 or 6.
Neither the v5 format (ZIP 225) nor v6 (ZIP 229) has fields for Sprout JoinSplits, so dropping v4 also drops the only way to spend Sprout notes. The ZIP says the funds are not burned and stay counted in issued supply, and that the ability to spend them "may never be re-enabled".
Any software that still produced v4 transactions for other reasons breaks as well, which is worth checking if you maintain a Zcash integration.
Where the timestamp lives
A Zcash block header is a fixed-size prefix followed by the Equihash solution:
| Field | Bytes | Offset |
|---|---|---|
nVersion |
4 | 0 |
hashPrevBlock |
32 | 4 |
hashMerkleRoot |
32 | 36 |
hashBlockCommitments |
32 | 68 |
nTime |
4 | 100 |
nBits |
4 | 104 |
nNonce |
32 | 108 |
nTime is a little-endian Unix timestamp at byte 100. Blockchair's public API returns the raw block as hex at /zcash/raw/block/<height>, so we take the first 108 bytes, read four bytes at offset 100, and we have the block's time.
The script
// Measure Zcash block times before and after NU7 from raw block headers.
// Usage: node block-times.mjs [activationHeight=3543405] [window=1000]
// Data: Blockchair's public raw-block endpoint (no key needed for a few calls).
const ACT = Number(process.argv[2] ?? 3543405);
const WIN = Number(process.argv[3] ?? 1000);
const API = 'https://api.blockchair.com/zcash';
async function json(url) {
const r = await fetch(url);
if (!r.ok) throw new Error(`${r.status} for ${url}`);
return r.json();
}
// Zcash header layout: version(4) | prevHash(32) | merkleRoot(32) |
// blockCommitments(32) | nTime(4, little-endian) | nBits(4) | nonce(32) | solution...
// so nTime sits at byte offset 100.
async function blockTime(height) {
const j = await json(`${API}/raw/block/${height}`);
const hex = j.data[height].raw_block;
const buf = Buffer.from(hex.slice(0, 216), 'hex');
return buf.readUInt32LE(100);
}
const tip = (await json(`${API}/stats`)).data.best_block_height;
const before = ACT - WIN;
const heights = [before, ACT, tip];
const t = {};
for (const h of heights) {
t[h] = await blockTime(h);
await new Promise((r) => setTimeout(r, 1200)); // be gentle with the free API
}
const avg = (a, b) => (t[b] - t[a]) / (b - a);
const iso = (s) => new Date(s * 1000).toISOString().replace('.000Z', 'Z');
console.log(`activation block ${ACT} at ${iso(t[ACT])}`);
console.log(`before: ${WIN} blocks (${before}..${ACT}) averaged ${avg(before, ACT).toFixed(1)} s`);
console.log(`after: ${tip - ACT} blocks (${ACT}..${tip}) averaged ${avg(ACT, tip).toFixed(1)} s`);
console.log(`target after NU7: 25 s (was 75 s)`);
It needs Node 18 or newer for the built-in fetch, and nothing else.
What it printed
I ran it at about 12:15 UTC on November 6:
activation block 3543405 at 2026-11-06T02:34:02Z
before: 1000 blocks (3542405..3543405) averaged 75.4 s
after: 1238 blocks (3543405..3544643) averaged 28.1 s
target after NU7: 25 s (was 75 s)
The activation time matches the 02:34 UTC that ZecStats reported. Before the fork, blocks landed almost exactly on the old 75-second target. After it, the average is already 28.1 seconds. It sits a little above 25 because difficulty adjusts over a window of past blocks, so the first few hours run slow. Run the script again in a day or two and the number should get closer to 25.
Caveats
- Block timestamps are set by miners and can be off by a bit. Averaging over hundreds of blocks smooths that out, but don't read much into any single interval.
- Taking the average from two endpoints is cheap and ignores what happens in between. If you want the distribution, fetch every block in the range, but mind the API's rate limits or point the same parser at your own
zebradnode. - Blockchair is a third party. You can check its bytes, though: hash the full header with double SHA-256, reverse the bytes, and compare the result with the block hash a second source gives you.
Top comments (0)