ox
Version:
483 lines • 15.4 kB
JavaScript
import { keccak_256 } from '@noble/hashes/sha3';
import * as Address from '../core/Address.js';
import * as Bytes from '../core/Bytes.js';
import * as Errors from '../core/Errors.js';
import * as Hash from '../core/Hash.js';
import * as Hex from '../core/Hex.js';
import * as VirtualMasterPool from './internal/virtualMasterPool.js';
import * as TempoAddress from './TempoAddress.js';
import * as VirtualAddress from './VirtualAddress.js';
const tip20Prefix = '0x20c000000000000000000000';
const zeroAddress = '0x0000000000000000000000000000000000000000';
/**
* Computes the TIP-1022 registration hash for a master address and salt.
*
* [TIP-1022](https://docs.tempo.xyz/protocol/tips/tip-1022)
*
* The registration hash is `keccak256(masterAddress || salt)` where `salt`
* is encoded as a 32-byte value.
*
* Master addresses must satisfy TIP-1022 registration constraints: they cannot
* be the zero address, another virtual address, or a TIP-20 token address.
*
* @example
* ```ts twoslash
* import { Address, Hex } from 'ox'
* import { VirtualMaster } from 'ox/tempo'
*
* const hash = VirtualMaster.getRegistrationHash({
* address: Address.from('0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'),
* salt: Hex.from('0x00000000000000000000000000000000000000000000000000000000abf52baf'),
* })
*
* hash
* // @log: '0x0000000058e21090d8f4bee424b90cddc2378aefa1bbbfa1443631a929ae966d'
* ```
*
* @param value - Master address and salt.
* @returns The registration hash.
*/
export function getRegistrationHash(value) {
return Hash.keccak256(Hex.concat(resolveAddress(value.address), toFixedHex(value.salt, 32)));
}
/**
* Derives the 4-byte TIP-1022 `masterId` from a master address and salt.
*
* [TIP-1022](https://docs.tempo.xyz/protocol/tips/tip-1022)
*
* This returns bytes `[4:8]` of the registration hash, regardless of whether the
* salt satisfies the proof-of-work requirement.
*
* Master addresses must satisfy TIP-1022 registration constraints: they cannot
* be the zero address, another virtual address, or a TIP-20 token address.
*
* @example
* ```ts twoslash
* import { Address, Hex } from 'ox'
* import { VirtualMaster } from 'ox/tempo'
*
* const masterId = VirtualMaster.getMasterId({
* address: Address.from('0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'),
* salt: Hex.from('0x00000000000000000000000000000000000000000000000000000000abf52baf'),
* })
*
* masterId
* // @log: '0x58e21090'
* ```
*
* @param value - Master address and salt.
* @returns The derived master identifier.
*/
export function getMasterId(value) {
return Hex.slice(getRegistrationHash(value), 4, 8);
}
/**
* Validates that a salt satisfies the TIP-1022 32-bit proof-of-work requirement.
*
* [TIP-1022](https://docs.tempo.xyz/protocol/tips/tip-1022)
*
* Returns `false` for invalid master addresses, including the zero address,
* virtual addresses, and TIP-20 token addresses.
*
* @example
* ```ts twoslash
* import { Address, Hex } from 'ox'
* import { VirtualMaster } from 'ox/tempo'
*
* const valid = VirtualMaster.validateSalt({
* address: Address.from('0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'),
* salt: Hex.from('0x00000000000000000000000000000000000000000000000000000000abf52baf'),
* })
*
* valid
* // @log: true
* ```
*
* @param value - Master address and salt.
* @returns `true` if the first 4 bytes of the registration hash are zero.
*/
export function validateSalt(value) {
try {
return hasProofOfWork(Hash.keccak256(Hex.concat(resolveAddress(value.address), toFixedHex(value.salt, 32)), { as: 'Bytes' }));
}
catch {
return false;
}
}
/**
* Searches a bounded range of salts for the first value that satisfies TIP-1022 PoW.
*
* [TIP-1022](https://tips.sh/1022)
*
* This is intentionally a small, deterministic primitive. It does not coordinate
* workers or async execution. Callers that need large searches can shard ranges
* externally.
*
* Master addresses must satisfy TIP-1022 registration constraints: they cannot
* be the zero address, another virtual address, or a TIP-20 token address.
*
* :::warning
*
* It is strongly recommended to use {@link ox#VirtualMaster.(mineSaltAsync:function)} instead of this
* function. `mineSaltAsync` uses WASM-accelerated keccak256 with parallel
* workers and is a lot faster than the pure JS implementation used here.
*
* :::
*
* @example
* ```ts twoslash
* import { Address } from 'ox'
* import { VirtualMaster } from 'ox/tempo'
*
* const result = VirtualMaster.mineSalt({
* address: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
* })
*
* result?.salt
* // @log: '0x00000000000000000000000000000000000000000000000000000000abf52baf'
* ```
*
* @param value - Search range parameters.
* @returns The first matching salt in the range, if any.
*/
export function mineSalt(value) {
const count = value.count ?? 2 ** 32;
assertCount(count);
const addressBytes = Bytes.fromHex(resolveAddress(value.address));
const input = new Uint8Array(addressBytes.length + 32);
input.set(addressBytes);
// Salt is a view into input — increment mutates input directly, no copy.
const saltView = input.subarray(addressBytes.length);
saltView.set(toFixedBytes(value.start ?? 0n, 32));
for (let i = 0; i < count; i++) {
const hash = keccak_256(input);
if (hash[0] === 0 && hash[1] === 0 && hash[2] === 0 && hash[3] === 0) {
return {
masterId: Hex.fromBytes(hash.subarray(4, 8)),
registrationHash: Hex.fromBytes(hash),
salt: Hex.fromBytes(saltView),
};
}
if (i < count - 1 && !increment(saltView))
break;
}
return undefined;
}
/**
* Searches for a salt that satisfies TIP-1022 PoW using parallel workers and
* WASM-accelerated keccak256.
*
* [TIP-1022](https://tips.sh/1022)
*
* Uses WASM-accelerated keccak256 with parallel
* workers when available. Falls back to chunked single-threaded mining in
* environments without worker support.
*
* - **Node.js / Bun / Deno**: Spawns `worker_threads` with inline WASM keccak256.
* - **Browsers**: Spawns Web Workers via Blob URLs with inline WASM keccak256.
*
* @example
* ```ts twoslash
* import { Address } from 'ox'
* import { VirtualMaster } from 'ox/tempo'
*
* const result = await VirtualMaster.mineSaltAsync({
* address: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
* })
* ```
*
* @param parameters - Search parameters.
* @returns The first matching salt, if any.
*/
export async function mineSaltAsync(parameters) {
const { chunkSize = 100_000, count = 2 ** 32, onProgress, signal, start: start_ = 0n, workers = getDefaultWorkerCount(), } = parameters;
const address = resolveAddress(parameters.address);
const start = toFixedHex(start_, 32);
assertCount(count);
if (workers !== undefined)
assertWorkers(workers);
throwIfAborted(signal);
const workerCount = Math.max(1, Math.min(workers, Math.ceil(count / chunkSize)));
if (workerCount <= 1)
return mineSaltAsyncFallback({
address,
chunkSize,
count,
onProgress,
signal,
start,
});
const pool = await VirtualMasterPool.resolve();
if (!pool)
return mineSaltAsyncFallback({
address,
chunkSize,
count,
onProgress,
signal,
start,
});
return mineSaltWithWorkerPool({
address,
chunkSize,
count,
onProgress,
pool,
signal,
start,
workerCount,
});
}
/**
* Runs parallel salt mining across a worker pool.
*
* @internal
*/
// biome-ignore lint/correctness/noUnusedVariables: _
function mineSaltWithWorkerPool(value) {
const startedAt = Date.now();
return new Promise((resolve, reject) => {
let settled = false;
let attempts = 0;
let completedWorkers = 0;
const handles = [];
const emitProgress = () => {
if (!value.onProgress)
return;
const elapsed = Date.now() - startedAt;
const seconds = elapsed / 1000;
value.onProgress({
attempts,
chunkSize: value.chunkSize,
count: value.count,
elapsed,
progress: Math.min(1, attempts / value.count),
rate: seconds === 0 ? 0 : attempts / seconds,
workers: value.workerCount,
});
};
const terminateAll = () => {
for (const h of handles)
h.terminate();
};
const succeed = (result) => {
if (settled)
return;
settled = true;
value.signal?.removeEventListener('abort', onAbort);
terminateAll();
resolve(result);
};
const fail = (error) => {
if (settled)
return;
settled = true;
value.signal?.removeEventListener('abort', onAbort);
terminateAll();
reject(error instanceof Error
? error
: new Errors.BaseError('Failed to mine virtual master salt.'));
};
const onMessage = (msg) => {
if (settled)
return;
switch (msg.type) {
case 'found':
succeed(msg.result);
return;
case 'progress':
attempts += msg.attempts;
emitProgress();
return;
case 'done':
completedWorkers++;
if (completedWorkers === value.workerCount)
succeed(undefined);
return;
case 'error':
fail(new Errors.BaseError(msg.message));
return;
}
};
const onAbort = () => fail(getAbortError(value.signal));
value.signal?.addEventListener('abort', onAbort, { once: true });
for (let i = 0; i < value.workerCount; i++) {
const handle = value.pool.spawn(i, onMessage, fail);
handles.push(handle);
handle.postMessage({
type: 'start',
address: value.address,
chunkSize: value.chunkSize,
count: value.count,
start: value.start,
workerCount: value.workerCount,
workerIndex: i,
});
}
});
}
/**
* Single-threaded chunked fallback when no worker pool is available.
*
* @internal
*/
// biome-ignore lint/correctness/noUnusedVariables: _
async function mineSaltAsyncFallback(value) {
const startedAt = Date.now();
const startBigInt = BigInt(value.start);
for (let offset = 0; offset < value.count; offset += value.chunkSize) {
throwIfAborted(value.signal);
const count = Math.min(value.chunkSize, value.count - offset);
const result = mineSalt({
address: value.address,
count,
start: startBigInt + BigInt(offset),
});
if (value.onProgress) {
const attempts = Math.min(value.count, offset + count);
const elapsed = Date.now() - startedAt;
const seconds = elapsed / 1000;
value.onProgress({
attempts,
chunkSize: value.chunkSize,
count: value.count,
elapsed,
progress: Math.min(1, attempts / value.count),
rate: seconds === 0 ? 0 : attempts / seconds,
workers: 1,
});
}
if (result)
return result;
// Yield to the event loop between chunks.
await new Promise((r) => setTimeout(r, 0));
}
return undefined;
}
/**
* Asserts that `workers` is a non-negative safe integer.
*
* @internal
*/
function assertWorkers(workers) {
if (Number.isSafeInteger(workers) && workers >= 0)
return;
throw new Errors.BaseError(`Workers "${workers}" is invalid. Expected a non-negative safe integer.`);
}
/**
* Extracts or creates an error from an `AbortSignal`.
*
* @internal
*/
function getAbortError(signal) {
const reason = signal?.reason;
if (reason instanceof Error)
return reason;
return new Errors.BaseError('The operation was aborted.');
}
/**
* Returns the default number of workers for the current platform.
*
* @internal
*/
function getDefaultWorkerCount() {
if (typeof navigator !== 'undefined') {
const c = navigator.hardwareConcurrency;
if (c && c > 1)
return c - 1;
}
return 1;
}
/**
* Throws the signal's abort reason if the signal is aborted.
*
* @internal
*/
function throwIfAborted(signal) {
if (!signal?.aborted)
return;
throw getAbortError(signal);
}
/**
* Returns `true` if the first 4 bytes of a hash are zero.
*
* @internal
*/
function hasProofOfWork(hash) {
return hash[0] === 0 && hash[1] === 0 && hash[2] === 0 && hash[3] === 0;
}
/**
* Asserts that `count` is a positive safe integer.
*
* @internal
*/
function assertCount(count) {
if (Number.isSafeInteger(count) && count > 0)
return;
throw new Errors.BaseError(`Count "${count}" is invalid. Expected a positive safe integer.`);
}
/**
* Increments a big-endian byte array by one. Returns `false` on overflow.
*
* @internal
*/
function increment(bytes) {
for (let i = bytes.length - 1; i >= 0; i--) {
const value = bytes[i];
if (value === 0xff) {
bytes[i] = 0;
continue;
}
bytes[i] = value + 1;
return true;
}
return false;
}
/**
* Resolves a Tempo or hex address, validates it as a valid master.
*
* @internal
*/
function resolveAddress(address) {
const resolved = TempoAddress.resolve(address);
Address.assert(resolved, { strict: false });
assertValidMasterAddress(resolved);
return resolved;
}
/**
* Throws if the address is zero, virtual, or a TIP-20 token.
*
* @internal
*/
function assertValidMasterAddress(address) {
const normalized = address.toLowerCase();
if (normalized === zeroAddress)
throw new Errors.BaseError('Virtual master address cannot be the zero address.');
if (VirtualAddress.isVirtual(address))
throw new Errors.BaseError('Virtual master address cannot itself be a virtual address.');
if (normalized.startsWith(tip20Prefix))
throw new Errors.BaseError('Virtual master address cannot be a TIP-20 token address.');
}
/**
* Converts a salt to a fixed-size byte array.
*
* @internal
*/
function toFixedBytes(value, size) {
return Bytes.fromHex(toFixedHex(value, size));
}
/**
* Converts a salt to a zero-padded hex string of the given size.
*
* @internal
*/
function toFixedHex(value, size) {
if (typeof value === 'number' || typeof value === 'bigint')
return Hex.fromNumber(value, { size });
if (typeof value === 'string') {
Hex.assert(value, { strict: true });
return Hex.padLeft(value, size);
}
return Hex.fromBytes(Bytes.padLeft(value, size));
}
//# sourceMappingURL=VirtualMaster.js.map