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
async submitEpoch(
params: EpochSubmissionParams
): Promise<SubmittedEpoch>EpochSubmissionParams Structure
The EpochSubmissionParams interface defines the required data for submitting an epoch solution.
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.
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.
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.
// 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.
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.
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.
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.
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.
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.
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
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);