PoW Challenges

Overview

OP_NET transactions require a proof-of-work (PoW) challenge to be solved and included when signing. This mechanism provides additional security by preventing transaction spam and ensuring that submitters expend computational resources before broadcasting. The provider exposes methods to retrieve the current challenge parameters, which are automatically integrated into the transaction signing process by the library.

Fetching the Current Challenge

Using getChallenge()

The getChallenge() method retrieves the current proof-of-work challenge parameters from the network. This method returns the challenge data required for transaction signing, including the difficulty target and any associated metadata. The challenge is typically fetched automatically by the library when preparing a transaction.

The returned Challenge object contains the challenge identifier, difficulty level, and expiration information. Applications that require manual challenge management can use this method to fetch and cache challenges, though most use cases rely on the library's automatic handling during transaction submission.

Method Signature

typescript
getChallenge() signature
async getChallenge(): Promise<ChallengeSolution>

ChallengeSolution Structure

The ChallengeSolution interface represents a solved proof-of-work challenge. The preimage field contains the solution data that satisfies the difficulty requirement, while reward indicates the incentive for solving. The optional difficulty field specifies the target difficulty level, and version identifies the challenge protocol version for compatibility.

typescript
ChallengeSolution structure
interface ChallengeSolution {
    preimage: Uint8Array;   // Challenge preimage data
    reward: bigint;         // Reward for solving
    difficulty?: bigint;    // Current difficulty
    version?: number;       // Challenge version
}

ProofOfWorkChallenge Structure in Transactions

When a transaction is processed, it includes the solved proof-of-work challenge as part of its payload. The ProofOfWorkChallenge interface mirrors the ChallengeSolution structure, containing the preimage solution, reward amount, and optional difficulty and version fields. The library automatically attaches this data during transaction signing, ensuring valid challenge inclusion without manual intervention.

typescript
ProofOfWorkChallenge structure
interface ProofOfWorkChallenge {
    preimage: Uint8Array;   // Challenge preimage
    reward: bigint;         // Reward amount
    difficulty?: bigint;    // Difficulty level
    version?: number;       // Version number
}

Basic Challenge Query

The following example demonstrates how to retrieve and inspect the current proof-of-work challenge. The challenge data includes the preimage, reward amount, difficulty level, and protocol version, providing all necessary information for challenge solving and transaction preparation.

typescript
Basic challenge query
import { JSONRpcProvider } from 'opnet';
import { networks, toHex } from '@btc-vision/bitcoin';

const network = networks.regtest;
const provider = new JSONRpcProvider({ url: 'https://regtest.opnet.org', network });

const challenge = await provider.getChallenge();

console.log('Challenge:');
console.log('  Preimage:', toHex(challenge.preimage));
console.log('  Reward:', challenge.reward);
console.log('  Difficulty:', challenge.difficulty);
console.log('  Version:', challenge.version);

Access PoW from Transaction

Completed transactions include the proof-of-work data that was submitted during signing. The pow property on a transaction provides access to the preimage, reward, and difficulty values used, enabling verification and analysis of the challenge solution included in the transaction.

typescript
Access PoW from transaction
const tx = await provider.getTransaction(txHash);

if (tx.pow) {
    console.log('Transaction PoW:');
    console.log('  Preimage:', toHex(tx.pow.preimage));
    console.log('  Reward:', tx.pow.reward);
    console.log('  Difficulty:', tx.pow.difficulty);
}

Challenge Validation

Challenges have a limited validity period and must be verified before use. If a challenge expires between retrieval and transaction submission, the transaction will be rejected. Applications should check challenge freshness before signing and be prepared to fetch a new challenge if the current one has expired. Implementing retry logic with fresh challenge retrieval ensures reliable transaction submission even during periods of high network latency.

Verify Challenge Freshness

Challenge validity can be verified by comparing a cached challenge against the current network challenge. If the preimages differ, the cached challenge has expired and a fresh one must be fetched before transaction submission. This check is useful when challenges are cached for performance optimization.

typescript
Verify challenge freshness
async function isChallengeValid(
    provider: JSONRpcProvider,
    challenge: ChallengeSolution
): Promise<boolean> {
    // Get current challenge
    const currentChallenge = await provider.getChallenge();

    // Compare preimages
    return challenge.preimage.length === currentChallenge.preimage.length &&
        challenge.preimage.every((b, i) => b === currentChallenge.preimage[i]);
}

// Usage
const challenge = await provider.getChallenge();
// ... some time passes ...
const stillValid = await isChallengeValid(provider, challenge);
console.log('Challenge still valid:', stillValid);
}

Handle Expired Challenges

When a challenge expires during transaction processing, the operation should be retried with a fresh challenge. Implementing a retry wrapper that catches challenge-related errors and automatically fetches new challenge data ensures robust transaction submission without manual intervention.

typescript
Handle expired challenges
async function executeWithFreshChallenge<T>(
    provider: JSONRpcProvider,
    operation: (challenge: ChallengeSolution) => Promise<T>,
    maxRetries: number = 3
): Promise<T> {
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            const challenge = await provider.getChallenge();
            return await operation(challenge);
        } catch (error: unknown) {
            const errorMessage = error instanceof Error ? error.message : '';
            if (errorMessage.includes('challenge') && attempt < maxRetries) {
                console.log(`Challenge expired, retrying (${attempt}/${maxRetries})...`);
                continue;
            }
            throw error;
        }
    }

    throw new Error('Max retries exceeded');
}

// Usage
const result = await executeWithFreshChallenge(provider, async (challenge) => {
    // Use challenge in transaction
    const simulation = await contract.transfer(recipient, amount, new Uint8Array(0));
    return simulation;
});

Difficulty Analysis

Challenge difficulty fluctuates based on network conditions and can impact transaction processing time. Tracking difficulty over time enables applications to identify optimal periods for transaction submission and estimate solving times. The following example demonstrates a utility class that records difficulty samples and calculates averages for trend analysis.

typescript
Track difficulty over time
interface DifficultyRecord {
    timestamp: number;
    difficulty: bigint;
    reward: bigint;
}

class DifficultyTracker {
    private records: DifficultyRecord[] = [];

    async recordCurrent(provider: JSONRpcProvider): Promise<void> {
        const challenge = await provider.getChallenge();

        this.records.push({
            timestamp: Date.now(),
            difficulty: challenge.difficulty ?? 0n,
            reward: challenge.reward,
        });

        // Keep last 100 records
        if (this.records.length > 100) {
            this.records.shift();
        }
    }

    getAverageDifficulty(): bigint {
        if (this.records.length === 0) return 0n;

        const sum = this.records.reduce(
            (acc, record) => acc + record.difficulty,
            0n
        );

        return sum / BigInt(this.records.length);
    }

    getAverageReward(): bigint {
        if (this.records.length === 0) return 0n;

        const sum = this.records.reduce(
            (acc, record) => acc + record.reward,
            0n
        );

        return sum / BigInt(this.records.length);
    }

    getRecords(): DifficultyRecord[] {
        return [...this.records];
    }
}

// Usage
const tracker = new DifficultyTracker();

// Record periodically
await tracker.recordCurrent(provider);

console.log('Average difficulty:', tracker.getAverageDifficulty());
console.log('Average reward:', tracker.getAverageReward());

Challenge Monitoring

Applications can monitor for challenge changes by polling the network at regular intervals and comparing preimages. When a new challenge is detected, the callback is invoked with the updated challenge data. This pattern is useful for applications that maintain cached challenges or need to react to difficulty adjustments in real-time.

typescript
Monitor challenge changes
async function monitorChallenges(
    provider: JSONRpcProvider,
    onNewChallenge: (challenge: ChallengeSolution) => void,
    intervalMs: number = 10000
): Promise<() => void> {
    let lastPreimage: string | null = null;

    const intervalId = setInterval(async () => {
        try {
            const challenge = await provider.getChallenge();
            const preimageHex = toHex(challenge.preimage);

            if (preimageHex !== lastPreimage) {
                lastPreimage = preimageHex;
                onNewChallenge(challenge);
            }
        } catch (error) {
            console.error('Error fetching challenge:', error);
        }
    }, intervalMs);

    return () => clearInterval(intervalId);
}

// Usage
const stopMonitoring = await monitorChallenges(provider, (challenge) => {
    console.log('New challenge detected!');
    console.log('  Reward:', challenge.reward);
    console.log('  Difficulty:', challenge.difficulty);
});

// Later: stop monitoring
// stopMonitoring();

Complete Challenge Service

typescript
ChallengeService
class ChallengeService {
    private provider: JSONRpcProvider;
    private cache: { challenge: ChallengeSolution; expireAt: number } | null = null;
    private cacheDuration: number = 30000; // 30 seconds

    constructor(provider: JSONRpcProvider) {
        this.provider = provider;
    }

    async getChallenge(): Promise<ChallengeSolution> {
        if (this.cache && Date.now() < this.cache.expireAt) {
            return this.cache.challenge;
        }

        const challenge = await this.provider.getChallenge();

        this.cache = {
            challenge,
            expireAt: Date.now() + this.cacheDuration,
        };

        return challenge;
    }

    async getReward(): Promise<bigint> {
        const challenge = await this.getChallenge();
        return challenge.reward;
    }

    async getDifficulty(): Promise<bigint> {
        const challenge = await this.getChallenge();
        return challenge.difficulty ?? 0n;
    }

    async getPreimage(): Promise<Uint8Array> {
        const challenge = await this.getChallenge();
        return challenge.preimage;
    }

    async isExpired(): Promise<boolean> {
        if (!this.cache) return true;
        return Date.now() >= this.cache.expireAt;
    }

    invalidate(): void {
        this.cache = null;
    }

    setCacheDuration(ms: number): void {
        this.cacheDuration = ms;
    }
}

// Usage
const challengeService = new ChallengeService(provider);

// Get current challenge
const challenge = await challengeService.getChallenge();
console.log('Challenge reward:', challenge.reward);

// Get specific properties
const reward = await challengeService.getReward();
const difficulty = await challengeService.getDifficulty();

console.log('Reward:', reward);
console.log('Difficulty:', difficulty);

// Check expiration
if (await challengeService.isExpired()) {
    console.log('Challenge cache expired');
}