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:
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:
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 storesblockhash(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.timestampat 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":
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:
| Scenario | Result |
|---|---|
init() called at interval _timestamps[bh] = t | t is within interval |
mint() called in interval | currentInterval() * 1h = t_start; t >= t_start → valid |
mint() called in interval | currentInterval() * 1h = t_start + 1h; t < t_start → expired |
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:
- A block hash cannot be used for mining until it's been recorded via
init() init()only acceptsblockhash(block.number - 1)— a block that already exists on-chain- The mined solution must use this same block hash in its preimage
- 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:
- Monitor intervals — Track
block.timestamp / 3600; when it changes, a new interval has begun - Init immediately — Call
XPower.init()to register theblock hash for the new interval - Mine the full window — Compute nonces against the registered block hash for up to 3,600 seconds
- Submit before expiry — Ensure
mint()is called within the same interval asinit()
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
- 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
- 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].
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 minerinit()-ed
Practical impact: This is not an exploitable vulnerability because:
_recent()validates that the block hash was cached within the current interval- Only block hashes from
init()calls in the current interval pass the freshness check - 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 = 1718403600The 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 init() at block 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
(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