Skip to content

Block Hash Intervals

XPower mining operates within 1-hour windows anchored to Avalanche block hashes. This temporal binding prevents pre-computation attacks and ensures every mined solution is tied to a specific point in the chain's history.

The 1-Hour Window

The mining interval is determined by wall-clock time, independent of block numbers:

solidity
function currentInterval() public view returns (uint256) {
    return block.timestamp / (1 hours);
}

An interval is a discrete 1-hour bucket. For example, Unix timestamp 1718400000 maps to interval 477333, and 1718403599 maps to the same interval. At 1718403600, the interval advances to 477334.

Initializing a Block Hash

To begin mining, a transaction must register the most recent Avalanche block hash for the current interval:

solidity
function init() external {
    uint256 interval = currentInterval();
    if (uint256(_blockHashes[interval]) == 0) {
        bytes32 blockHash = blockhash(block.number - 1);
        uint256 timestamp = block.timestamp;
        _blockHashes[interval] = blockHash;
        _timestamps[blockHash] = timestamp;
        emit Init(blockHash, timestamp);
    } else {
        bytes32 blockHash = _blockHashes[interval];
        uint256 timestamp = _timestamps[blockHash];
        emit Init(blockHash, timestamp);
    }
}

Key behaviors:

  • First caller wins — The first init() call in an interval stores blockhash(block.number - 1) as the canonical block hash for that interval
  • Idempotent — Subsequent init() calls in the same interval return the already-cached block hash; they do not overwrite it
  • Timestamp recording — The block.timestamp at initialization is stored alongside the block hash, used later for freshness validation

This means only one block hash per interval is valid for mining. All miners in a given 1-hour window mine against the same block hash.

Freshness Validation

The mint() function requires the supplied blockHash to be "recent":

solidity
function _recent(bytes32 blockHash) private view returns (bool) {
    return _timestamps[blockHash] >= currentInterval() * (1 hours);
}

A block hash is considered recent if its cached timestamp is at or after the start of the current interval. In practice:

ScenarioResult
init() called at interval N, _timestamps[bh] = tt is within interval N
mint() called in interval NcurrentInterval() * 1h = t_start; t >= t_start → valid
mint() called in interval N+1currentInterval() * 1h = t_start + 1h; t < t_startexpired

The maximum effective validity window is from the init() timestamp until the end of that same interval — anywhere from near-zero (if initialized at the very end of an interval) to slightly over 3,599 seconds (if initialized at the start).

Pre-Computation Prevention

Without temporal binding, a miner could pre-compute nonces against arbitrary block hashes — perhaps against block hashes that haven't even been produced yet — and submit solutions instantly, defeating the purpose of proof-of-work.

The 1-hour window prevents this class of attack:

  1. A block hash cannot be used for mining until it's been recorded via init()
  2. init() only accepts blockhash(block.number - 1) — a block that already exists on-chain
  3. The mined solution must use this same block hash in its preimage
  4. Solutions expire after the current interval ends

This creates a commit-reveal temporal binding: the block hash is committed at init(), and the miner has at most the remainder of the 1-hour interval to find and submit a valid nonce against it.

Multi-Block Strategy

A savvy miner should call init() as early as possible in each new 1-hour interval to maximize the mining window. The strategy:

  1. Monitor intervals — Track block.timestamp / 3600; when it changes, a new interval has begun
  2. Init immediately — Call XPower.init() to register the t1 block hash for the new interval
  3. Mine the full window — Compute nonces against the registered block hash for up to 3,600 seconds
  4. Submit before expiry — Ensure mint() is called within the same interval as init()

Why Not Use the Genesis Approach?

Bitcoin miners continuously hash against the latest block, starting over each time a new block arrives (~10 minutes). In XPower, the block hash is fixed for a full hour. This is deliberate:

  • Browser miners operate at much lower hash rates than ASIC farms. A 10-minute window would be insufficient for in-browser mining to find solutions at medium-to-high difficulties
  • Deterministic interval — The fixed 1-hour window is predictable and independent of Avalanche's variable block times (~2 seconds per block)
  • Fairness — All miners in an interval compete on the same block hash; there is no advantage to being the first to init()

Strategy for Low Difficulty

At z=1 (expected 16 hashes per solution), a browser miner can expect to find a solution in under a second. In this case, the miner should:

  • Call init() once per interval
  • Search nonces and submit each solution immediately
  • Expect multiple solutions per interval (subject to the pairIndex uniqueness constraint)

Strategy for High Difficulty

At z=8 (expected ~4.3B hashes per solution), in-browser mining is impractical. Miners targeting high difficulty should:

  • Use native Keccak-256 implementations (Rust, C++)
  • Pre-compute nonces across the full interval
  • Submit only when a solution is found (avoid wasting gas on failed attempts)
  • Accept that some intervals may yield no solution

Critical Audit Finding: Block Hash Validation

The mint() function accepts any blockHash bytes32 argument as long as it passes the freshness check. It does not verify that the supplied blockHash matches the one stored in _blockHashes[interval].

solidity
function mint(address to, bytes32 blockHash, bytes calldata data) external {
    require(_recent(blockHash), ExpiredBlockHash(blockHash));
    // ... computes hash using the caller-supplied blockHash ...
}

This means:

  • If two different init() calls were made in the same interval (by different miners in different Avalanche blocks), each would cache a different block hash
  • A mint() call could use any block hash that was cached within the current interval, not necessarily the one the miner init()-ed

Practical impact: This is not an exploitable vulnerability because:

  1. _recent() validates that the block hash was cached within the current interval
  2. Only block hashes from init() calls in the current interval pass the freshness check
  3. Any cached block hash from the current interval is a legitimate Avalanche block hash — there is no advantage to using one over another

The audit finding (Hacken.io, January 2024) noted this design choice and confirmed it does not create a security risk, though it means the contract trusts the caller's choice of block hash within the set of recently-cached hashes.

Edge Cases

Interval Boundary Racing

If a miner calls init() at t = 1718403595 (5 seconds before interval end) and then calls mint() at t = 1718403605 (5 seconds into the next interval), the mint will revert because:

_timestamps[bh] = 1718403595  <  currentInterval() * 1h = 1718403600

The solution is to call init() again in the new interval and mine against the new block hash.

Empty Blocks

If Avalanche produces an empty block (no transactions), blockhash(block.number - 1) is still a valid, non-zero hash. The init() call will succeed regardless of the previous block's contents.

Same Interval, Different Blocks

If miner A calls init() at block N and miner B calls init() at block N+50 (both within the same 1-hour interval), miner B's init() will return miner A's cached block hash — the _blockHashes[interval] entry is already non-zero. Both miners mine against the same block hash.

Practical Timing

On Avalanche C-Chain, where block times average ~2 seconds:

  • A 1-hour interval spans approximately 1,800 Avalanche blocks
  • The init() transaction typically confirms in 2–6 seconds
  • A browser miner at ~10,000 hashes/second can search ~36 million nonces per interval
  • At z=4 (expected ~66K hashes), a browser miner finds approximately 550 solutions per interval — but each (beneficiary, blockHash, pairIndex) is single-use, so the effective cap depends on the uniqueness constraint