Skip to content

Hardfork 2

Hardfork 2 activates at the block height configured by ChainParams.hardfork2_height. On mainnet this is block 5,050,000, projected around 25 August 2026 (UTC). This is a block height, not a VDF height.

v1.5.0 is the minimum required Node version to support hardfork 2, commit b58fa631d034503b1a9aeb3397387b5c64374586.

The new rules change block header hashing, challenge calculation, and smart contract argument handling.

Hardfork 2 introduces:

  • A cumulative proof chain that is committed to each block header.
  • Proof-chain infusion into challenge and space-fork calculations.
  • A maximum space-fork interval, after which an infusion is forced.
  • Correct map and object key handling when values cross a contract boundary.
  • Correct 256-bit multiplication overflow detection in the VM.
  • Deterministic ordering for implicit multi-currency deployment outputs.
  • Canonical type tags for variants in transaction hashes.
  • Canonical transaction solutions and stricter multi-signature validation.
  • The SUPPORT_HARDFORK2 block support flag (0x2).

The block header has a new proof_chain field. Let H be hardfork2_height, P[h] the block’s proof_hash, and C[h] its proof_chain.

In the formulas below, hash() is SHA-256 and || denotes raw byte concatenation.

At the activation block:

C[H] = P[H]

For every later block:

C[h] = hash("proof_chain" || C[h - 1] || P[h])

The activation block is the anchor: its previous block is below the hardfork height, so its proof chain is initialized to its own proof hash. Starting with the next block, every value commits to the previous proof chain and the current proof hash.

For blocks at height H and later:

  • proof_chain is included in the block header hash.
  • The node verifies proof_chain against the formula above.
  • Challenge infusion uses proof_chain instead of the current block’s proof_hash.

For blocks below H, proof_chain is not included in the block header hash and the previous consensus behavior is preserved.

The candidate challenge is first advanced once for each VDF point in the block:

candidate = previous_challenge
repeat block.vdf_count times:
candidate = hash("next_challenge" || candidate)

Below hardfork 2, the infusion value is the block’s proof_hash. Starting at hardfork 2, it is the block’s proof_chain.

A normal space fork occurs when:

hash("proof_infusion_check" || candidate || infusion_value) mod challenge_interval == 0

When a space fork occurs, the final challenge is:

hash("challenge_infusion" || candidate || infusion_value)

Otherwise, the candidate becomes the next challenge without an infusion.

Hardfork 2 also bounds the time between space forks. An infusion is forced when:

previous.space_fork_len + block.vdf_count > max_space_fork_len * challenge_interval

The comparison is strictly greater than. On mainnet, max_space_fork_len is 8 and challenge_interval is 256, so an infusion is forced when the accumulated length exceeds 2,048 VDF points.

A normal or forced space fork resets space_fork_len to the current block’s vdf_count and updates space difficulty using the existing space-fork adjustment rules.

MMX VM map keys are address-backed. Before hardfork 2, transaction arguments and remote-call arguments were copied into the callee engine before the callee binary constants were loaded. String keys in map or object arguments could therefore receive different internal addresses from identical keys used by the contract. This could make fields appear missing or prevent values from being persisted under the expected global map entries.

Starting at hardfork 2, contract execution uses this order:

  1. Load the callee binary and initialize its constant keys.
  2. Materialize the deposit and call arguments in the callee engine.
  3. Execute the method.

The new order applies to contract deployment, direct transaction calls, and nested remote calls. Below the activation height, nodes retain the original setup-before-load order to preserve consensus compatibility.

The mmx_compile test harness always uses the corrected load-before-arguments order; it does not emulate the pre-fork behavior.

Before hardfork 2, the VM detected unsigned 256-bit multiplication overflow by comparing the wrapped result with both operands. This misses some overflows, including (2^255 + 1) * 2, whose wrapped result is 2.

Starting at hardfork 2, multiplication instructions with overflow checking enabled reject the operation when the left operand is non-zero and the right operand exceeds MAX_UINT256 / left. Multiplication without overflow checking continues to wrap modulo 2^256. The legacy comparison remains active below the hardfork height so historical contract execution is unchanged. The mmx_compile execution harness uses the corrected behavior.

A deployment can leave input balances in multiple currencies that are implicitly deposited into the new contract. Before hardfork 2, these outputs were emitted by iterating an unordered map, so their order could depend on the standard library and platform.

Starting at hardfork 2, leftover currencies are ordered by address before their implicit outputs are appended. Below the hardfork height, the original unordered iteration is retained for historical transaction-result compatibility.

Before hardfork 2, boolean, signed-integer, and unsigned-integer variants were written to transaction hashes without a type tag. Adjacent argument values could therefore have the same byte encoding despite having different VM semantics. For example, [true, uint64(0)] and [uint64(1), false] produced the same operation hash.

Starting at hardfork 2, transactions must use version 1. Version-1 hashing prefixes boolean, signed 64-bit integer, and unsigned 64-bit integer variants with distinct type tags. A variant outside the accepted JSON value model is prefixed with variant<> before its opaque VNX encoding. This domain prefix prevents an unsupported encoding from colliding with an accepted encoding; it does not validate or reject the value. The version is propagated recursively through arrays and objects and into operation and deployment hashes. Transactions below the activation height must use version 0, which preserves all historical transaction IDs.

Wallets select the transaction version for the next block height. A version-0 transaction that has not been included before activation is no longer eligible for inclusion after hardfork 2.

Exchanges and other services using the official MMX node and wallet software need to upgrade those components to a hardfork-2-compatible release before activation. Services that construct and sign raw transactions themselves need to update their transaction builder:

  1. Select transaction version 0 when targeting a block below hardfork2_height, and version 1 when targeting the activation block or any later block. The node requires the version matching the candidate block height.
  2. Set the transaction version before calculating the transaction ID. Do not change it after signing.
  3. Sign the resulting transaction ID, attach the solutions, and then calculate the transaction content_hash.
  4. Rebuild and re-sign any pending version-0 withdrawal that was not included before activation. A version-0 transaction cannot be carried across the activation boundary, and a version-1 transaction cannot be included before activation.

For an ordinary transfer containing only inputs and outputs, the version field is the only hardfork-2 change to the transaction ID calculation. Contract calls and deployments must additionally implement the version-1 variant hashing rules described above.

Custom multi-signature builders must not include unused or duplicate top-level solutions. A version-1 multi-signature solution may contain signatures only from contract owners, must use the contract’s num_required value, and must use a public-key signature solution for every included owner.

A service using the official node API does not need to implement the new block rules itself. A service that parses or validates blocks independently must also support the proof_chain header field and all activation-height rules in this document.

Starting with transaction version 1, every top-level solution must be referenced by the sender, an input, or an authorized contract call. Top-level solutions must also have unique hashes. This prevents an otherwise unused or duplicate signature from being added while changing only non-cryptographic solution indexes. A single solution can still authorize multiple uses by sharing its index.

For a version-1 multi-signature solution, every map entry must belong to an owner of the multi-signature contract and must contain a public-key signature for that owner. Entries from non-owners and entries of other solution types are rejected instead of ignored, and the solution’s required-signature count must match the contract. Additional valid signatures from owners remain allowed. Version-0 transactions retain the legacy behavior for historical validation.

Hardfork-2-capable nodes set BlockHeader.SUPPORT_HARDFORK2 (0x2) in support_flags when producing blocks. Activation is determined by hardfork2_height; the support flag does not replace the height gate.

Block header hashing now requires chain parameters so it can decide whether proof_chain is part of the committed header. Accordingly, the internal BlockHeader::calc_hash(), BlockHeader::is_valid(), Block::finalize(), and Block::is_valid() APIs receive ChainParams.

The height gates preserve the original block hash, challenge calculation, and VM argument order for historical blocks. No historical block rewrite is required.

At the activation height, all validating and block-producing nodes must apply the new rules. Existing contracts do not need to be redeployed, but calls that pass maps or objects across a contract boundary will begin using the corrected key semantics.