Duplicate Prevention
Every mining solution must be globally unique. Without duplicate prevention, a miner could submit the same valid nonce multiple times, minting unlimited XPOW from a single proof-of-work. XPower prevents this through a pair index — a compact encoding that maps each (beneficiary, blockHash, nonce) tuple to a unique storage slot.
The Pair Index
The pair index is computed during the mint() call alongside the nonce hash:
function _hashOf(address to, bytes32 blockHash, bytes calldata data)
internal view returns (bytes32, bytes32)
{
bytes32 nonceHash = keccak256(
bytes.concat(bytes20(uint160(address(this)) ^ uint160(to)), blockHash, data)
);
return (nonceHash, nonceHash ^ blockHash);
}The function returns two values:
| Return Value | Symbol | Formula | Purpose |
|---|---|---|---|
| Nonce hash | Checked for leading zero nibbles | ||
| Pair index | Uniqueness guard |
The pair index is derived by XOR-ing the nonce hash with the block hash:
One-to-One Mapping
Given fixed blockHash and beneficiary, the mapping from data (nonce) to pair index is injective (one-to-one):
This holds because:
is collision-resistant: different datavalues produce differentwith overwhelming probability - XOR with a constant (
) is a bijection:
Conversely, given a pair index
This reversibility is intentional — it means the pair index encodes the same amount of information as the nonce hash, just in a different form.
Why XOR?
The XOR operation with the block hash serves a specific purpose: scope binding.
Without the XOR, the uniqueness guard would be
The same nonce produces different pair indices for different block hashes. If a miner finds a nonce for block hash
More critically, XOR prevents a miner from predicting which pair indices will be valid before the block hash is known, since everyone mines against the same block hash per interval. If pair index were simply
The Combined Uniqueness Tuple
The effective uniqueness constraint spans three dimensions:
The beneficiary is embedded in the nonce hash (via the XOR in the preimage), so it's not separately checked — two different beneficiaries with the same data and blockHash will produce different nonce hashes and therefore different pair indices. The storage key path is:
_hashes[pairIndex] → bool (used or not)A bool mapping entry costs 20,000 gas for the initial write (SSTORE from zero to non-zero), equivalent to approximately:
- 0.0005 AVAX at 25 nAVAX gas price
- A small fraction of the total
mint()gas cost (~150,000 gas)
Replay Attack Resistance
Consider a miner who finds a valid nonce and submits:
XPower.mint(alice, blockHash_B, data)→ succeeds,_hashes[p] = trueXPower.mint(alice, blockHash_B, data)→ reverts withDuplicatePairIndex(p)
The re-submission fails because _unique(p) checks:
function _unique(bytes32 pairIndex) private view returns (bool) {
return !_hashes[pairIndex];
}Now consider a miner attempting to reuse the nonce with a different beneficiary:
XPower.mint(bob, blockHash_B, data)— The preimage changes becausecontract ^ bob != contract ^ alice, producing a different nonce hash and pair index. This is either unused (succeeds, but requires a different nonce hash that must independently satisfy the difficulty check) or collides (vanishingly unlikely).
Cross-Contract Replay
The contract address is part of the nonce hash preimage via address(this) ^ beneficiary. A nonce that produces a valid solution for one XPower deployment will not produce a valid solution for a different deployment (e.g., testnet vs. mainnet), because the XOR of contract address and beneficiary changes.
Storage Implications
Gas Cost
Each successful mint() performs one SSTORE:
_hashes[pairIndex] = true;This is an irreversible write — pair indices are never cleared. Over time, the _hashes mapping accumulates entries proportional to the total number of mining solutions submitted.
| Metric | Value |
|---|---|
Gas per initial SSTORE | 20,000 |
Gas per subsequent SLOAD (check) | 2,100 (warm) / 100 (cold SLOAD, EIP-2929) |
| Data per entry | 32 bytes (key) + 1 byte (value) → ~33 bytes of state |
| 1 million solutions | ~33 MB of state growth |
For context, 1 million solutions corresponds to approximately 500,000 XPOW minted (at
State Growth
The _hashes mapping grows monotonically — entries are never deleted. This is by design:
- Immutability of proof-of-work — Once a nonce has been used to mint tokens, the record must persist permanently to prevent re-submission
- No cleanup mechanism — Unlike NFT burn or token transfer, there is no concept of "undoing" a mining solution
The trade-off is accepted because:
- The growth rate is bounded by the gas cost of
mint()itself — each new entry costs 20,000 gas, making mass state-bloating economically self-limiting - Avalanche's state storage model (C-Chain as an EVM subnet) supports large state growth without compromising consensus performance
- Future protocol upgrades could introduce state expiry or snapshot-based pruning if needed
Comparison to UTXO Model
Bitcoin's UTXO set also grows indefinitely, but UTXOs can be spent (destroyed). XPower's pair index set is strictly append-only. However:
- Bitcoin's UTXO set is ~100 million entries after 15+ years
- XPower's pair index set would reach comparable size only after billions of solutions — a scale that would require sustained, economically irrational mining activity
Edge Cases and Guarantees
Nonce Hash Collision
If two different nonces produce the same nonce hash (a Keccak-256 collision), they would also produce the same pair index — and the second submission would revert. Keccak-256 has
Beneficiary Collision
If contract ^ alice == contract ^ bob, then alice == bob (XOR with the same operand is injective), so two different beneficiaries always produce different preimages.
Block Hash = 0
If blockHash == 0 (impossible — Avalanche block hashes are non-zero, and blockhash(block.number - 1) for block.number > 256 returns zero, but the 1-hour window means the block is always within 256 blocks of the current tip), then pairIndex == nonceHash. This would reduce XOR to a no-op, but the scenario cannot occur in practice.
Same Nonce, Different Interval
A nonce valid in interval