Skip to content

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:

solidity
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 ValueSymbolFormulaPurpose
Nonce hashhnkeccak256(contracttoblockHashdata)Checked for leading zero nibbles
Pair indexphnblockHashUniqueness guard

The pair index is derived by XOR-ing the nonce hash with the block hash:

p=hnblockHash

One-to-One Mapping

Given fixed blockHash and beneficiary, the mapping from data (nonce) to pair index is injective (one-to-one):

data1data2:p(data1)p(data2)

This holds because:

  1. keccak256 is collision-resistant: different data values produce different hn with overwhelming probability
  2. XOR with a constant (blockHash) is a bijection: hnK=hnKhn=hn

Conversely, given a pair index p, there is exactly one nonce hash that produces it:

hn=pblockHash

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 keccak(nonce) alone. A nonce that works for block hash B1 would also be valid for block hash B2, enabling cross-interval replay. With the XOR:

p=keccak(nonce)B

The same nonce produces different pair indices for different block hashes. If a miner finds a nonce for block hash B1 and submits it, the pair index p1 is consumed. In the next interval, the same nonce against block hash B2 produces a different pair index p2p1, which has not been used — but recalculating the nonce hash against B2 would yield a different result entirely, so this is not a practical concern.

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 keccak(nonce), an attacker could pre-compute a large set of valid nonces and front-run other miners' submissions.

The Combined Uniqueness Tuple

The effective uniqueness constraint spans three dimensions:

(beneficiary,blockHash,pairIndex)

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:

  1. XPower.mint(alice, blockHash_B, data) → succeeds, _hashes[p] = true
  2. XPower.mint(alice, blockHash_B, data)reverts with DuplicatePairIndex(p)

The re-submission fails because _unique(p) checks:

solidity
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 because contract ^ 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.

MetricValue
Gas per initial SSTORE20,000
Gas per subsequent SLOAD (check)2,100 (warm) / 100 (cold SLOAD, EIP-2929)
Data per entry32 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 z=1 average) — the state growth is proportional to mining activity.

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 2256 possible outputs; the birthday bound for a 50% collision probability is approximately 2128 attempts, which is computationally infeasible.

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 N (block hash BN) has pair index pN=hnBN. In interval N+1 (block hash BN+1), the same nonce would produce a different pair index pN+1=hnBN+1. However, the nonce hash hn incorporates BN, not BN+1, so the solution is not valid for the new interval regardless of the pair index.