Submitting Epochs

Overview

After finding a valid SHA-1 collision solution, miners submit it to the network to become the epoch proposer. The first valid submission wins the epoch, granting the submitter proposer status and any associated rewards. The provider exposes the submitEpoch() method for broadcasting solutions and the getEpochTemplate() method for retrieving the current mining target.

Submit Epoch Solution

Using submitEpoch()

The submitEpoch() method broadcasts a SHA-1 collision solution to the network for epoch proposer selection. This method is used by miners who have successfully computed a valid collision solution matching the current epoch's target difficulty.

The method accepts an EpochSubmissionParams object containing the solution, salt, public key, and optional graffiti. It returns a SubmittedEpoch object confirming the submission status. If the solution is valid and submitted before other miners, the submitter becomes the epoch proposer.

Method Signature

typescript
submitEpoch signature
async submitEpoch(
    params: EpochSubmissionParams
): Promise<SubmittedEpoch>

EpochSubmissionParams Structure

The EpochSubmissionParams interface defines the required data for submitting an epoch solution.

typescript
EpochSubmissionParams structure
interface EpochSubmissionParams {
    readonly epochNumber: bigint;         // Epoch number to submit for
    readonly checksumRoot: Uint8Array;    // Checksum root
    readonly salt: Uint8Array;            // 32-byte salt used in collision
    readonly mldsaPublicKey: Uint8Array;  // ML-DSA public key
    readonly signature: Uint8Array;       // ML-DSA signature
    readonly graffiti?: Uint8Array;       // Optional message (max 32 bytes)
}

SubmittedEpoch Result

The SubmittedEpoch interface represents the result of an epoch submission.

typescript
SubmittedEpoch structure
interface SubmittedEpoch {
    readonly epochNumber: bigint;            // Epoch that was submitted to
    readonly submissionHash: Uint8Array;     // Hash of the submission
    readonly difficulty: number;             // Difficulty of the submission
    readonly timestamp: Date;                // When the submission was made
    readonly status: SubmissionStatus;       // Acceptance status
    readonly message?: string;               // Additional status message
}

enum SubmissionStatus {
    ACCEPTED = 'accepted',
    REJECTED = 'rejected',
}

Basic Submission

The following example demonstrates submitting a SHA-1 collision solution for an epoch. The submission includes the epoch number, checksum root, salt used in the solution, ML-DSA public key, and a signature proving ownership of the key.

typescript
Basic submission
import { JSONRpcProvider, EpochSubmissionParams } from 'opnet';
import { networks, fromHex } from '@btc-vision/bitcoin';

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

// Prepare submission parameters
const submission: EpochSubmissionParams = {
    epochNumber: 100n,
    checksumRoot: fromHex('your-checksum-root-here...'),
    salt: fromHex('your-32-byte-salt-here...'),
    mldsaPublicKey: fromHex('your-mldsa-public-key...'),
    signature: fromHex('your-mldsa-signature...'),
};

// Submit the solution
const result = await provider.submitEpoch(submission);

console.log('Submission Result:');
console.log('  Status:', result.status);
console.log('  Epoch:', result.epochNumber);

With Graffiti

The optional graffiti field allows miners to include a custom message up to 32 bytes in their submission. This can be used to identify the mining pool, software version, or any other brief identifier visible on-chain.

typescript
Submission with graffiti
// Include optional graffiti message (max 32 bytes)
const submissionWithGraffiti: EpochSubmissionParams = {
    epochNumber: template.epochNumber,
    checksumRoot: checksumRoot,
    salt: solutionSalt,
    mldsaPublicKey: mldsaPublicKey,
    signature: solutionSignature,
    graffiti: new TextEncoder().encode('Mined by MyPool v1.0'),
};

const result = await provider.submitEpoch(submissionWithGraffiti);

Handling Submission Results

Check Submission Status

Submission results should be handled based on the returned status. An accepted submission means the miner has become the epoch proposer, while rejected submissions include a message explaining the failure reason. The following example demonstrates proper status handling with error management.

typescript
Check Submission Status
async function submitAndHandle(
    provider: JSONRpcProvider,
    params: EpochSubmissionParams
): Promise<boolean> {
    try {
        const result = await provider.submitEpoch(params);

        switch (result.status) {
            case 'accepted':
                console.log(`Successfully proposed epoch ${result.epochNumber}!`);
                console.log(`  Difficulty: ${result.difficulty}`);
                return true;

            case 'rejected':
                console.log('Submission rejected:', result.message);
                return false;

            default:
                console.log('Unknown status:', result.status);
                return false;
        }
    } catch (error) {
        console.error('Submission failed:', error);
        return false;
    }
}

// Usage
const success = await submitAndHandle(provider, submission);
if (success) {
    console.log('You are the epoch proposer!');
}

Verify Submission On-Chain

After submitting a solution, the result can be verified by fetching the epoch and checking if the proposer matches the expected address. The following example demonstrates confirming that a submission was accepted and the submitter became the epoch proposer.

typescript
Verify Submission On-Chain
async function verifySubmission(
    provider: JSONRpcProvider,
    epochNumber: bigint,
    expectedProposer: string
): Promise<boolean> {
    try {
        const epoch = await provider.getEpochByNumber(epochNumber);
        return epoch.proposer.publicKey.toHex() === expectedProposer;
    } catch {
        return false;
    }
}

// Usage
const isProposer = await verifySubmission(
    provider,
    100n,
    'bc1p...my-address...'
);

if (isProposer) {
    console.log('Confirmed as epoch proposer!');
}

Submission Workflow

Mining and Submitting

The following example demonstrates a complete epoch mining implementation. The miner fetches the current template, computes SHA-1 collision solutions by iterating through random salts, and submits valid solutions to the network. The solution is found when the hash of the XOR'd preimage matches the target hash with sufficient matching bits.

typescript
Mining and submitting
import { createHash } from 'crypto';

class EpochMiner {
    private provider: JSONRpcProvider;
    private mldsaPublicKey: Uint8Array;
    private minDifficulty: number = 20;

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

    async mineAndSubmit(
        graffiti?: string
    ): Promise<SubmittedEpoch | null> {
        const template = await this.provider.getEpochTemplate();
        console.log('Mining epoch:', template.epochNumber);

        const checksumRoot = template.epochTarget;
        const targetHash = createHash('sha1').update(checksumRoot).digest();

        const solution = await this.findSolution(checksumRoot, targetHash);

        if (!solution) {
            console.log('No solution found');
            return null;
        }

        console.log(`Solution found with ${solution.matchingBits} matching bits`);

        const params: EpochSubmissionParams = {
            epochNumber: template.epochNumber,
            checksumRoot: checksumRoot,
            salt: solution.salt,
            mldsaPublicKey: this.mldsaPublicKey,
            signature: this.signSolution(solution.salt), // Sign with ML-DSA key
            graffiti: graffiti
                ? new TextEncoder().encode(graffiti.slice(0, 32))
                : undefined,
        };

        return this.provider.submitEpoch(params);
    }

    private calculatePreimage(
        checksumRoot: Uint8Array,
        salt: Uint8Array
    ): Uint8Array {
        const target32 = new Uint8Array(32);
        const pubKey32 = new Uint8Array(32);
        const salt32 = new Uint8Array(32);

        target32.set(checksumRoot.subarray(0, Math.min(32, checksumRoot.length)));
        pubKey32.set(this.mldsaPublicKey.subarray(0, Math.min(32, this.mldsaPublicKey.length)));
        salt32.set(salt.subarray(0, Math.min(32, salt.length)));

        const preimage = new Uint8Array(32);
        for (let i = 0; i < 32; i++) {
            preimage[i] = target32[i] ^ pubKey32[i] ^ salt32[i];
        }

        return preimage;
    }

    private countMatchingBits(hash1: Uint8Array, hash2: Uint8Array): number {
        let matchingBits = 0;
        const minLength = Math.min(hash1.length, hash2.length);

        for (let i = 0; i < minLength; i++) {
            if (hash1[i] === hash2[i]) {
                matchingBits += 8;
            } else {
                for (let bit = 7; bit >= 0; bit--) {
                    if (((hash1[i] >> bit) & 1) === ((hash2[i] >> bit) & 1)) {
                        matchingBits++;
                    } else {
                        return matchingBits;
                    }
                }
            }
        }

        return matchingBits;
    }

    private async findSolution(
        checksumRoot: Uint8Array,
        targetHash: Uint8Array
    ): Promise<{ salt: Uint8Array; matchingBits: number } | null> {
        const maxAttempts = 10000000;

        for (let i = 0; i < maxAttempts; i++) {
            const salt = crypto.getRandomValues(new Uint8Array(32));

            const preimage = this.calculatePreimage(checksumRoot, salt);
            const hash = createHash('sha1').update(preimage).digest();

            const matchingBits = this.countMatchingBits(hash, targetHash);

            if (matchingBits >= this.minDifficulty) {
                return { salt, matchingBits };
            }
        }

        return null;
    }

    private signSolution(salt: Uint8Array): Uint8Array {
        // Sign with ML-DSA private key
        // Implementation depends on your ML-DSA library
        return new Uint8Array(0); // Placeholder
    }
}

// Usage
const miner = new EpochMiner(provider, wallet.mldsaKeypair.publicKey);
const result = await miner.mineAndSubmit('MyMiner v1.0');

if (result?.status === 'accepted') {
    console.log('Successfully proposed epoch', result.epochNumber);
}

Competitive Submission

For continuous mining operations, the previous example can be extended into a competitive miner that runs indefinitely. The following implementation monitors for epoch changes, restarts mining when a new epoch begins, and automatically submits solutions as they are found. This pattern is suitable for dedicated mining operations that compete for epoch proposer status across multiple epochs.

typescript
Race to submit
class CompetitiveMiner {
    private provider: JSONRpcProvider;
    private mldsaPublicKey: Uint8Array;
    private isRunning: boolean = false;
    private minDifficulty: number = 20;

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

    async startMining(graffiti?: string): Promise<void> {
        this.isRunning = true;

        while (this.isRunning) {
            const template = await this.provider.getEpochTemplate();
            const startEpoch = template.epochNumber;

            console.log(`Mining epoch ${startEpoch}...`);

            const checksumRoot = template.epochTarget;
            const targetHash = createHash('sha1').update(checksumRoot).digest();

            while (this.isRunning) {
                const current = await this.provider.getEpochTemplate();
                if (current.epochNumber !== startEpoch) {
                    console.log('Epoch changed, restarting...');
                    break;
                }

                const solution = await this.mineBatch(checksumRoot, targetHash, 100000);

                if (solution) {
                    const params: EpochSubmissionParams = {
                        epochNumber: template.epochNumber,
                        checksumRoot: checksumRoot,
                        salt: solution.salt,
                        mldsaPublicKey: this.mldsaPublicKey,
                        signature: this.signSolution(solution.salt),
                        graffiti: graffiti
                            ? new TextEncoder().encode(graffiti.slice(0, 32))
                            : undefined,
                    };

                    try {
                        const result = await this.provider.submitEpoch(params);

                        if (result.status === 'accepted') {
                            console.log(`WON epoch ${result.epochNumber}!`);
                        } else {
                            console.log('Submission rejected:', result.status);
                        }
                    } catch (error) {
                        console.error('Submission error:', error);
                    }

                    break;
                }
            }
        }
    }

    stop(): void {
        this.isRunning = false;
    }

    private calculatePreimage(checksumRoot: Uint8Array, salt: Uint8Array): Uint8Array {
        const target32 = new Uint8Array(32);
        const pubKey32 = new Uint8Array(32);
        const salt32 = new Uint8Array(32);

        target32.set(checksumRoot.subarray(0, Math.min(32, checksumRoot.length)));
        pubKey32.set(this.mldsaPublicKey.subarray(0, Math.min(32, this.mldsaPublicKey.length)));
        salt32.set(salt.subarray(0, Math.min(32, salt.length)));

        const preimage = new Uint8Array(32);
        for (let i = 0; i < 32; i++) {
            preimage[i] = target32[i] ^ pubKey32[i] ^ salt32[i];
        }

        return preimage;
    }

    private countMatchingBits(hash1: Uint8Array, hash2: Uint8Array): number {
        let matchingBits = 0;
        const minLength = Math.min(hash1.length, hash2.length);

        for (let i = 0; i < minLength; i++) {
            if (hash1[i] === hash2[i]) {
                matchingBits += 8;
            } else {
                for (let bit = 7; bit >= 0; bit--) {
                    if (((hash1[i] >> bit) & 1) === ((hash2[i] >> bit) & 1)) {
                        matchingBits++;
                    } else {
                        return matchingBits;
                    }
                }
            }
        }

        return matchingBits;
    }

    private async mineBatch(
        checksumRoot: Uint8Array,
        targetHash: Uint8Array,
        batchSize: number
    ): Promise<{ salt: Uint8Array; matchingBits: number } | null> {
        for (let i = 0; i < batchSize; i++) {
            const salt = crypto.getRandomValues(new Uint8Array(32));

            const preimage = this.calculatePreimage(checksumRoot, salt);
            const hash = createHash('sha1').update(preimage).digest();

            const matchingBits = this.countMatchingBits(hash, targetHash);

            if (matchingBits >= this.minDifficulty) {
                return { salt, matchingBits };
            }
        }
        return null;
    }

    private signSolution(salt: Uint8Array): Uint8Array {
        // Sign with ML-DSA private key
        return new Uint8Array(0); // Placeholder
    }
}

// Usage
const competitiveMiner = new CompetitiveMiner(provider, wallet.mldsaKeypair.publicKey);
competitiveMiner.startMining('CompetitiveMiner');

// Later: stop mining
// competitiveMiner.stop();

Error Recovery

In addition to mining and submitting, handling transient network failures improves mining reliability. The following example implements retry logic with exponential backoff for submissions that fail due to network errors, while avoiding retries for definitive acceptance or rejection responses.

typescript
Handle submission failures
async function submitWithRetry(
    provider: JSONRpcProvider,
    params: EpochSubmissionParams,
    maxRetries: number = 3
): Promise<SubmittedEpoch | null> {
    let lastError: Error | null = null;

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            const result = await provider.submitEpoch(params);

            // Don't retry on definitive results
            if (result.status === 'accepted' || result.status === 'rejected') {
                return result;
            }

            console.log(`Attempt ${attempt} returned unexpected status: ${result.status}`);

        } catch (error) {
            lastError = error as Error;
            console.log(`Attempt ${attempt} error:`, error);
        }

        // Wait before retry
        if (attempt < maxRetries) {
            await new Promise(r => setTimeout(r, 1000 * attempt));
        }
    }

    console.error('All retries failed:', lastError);
    return null;
}

// Usage
const result = await submitWithRetry(provider, submission);
if (result?.status === 'accepted') {
    console.log('Submission accepted after retries');
}

Submission Monitoring

Tracking submission history provides insight into mining performance and success rates over time. The following example demonstrates a utility class that records submission results, calculates acceptance statistics, and retrieves epochs won by the miner.

typescript
Track your submissions
interface SubmissionRecord {
    epochNumber: bigint;
    timestamp: number;
    status: string;
    wasAccepted: boolean;
}

class SubmissionTracker {
    private submissions: SubmissionRecord[] = [];

    record(result: SubmittedEpoch): void {
        this.submissions.push({
            epochNumber: result.epochNumber,
            timestamp: Date.now(),
            status: result.status,
            wasAccepted: result.status === 'accepted',
        });
    }

    getStats(): {
        total: number;
        accepted: number;
        rejected: number;
        acceptRate: number;
    } {
        const accepted = this.submissions.filter(s => s.wasAccepted).length;
        const rejected = this.submissions.length - accepted;

        return {
            total: this.submissions.length,
            accepted,
            rejected,
            acceptRate: this.submissions.length > 0
                ? (accepted / this.submissions.length) * 100
                : 0,
        };
    }

    getRecentSubmissions(count: number = 10): SubmissionRecord[] {
        return this.submissions.slice(-count);
    }

    getEpochsWon(): bigint[] {
        return this.submissions
            .filter(s => s.wasAccepted)
            .map(s => s.epochNumber);
    }
}

// Usage
const tracker = new SubmissionTracker();

// After each submission
tracker.record(result);

// Check stats
const stats = tracker.getStats();
console.log(`Submissions: ${stats.total}, Won: ${stats.accepted}`);
console.log(`Accept rate: ${stats.acceptRate.toFixed(1)}%`);

Complete Submission Service

typescript
Submission service
class EpochSubmissionService {
    private provider: JSONRpcProvider;
    private tracker: SubmissionTracker;

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

    async submit(
        epochNumber: bigint,
        checksumRoot: Uint8Array,
        salt: Uint8Array,
        mldsaPublicKey: Uint8Array,
        signature: Uint8Array,
        graffiti?: string
    ): Promise<SubmittedEpoch> {
        const params: EpochSubmissionParams = {
            epochNumber,
            checksumRoot,
            salt,
            mldsaPublicKey,
            signature,
            graffiti: graffiti
                ? new TextEncoder().encode(graffiti.slice(0, 32))
                : undefined,
        };

        const result = await this.provider.submitEpoch(params);
        this.tracker.record(result);

        return result;
    }

    async submitWithVerification(
        epochNumber: bigint,
        checksumRoot: Uint8Array,
        salt: Uint8Array,
        mldsaPublicKey: Uint8Array,
        signature: Uint8Array,
        publicKeyHex: string,
        graffiti?: string
    ): Promise<{
        submitted: SubmittedEpoch;
        verified: boolean;
    }> {
        const result = await this.submit(epochNumber, checksumRoot, salt, mldsaPublicKey, signature, graffiti);

        let verified = false;
        if (result.status === 'accepted') {
            // Wait a moment for propagation
            await new Promise(r => setTimeout(r, 2000));

            // Verify on-chain
            const epoch = await this.provider.getEpochByNumber(result.epochNumber);
            verified = epoch.proposer.publicKey.toHex() === publicKeyHex;
        }

        return { submitted: result, verified };
    }

    getStats() {
        return this.tracker.getStats();
    }

    getEpochsWon(): bigint[] {
        return this.tracker.getEpochsWon();
    }
}

// Usage
const submissionService = new EpochSubmissionService(provider);

const { submitted, verified } = await submissionService.submitWithVerification(
    template.epochNumber,
    checksumRoot,
    solutionSalt,
    mldsaPublicKey,
    solutionSignature,
    minerPublicKeyHex,
    'MyMiner'
);

console.log('Status:', submitted.status);
console.log('Verified on-chain:', verified);
console.log('Total wins:', submissionService.getEpochsWon().length);