§ 11 · Blockchain
Forks and upgrades
Four rule changes, each switched on at a block height fixed in advance
On this page 5 sections
A blockchain’s rules live in software that many independent people run. Changing a rule means getting all of them to change at the same moment, or the network splits into nodes that accept a block and nodes that don’t.
Pearl’s answer is simple: pick a block height, write it into the next release, and give everyone time to upgrade. When the chain reaches that height, the new rule applies, on every up-to-date node at once. Four rules have changed this way so far.
What is a fork?
The word has two meanings in blockchains.
- A chain split: two blocks on the same parent, for a moment. That’s ordinary and resolves itself; see the most-work chain.
- A rule change: a new version of the rules. That’s this page.
Rule changes come in two kinds:
| Soft forkSoft fork: A rule change that only tightens the rules, so old software still accepts the new blocks. Pearl's soft forks came at blocks 91,630 and 96,251. | Hard forkHard fork: A rule change that old software rejects, so every node must upgrade. Pearl has had two, at blocks 71,935 and 99,000, each requiring a new certificate version. | |
|---|---|---|
| What changes | The rules get stricter | Something new is allowed |
| Old nodes | Still accept every new block | Reject new blocks and fall behind |
| Who must upgrade | Miners, so they don’t make blocks the new rules reject | Everyone: nodes, miners, pools |
| On Pearl | Dense-only (91,630), rank penalty (96,251) | MoE (71,935), salted noise seeds (99,000) |
How does Pearl schedule a change?
With a number in the chain parameters. Each fork has a height, and helper functions like IsMoEForkActive(height) answer “does this rule apply here?” Nodes check the certificate rules for every block against its height. The software even refuses to start if the rank-penalty or salted-seed height is set before the MoE height, since both rules assume V2 certificates exist.
Miners don’t vote. The header’s version field still carries Bitcoin’s BIP-9 version bitsVersion bits: Bitcoin’s scheme for miners to signal support for an upgrade by setting bits in the header’s version field. Pearl inherited the machinery but upgrades at fixed heights instead, so the bits mean nothing., but Pearl’s mainnet defines only three test dummy deployments. Blocks from about 2,100 to 100,799 carry version 0x20400000 (bit 22 set), most likely because of one of those dummies; either way it means nothing. See version.
What are the four forks?
| Height | Activated | Fork | Kind | What changed |
|---|---|---|---|---|
| 71,935 | 2026-06-12 | MoE hard fork | hard | Blocks must carry a V2 certificate, which adds support for Mixture-of-Experts proofs. |
| 91,630 | 2026-07-23 | Dense-only soft fork | soft | Mixture-of-Experts proofs are rejected again; only dense proofs count. |
| 96,251 | 2026-08-06 | Rank-penalty soft fork | soft | Noise rank must be at least 128, and a higher rank makes the winning bar stricter in proportion. |
| 99,000 | 2026-08-11 | Salted noise-seed hard fork | hard | Blocks must carry a V3 certificate: the noise seeds now also commit to the matrix sizes. |
All four touch the mining certificateCertificate: The proof of work that travels in front of every block: about 60 KB, mostly a zero-knowledge proof. Any node can check it in milliseconds without seeing the miner's matrices., never transactions or the header layout.
MoE hard fork, block 71,935
From this height, blocks must carry a V2 certificate instead of V1. V2 made the certificate’s public dataPublic data: The 164-byte readable part of a certificate: the mining settings, fingerprints of both matrices, the winning ticket, the matrix sizes and the tile's position. The header's proof commitment is a hash of it. variable in length, so it could describe mixture-of-expertsMixture of experts: An AI model design that sends each word to a few specialist sub-networks. Pearl accepted mining proofs from such layers from block 71,935, and has rejected them again since block 91,630. jobs: the matrix multiplications inside MoE layers of large language models, where each token is routed to a few “expert” weight matrices.
Dense proofs, the ordinary kind, stayed valid in V2, so hashing miners didn’t need to change. Nodes and the software that turns a share into a certificate did: an old node rejects a V2 certificate. That’s what made it hard.
Dense-only soft fork, block 91,630
From this height, a V2 certificate must be dense: public data longer than 164 bytes (an MoE proof) is rejected. The set of valid blocks only shrank, so old nodes kept accepting everything, a soft fork. The height moved from 91,600 to 91,630 during development, after a fix for empty block templates being mistaken for MoE.
Why MoE proofs were switched off again hasn’t been published. MoE support continues in Pearl’s separate FP8 work, which is not live.
Rank-penalty soft fork, block 96,251
From this height, the noise rank must be at least 128, and a certificate with a higher rank faces a stricter bar, scaled by 128 ÷ rank. Pearl’s own explanation: a proof’s ticket bar “is scaled by the work one attempt costs, and the noise rank divides that work, so mining at a larger rank buys a proportionally easier bound for the same effort.” The penalty cancels that advantage. Details are on rank penalty.
Salted noise-seed hard fork, block 99,000
From this height, blocks must carry a V3 certificate. The bytes are laid out exactly like V2; what changed is how the noise seeds are derived. Each matrix fingerprint is now salted with the matrix size the miner claims, so one committed matrix can no longer be “read” under several shapes to collect extra tickets, a form of work amplificationWork amplification: Getting more lottery tickets than the work done justifies, for example by reading one committed matrix under several shapes. Pearl closed the known form with salted noise seeds at block 99,000..
Because the noise changed, miners had to upgrade too this time. The height moved twice in development, from 98,830 to 98,900 to 99,000. A related security advisory is referenced in Pearl’s code history but not public; see noise seeds.
Can you see the forks on chain?
Yes, in the very first bytes of each raw block, which start with the certificate version:
| Block | First four bytes | Certificate |
|---|---|---|
| 71,934 | 01000000 |
V1 |
| 71,935 | 02000000 |
V2 |
| 98,999 | 02000000 |
V2 |
| 99,000 | 03000000 |
V3 |
Here is block 1’s certificate, a V1, and block 130,827’s, a V3:
| Field | Bytes | Value |
|---|---|---|
| Certificate version certificate.version | 4 @0 | 1 u32 LE. V1 before block 71,935, V2 until 98,999, V3 (salted noise seeds) from 99,000. 0 = no certificate. |
| Block hash certificate.blockHash | 32 @4 | a09851a4761539d170f7dfe7531864ae92879b02bbe999a6c238f63745a69726 SHA256d of the 108-byte header this certificate proves (stored byte-reversed); must equal the header’s hash. |
| Common dimension k certificate.publicData.k | 4 @36 | 4096 u32 LE: the shared dimension of A (m×k) and B (k×n). |
| Noise rank r certificate.publicData.rank | 2 @40 | 128 u16 LE: rank of the low-rank noise; must be ≥ 128 since block 96,251. |
| MMA type certificate.publicData.mmaType | 2 @42 | 0 0 = Int7 × Int7 → Int32. |
| Rows pattern certificate.publicData.rowsPattern | 6 @44 | [0,8,64,72,128,136,192,200] (h=8) Periodic row offsets of the hash tile, stored as (factor−1, length−1) × 3. |
| Columns pattern certificate.publicData.colsPattern | 6 @50 | [0,1,8,9,32,33,40,41,64,65,72,73,96,97,104,105] (w=16) Periodic column offsets of the hash tile. |
| Experts e certificate.publicData.experts | 2 @56 | 0 Number of MoE experts; 0 = dense (MoE is invalid on mainnet since 91,630). |
| Top-k certificate.publicData.topK | 2 @58 | 0 0 for dense jobs. |
| Reserved certificate.publicData.reserved | 28 @60 | 00000000000000000000000000000000000000000000000000000000 Must be zero. |
| hash_A certificate.publicData.hashA | 32 @88 | f979a482b148ad659aed4fe23f13724797538eeda9e89d92bd2f575bd6479bc7 Keyed-BLAKE3 Merkle root of matrix A (key = job key). |
| hash_B certificate.publicData.hashB | 32 @120 | a8eaf0d7b08f0a6a5fd0b6adbf08cdc2e0e649035047d1a373a5d7d12641047f Keyed-BLAKE3 Merkle root of Bᵀ (B column-major). |
| hash_jackpot certificate.publicData.hashJackpot | 32 @152 | 7e0c03ed79c5602451370ad176151ac5568124a98e3d3dc95576534f07000000 The winning ticket: BLAKE3 keyed with seed_A over the tile transcript; read as a little-endian number and compared with the bound. |
| m (rows of A) certificate.publicData.m | 4 @184 | 32768 u32 LE; bound into the noise seeds since certificate V3. |
| n (columns of B) certificate.publicData.n | 4 @188 | 28672 u32 LE; bound into the noise seeds since certificate V3. |
| Tile row offset certificate.publicData.tRows | 4 @192 | 30752 Smallest row index of the winning hash tile. |
| Tile column offset certificate.publicData.tCols | 4 @196 | 20480 Smallest column index of the winning hash tile. |
| Proof length certificate.proofLength | 4 @200 | 59560 u32 LE; at most 60,000 bytes. |
| ZK proof (first 32 of 59,560 bytes) certificate.proof | 32 @204 | 59,560 bytes, collapsed pow_bits(3) ‖ rate_bits(3) ‖ zeta(16) ‖ Plonky2 proof: a STARK wrapped in two Plonky2 recursions. Not covered by the block hash. |
| Field | Bytes | Value |
|---|---|---|
| Certificate version certificate.version | 4 @0 | 3 u32 LE. V1 before block 71,935, V2 until 98,999, V3 (salted noise seeds) from 99,000. 0 = no certificate. |
| Block hash certificate.blockHash | 32 @4 | 1df2f2c1bf39b1a444e622c1788c11736da2f91f4e9a875a38dc23063c6d45b9 SHA256d of the 108-byte header this certificate proves (stored byte-reversed); must equal the header’s hash. |
| Public data length certificate.publicDataLength | 4 @36 | 164 164 for dense jobs; longer means MoE (invalid since 91,630). |
| Common dimension k certificate.publicData.k | 4 @40 | 8192 u32 LE: the shared dimension of A (m×k) and B (k×n). |
| Noise rank r certificate.publicData.rank | 2 @44 | 128 u16 LE: rank of the low-rank noise; must be ≥ 128 since block 96,251. |
| MMA type certificate.publicData.mmaType | 2 @46 | 0 0 = Int7 × Int7 → Int32. |
| Rows pattern certificate.publicData.rowsPattern | 6 @48 | [0,8,32,40,64,72,96,104] (h=8) Periodic row offsets of the hash tile, stored as (factor−1, length−1) × 3. |
| Columns pattern certificate.publicData.colsPattern | 6 @54 | [0,1,32,33,64,65,96,97,128,129,160,161,192,193,224,225] (w=16) Periodic column offsets of the hash tile. |
| Experts e certificate.publicData.experts | 2 @60 | 0 Number of MoE experts; 0 = dense (MoE is invalid on mainnet since 91,630). |
| Top-k certificate.publicData.topK | 2 @62 | 0 0 for dense jobs. |
| Reserved certificate.publicData.reserved | 28 @64 | 00000000000000000000000000000000000000000000000000000000 Must be zero. |
| hash_A certificate.publicData.hashA | 32 @92 | 9e1adb4617d7d05111fe2a7e074f81738df4e2181b966ac48f66289b1cab5d4a Keyed-BLAKE3 Merkle root of matrix A (key = job key). |
| hash_B certificate.publicData.hashB | 32 @124 | d1b42f82cfe36192a36ed37be30a6f9f75e7bca372449aec615e7d0721dc06ef Keyed-BLAKE3 Merkle root of Bᵀ (B column-major). |
| hash_jackpot certificate.publicData.hashJackpot | 32 @156 | 7540013c2866ec7f6b076e1445f907276b9f27ede2b298603604000000000000 The winning ticket: BLAKE3 keyed with seed_A over the tile transcript; read as a little-endian number and compared with the bound. |
| m (rows of A) certificate.publicData.m | 4 @188 | 131072 u32 LE; bound into the noise seeds since certificate V3. |
| n (columns of B) certificate.publicData.n | 4 @192 | 131072 u32 LE; bound into the noise seeds since certificate V3. |
| Tile row offset certificate.publicData.tRows | 4 @196 | 86148 Smallest row index of the winning hash tile. |
| Tile column offset certificate.publicData.tCols | 4 @200 | 67602 Smallest column index of the winning hash tile. |
| Proof length certificate.proofLength | 4 @204 | 59560 u32 LE; at most 60,000 bytes. |
| ZK proof (first 32 of 59,560 bytes) certificate.proof | 32 @208 | 59,560 bytes, collapsed pow_bits(3) ‖ rate_bits(3) ‖ zeta(16) ‖ Plonky2 proof: a STARK wrapped in two Plonky2 recursions. Not covered by the block hash. |
Are any other changes scheduled?
Not in v1.5.2. A search of the code finds no other height-gated rules, and the v1.5.x releases add no new fork height. Two smaller changes in those releases apply without a height: OP_CHECKXMSSSIG now fails on a wrong-length key or signature chunk, and spending an output created later in the same block is rejected, as in Bitcoin Core.
Pearl Research Labs is working on a new mining scheme (FP8) with a certificate V4. It is not live; if it ships, it will be another fork with its own height.
Make a guess
A pool still runs node software from before 71,935 when the MoE fork arrives. What happens to the blocks its miners find?
Think of your answer first, then check it.
Answer Every up-to-date node rejects them.
At 71,935 and above, up-to-date nodes require a V2 certificate. The old software keeps building V1 blocks, which everyone else rejects, so the pool’s blocks never join the main chain and its rewards are lost. That’s why the upgrade guides told node operators to upgrade well before the fork height.
Code: fork heights on every network
From params.go (node/chaincfg/params.go:364-371 at v1.5.2, on GitHub) and the testnet sections of the same file:
| Fork | Mainnet | Testnet | Testnet2 | Regtest |
|---|---|---|---|---|
| MoE | 71,935 | 1 | 54,869 | 1 |
| Dense-only | 91,630 | 28,262 | 80,051 | off |
| Rank penalty | 96,251 | 36,761 | 80,627 | 1 |
| Salted seed | 99,000 | 38,648 | 83,109 | 1 |
Simnet sets MoE and salted seed to 1. The certificate version a height needs comes from
RequiredCertVersion (node/chaincfg/params.go:326-338 at v1.5.2, on GitHub):
func (p *Params) RequiredCertVersion(height int32) wire.CertificateVersion {
if p.IsSaltedSeedForkActive(height) {
return wire.CertificateVersionV3
}
if p.IsMoEForkActive(height) {
return wire.CertificateVersionV2
}
return wire.CertificateVersionV1
} CheckCertificateRules (node/blockchain/validate.go:508-559 at v1.5.2, on GitHub) enforces it, along with
dense-only and the rank penalty, for every block and every synced header.