legalComments: 'inline',
outdir: 'dist',
target: 'esnext',
+ dropLabels: process.env.NODE_ENV !== 'production' ? [] : ['LOG'],
plugins: [
glsl({
minify: true,
CONFIG.EFFORT = +(process.env.NANO_POW_EFFORT ?? '') || CONFIG.EFFORT
CONFIG.PORT = process.send ? 0 : +(process.env.NANO_POW_PORT ?? '') || CONFIG.PORT
logger.isEnabled = CONFIG.DEBUG
- if (configFile.length === 0) logger.log('config file not found')
- logger.log(`config set: ${JSON.stringify(CONFIG)}`)
+ if (configFile.length === 0) LOG: logger.log('config file not found')
+ LOG: logger.log(`config set: ${JSON.stringify(CONFIG)}`)
}
await loadConfig()
// Initialize puppeteer
-logger.log('starting work server')
+LOG: logger.log('starting work server')
const NanoPow = await readFile(new URL('../main.min.js', import.meta.url), 'utf-8')
-logger.log('launching puppeteer browser')
+LOG: logger.log('launching puppeteer browser')
const browser = await launch({
handleSIGHUP: false,
handleSIGINT: false,
'--enable-unsafe-webgpu'
]
})
-logger.log('creating puppeteer page')
+LOG: logger.log('creating puppeteer page')
const page = await browser.newPage()
const src = `${NanoPow};window.NanoPow=NanoPow;`
<script type="module">${src}</script>
`
-logger.log('setting puppeteer request interception')
+LOG: logger.log('setting puppeteer request interception')
await page.setRequestInterception(true)
page.on('request', req => {
if (req.isInterceptResolutionHandled()) return
}
})
page.on('console', msg => logger.log(msg.text()))
-logger.log('navigating to https://nanopow.invalid/ to be intercepted')
+LOG: logger.log('navigating to https://nanopow.invalid/ to be intercepted')
await page.goto('https://nanopow.invalid/')
let NanoPowHandle = await page.waitForFunction(async () => {
return window.NanoPow
})
-logger.log('puppeteer initialized')
+LOG: logger.log('puppeteer initialized')
// Track requests by IP address, and let them fall off over time
const requests = new Map<string, { time: number, tokens: number }>()
async function work (data: unknown): Promise<string> {
let resBody = 'request failed'
if (data == null || typeof data !== 'object') {
- logger.log('Failed to parse request')
+ LOG: logger.log('Failed to parse request')
throw new Error(resBody)
}
if (Object.getPrototypeOf(data) !== Object.prototype) {
- logger.log('Data corrupted.')
+ LOG: logger.log('Data corrupted.')
throw new Error(resBody)
}
const dataParsed = data as { [key: string]: unknown }
let { action, hash, work, difficulty } = dataParsed
if (action !== 'work_generate' && action !== 'work_validate') {
- logger.log('Action must be work_generate or work_validate.')
+ LOG: logger.log('Action must be work_generate or work_validate.')
throw new Error(resBody)
}
resBody = `${action} failed`
if (typeof hash !== 'string' || !/[A-Fa-f\d]{64}/.test(hash)) {
- logger.log('Hash must be hex char(64)')
+ LOG: logger.log('Hash must be hex char(64)')
throw new Error(resBody)
}
if (difficulty !== undefined
&& (typeof difficulty !== 'string'
|| !/^[A-Fa-f\d]{16}$/.test(difficulty))
) {
- logger.log('Difficulty must be hex char(16).')
+ LOG: logger.log('Difficulty must be hex char(16).')
throw new Error(resBody)
}
if (action === 'work_validate') {
if (typeof work !== 'string' || !/^[A-Fa-f\d]{16}$/.test(work)) {
- logger.log('Work must be hex char(16).')
+ LOG: logger.log('Work must be hex char(16).')
throw new Error(resBody)
}
} else {
resBody = JSON.stringify(result)
return resBody
} catch (err) {
- logger.log(err)
+ LOG: logger.log(err)
throw new Error(resBody)
}
}
}
const client = requests.get(ip)
if (client && client.tokens-- <= 0) {
- logger.log(`==== Potential Abuse: ${ip} ====`)
+ LOG: logger.log(`==== Potential Abuse: ${ip} ====`)
return true
}
if (Date.now() - MAX_REQUEST_TIME > (client?.time ?? 0)) {
}
}
})
- logger.log(`server listening on ipc`)
+ LOG: logger.log(`server listening on ipc`)
process.send?.({ type: 'listening', data: 'ipc' })
} else {
server.listen(CONFIG.PORT, '127.0.0.1', () => {
const address = server.address()
if (address == null) {
- logger.log(`server closed`)
+ LOG: logger.log(`server closed`)
} else if (typeof address === 'string') {
- logger.log(`server listening on ${address}`)
+ LOG: logger.log(`server listening on ${address}`)
process.send?.({ type: 'listening', data: address })
} else {
CONFIG.PORT = address.port
- logger.log(`server listening on port ${address.port}`)
+ LOG: logger.log(`server listening on port ${address.port}`)
process.send?.({ type: 'listening', data: address.port })
}
})
})
server.on('error', serverErr => {
- logger.log('server error', serverErr)
+ LOG: logger.log('server error', serverErr)
try {
shutdown()
} catch (shutdownErr) {
- logger.log('server failed to shut down', shutdownErr)
+ LOG: logger.log('server failed to shut down', shutdownErr)
process.exit(1)
}
})
* does not respond within 10 seconds.
*/
function shutdown (): void {
- logger.log('shutdown signal received')
+ LOG: logger.log('shutdown signal received')
const kill = setTimeout((): never => {
- logger.log('server unresponsive, forcefully stopped')
+ LOG: logger.log('server unresponsive, forcefully stopped')
process.exit(1)
}, 10_000)
server.close(async (): Promise<never> => {
await browser.close()
clearTimeout(kill)
- logger.log('server stopped')
+ LOG: logger.log('server stopped')
process.exit(0)
})
}
process.on('SIGTERM', shutdown)
process.on('SIGHUP', async () => {
- logger.log('reloading configuration')
+ LOG: logger.log('reloading configuration')
server.close(async () => {
await loadConfig()
await page.reload()
try {
url = URL.createObjectURL(new Blob([NanoPowWasmWorker], { type: 'text/javascript' }))
workers = []
- logger.log(`NanoPow CPU initialized.`)
+ LOG: logger.log(`NanoPow CPU initialized.`)
isReady = true
} catch (err) {
isReady = false
}
try {
await workersStarted()
- logger.log('Workers ready')
+ LOG: logger.log('Workers ready')
} catch (err: any) {
- logger.log(err.message)
+ LOG: logger.log(err.message)
throw new Error('Error loading workers')
}
}
w.onerror = err
w.onmessage = (msg) => {
const result = msg.data
- logger.log(`received result from worker ${i}`)
+ LOG: logger.log(`received result from worker ${i}`)
found(result)
}
- logger.log(`sending data to worker ${i}`)
+ LOG: logger.log(`sending data to worker ${i}`)
w.postMessage(JSON.stringify(data))
}))
}
export async function generate (hash: bigint, difficulty: bigint, effort: number, debug: boolean): Promise<WorkGenerateResponse> {
logger.isEnabled = debug
logger.groupStart('NanoPow WASM work_generate')
- logger.log('generating')
+ LOG: logger.log('generating')
if (isReady === false) setup()
await init(hash, difficulty, effort)
work = await dispatch()
result = (await NanoPowValidate(work, hash, difficulty, debug)).difficulty
} catch (err) {
- logger.log(err)
+ LOG: logger.log(err)
} finally {
const isStopped = await workersStopped()
if (isStopped) {
- logger.log('workers stopped')
+ LOG: logger.log('workers stopped')
} else {
reset()
- logger.log('workers reset')
+ LOG: logger.log('workers reset')
}
logger.groupEnd('NanoPow WASM work_generate')
}
throw new Error('NanoPow could not restore WebGL context.')
}, 10_000)
ev.preventDefault()
- logger.log('NanoPow WebGL createCanvas', 'WebGL context lost. Waiting for it to be restored...')
+ LOG: logger.log('NanoPow WebGL createCanvas', 'WebGL context lost. Waiting for it to be restored...')
})
canvas.addEventListener('webglcontextrestored', ev => {
window.clearTimeout(isContextLost)
isContextLost = 0
- logger.log('NanoPow WebGL createCanvas', 'WebGL context restored. Reinitializing...')
+ LOG: logger.log('NanoPow WebGL createCanvas', 'WebGL context restored. Reinitializing...')
})
}
if (context == null) throw new Error('WebGL 2 is required')
gl = context
- logger.log('NanoPow WebGL createCanvas', 'requested size', size)
- logger.log('NanoPow WebGL createCanvas', 'MAX_VIEWPORT_DIMS', ...gl.getParameter(gl.MAX_VIEWPORT_DIMS))
+ LOG: logger.log('NanoPow WebGL createCanvas', 'requested size', size)
+ LOG: logger.log('NanoPow WebGL createCanvas', 'MAX_VIEWPORT_DIMS', ...gl.getParameter(gl.MAX_VIEWPORT_DIMS))
size = Math.min(size, ...gl.getParameter(gl.MAX_VIEWPORT_DIMS))
size = Math.floor(size / 0x100) * 0x100
canvas.height = canvas.width = size
- logger.log('NanoPow WebGL createCanvas', 'canvas size', canvas.height, canvas.width)
- logger.log('NanoPow WebGL createCanvas', 'drawingBuffer size', gl.drawingBufferHeight, gl.drawingBufferWidth)
+ LOG: logger.log('NanoPow WebGL createCanvas', 'canvas size', canvas.height, canvas.width)
+ LOG: logger.log('NanoPow WebGL createCanvas', 'drawingBuffer size', gl.drawingBufferHeight, gl.drawingBufferWidth)
if (canvas.height !== gl.drawingBufferHeight
|| canvas.width !== gl.drawingBufferWidth
) {
size = Math.floor(Math.min(gl.drawingBufferHeight, gl.drawingBufferWidth) / 0x100) * 0x100
canvas.height = canvas.width = size
}
- logger.log('NanoPow WebGL createCanvas', 'final size', size)
+ LOG: logger.log('NanoPow WebGL createCanvas', 'final size', size)
logger.groupEnd('NanoPow WebGL createCanvas')
}
// Finalize configuration
pixels = new Uint32Array(finalFbo.size.x * finalFbo.size.y * 4)
isReady = true
- logger.log(`NanoPow WebGL initialized at ${gl.drawingBufferWidth}x${gl.drawingBufferHeight}. Maximum nonces checked per frame: ${gl.drawingBufferWidth * gl.drawingBufferHeight}`)
+ LOG: logger.log(`NanoPow WebGL initialized at ${gl.drawingBufferWidth}x${gl.drawingBufferHeight}. Maximum nonces checked per frame: ${gl.drawingBufferWidth * gl.drawingBufferHeight}`)
} catch (err) {
throw new Error('WebGL compilation failed.', { cause: err })
}
vertexShader = null
gl?.deleteProgram(drawProgram)
drawProgram = null
- logger.log('reset done')
+ LOG: logger.log('reset done')
}
function init (hash: Uint32Array, difficulty: bigint): void {
inputHashView.setUint32(i * 16, hash[i])
}
inputDifficultyView.setBigUint64(0, difficulty, true)
- logger.log('INPUT', inputArray.buffer.slice(0))
+ LOG: logger.log('INPUT', inputArray.buffer.slice(0))
gl.bindBuffer(gl.UNIFORM_BUFFER, inputBuffer)
gl.bufferSubData(gl.UNIFORM_BUFFER, 0, inputView)
gl.bindBuffer(gl.UNIFORM_BUFFER, null)
if (gl == null) throw new Error('WebGL 2 is required to draw')
if (drawFbo == null) throw new Error('FBO is required to draw')
if (query == null) throw new Error('Query is required to draw')
- logger.log(bigintToHex(seed, 16))
+ LOG: logger.log(bigintToHex(seed, 16))
// Upload work seed buffer
inputSeedView.setBigUint64(0, seed, true)
- logger.log('INPUT', inputView.buffer.slice(0))
+ LOG: logger.log('INPUT', inputView.buffer.slice(0))
gl.bindBuffer(gl.UNIFORM_BUFFER, inputBuffer)
gl.bufferSubData(gl.UNIFORM_BUFFER, 136, inputSeedView)
gl.bindBuffer(gl.UNIFORM_BUFFER, null)
// Loop pixels to find one with values
for (let i = 0; i < pixels.length; i += 4) {
if (pixels[i] || pixels[i + 1] || pixels[i + 2] || pixels[i + 3]) {
- logger.log(`rgba(${pixels[i]}, ${pixels[i + 1]}, ${pixels[i + 2]}, ${pixels[i + 3]})`)
+ LOG: logger.log(`rgba(${pixels[i]}, ${pixels[i + 1]}, ${pixels[i + 2]}, ${pixels[i + 3]})`)
resultView.setUint32(0, pixels[i], true)
resultView.setUint32(4, pixels[i + 1], true)
resultView.setUint32(8, pixels[i + 2], true)
}
}
}
- logger.log(`pixels[${pixels.length}]`, pixels)
+ LOG: logger.log(`pixels[${pixels.length}]`, pixels)
throw new Error('Query reported result but nonce value not found')
}
let timeout = false
const kill = setTimeout(() => {
timeout = true
- logger.log('timed out')
+ LOG: logger.log('timed out')
}, 60_000)
logger.groupStart('NanoPow WebGL work_generate')
- logger.log('generating')
+ LOG: logger.log('generating')
// Start drawing to calculate one nonce per pixel
let found = false
setup(effort)
}
init(bigintAsUintNArray(hash, 32, 8), difficulty)
- logger.log('drawing frame 0')
+ LOG: logger.log('drawing frame 0')
draw(bigintRandom(), drawFbos[0], queries[0])
draw(bigintRandom(), drawFbos[1], queries[1])
draw(bigintRandom(), drawFbos[2], queries[2])
let drawIndex = 3
do {
- logger.log(`drawing frame ${drawIndex}`)
+ LOG: logger.log(`drawing frame ${drawIndex}`)
draw(bigintRandom(), drawFbos[drawIndex], queries[drawIndex])
drawIndex = (drawIndex + 1) % 4
- logger.log(`checking frame ${drawIndex}`)
+ LOG: logger.log(`checking frame ${drawIndex}`)
found = await check(queries[drawIndex])
} while (!found && !timeout)
if (found) result = read(drawFbos[drawIndex])
isFirstRetry = false
} catch (err) {
isFirstRetry = !isFirstRetry
- logger.log(err)
+ LOG: logger.log(err)
if (!isFirstRetry) {
throw new Error('failed to restore context', { cause: err })
}
// Initialize WebGPU
async function start (): Promise<void> {
if (status === 'Idle') {
- logger.log('starting')
+ LOG: logger.log('starting')
status = 'Starting'
await getDevice()
try {
await compile()
- logger.log('ready')
+ LOG: logger.log('ready')
status = 'Ready'
} catch (err) {
status = 'Crashed'
- logger.log(err)
+ LOG: logger.log(err)
throw new Error('failed to compile', { cause: err })
}
}
throw new Error('failed to get device from gpu adapter')
}
device.lost?.then(async (deviceLostInfo) => {
- logger.log('device lost', deviceLostInfo)
+ LOG: logger.log('device lost', deviceLostInfo)
// Set up 30s timeout to prevent long-running calls
isContextLost = window.setTimeout(() => {
throw new Error('failed to restore device', { cause: deviceLostInfo })
}
async function restore (): Promise<void> {
- logger.log('restoring')
+ LOG: logger.log('restoring')
try {
status = 'Restoring'
for (let i = 0; i < 2; i++) {
await compile()
window.clearTimeout(isContextLost)
isContextLost = 0
- logger.log('ready')
+ LOG: logger.log('ready')
status = 'Ready'
} catch (err) {
status = 'Crashed'
}
async function init (hash: BigUint64Array, difficulty: bigint): Promise<void> {
- logger.log('variables initializing')
+ LOG: logger.log('variables initializing')
try {
// Save hash data for normal usage and potential recovery efforts
hashData.set(hash)
device.queue.writeBuffer(outputBuffers[0], 0, bufferReset)
device.queue.writeBuffer(outputBuffers[1], 0, bufferReset)
} catch (err) {
- logger.log(err)
+ LOG: logger.log(err)
throw new Error('failed to initialize', { cause: err })
}
}
async function dispatch (dispatchIndex: number, seed: bigint, effort: number): Promise<void> {
- logger.log('dispatching compute pass')
+ LOG: logger.log('dispatching compute pass')
try {
- logger.log('seed', bigintToHex(seed, 16))
+ LOG: logger.log('seed', bigintToHex(seed, 16))
// Copy seed into INPUT buffer
inputDataView.setBigUint64(40, seed, true)
- logger.log('INPUT', inputDataView)
+ LOG: logger.log('INPUT', inputDataView)
device.queue.writeBuffer(inputBuffers[dispatchIndex], 40, inputDataView, 40)
// Create command encoder to issue commands to GPU and initiate computation
// End computation by passing array of command buffers to command queue for execution
device.queue.submit([commandEncoder.finish()])
} catch (err) {
- logger.log(err)
+ LOG: logger.log(err)
throw new Error('failed to dispatch compute pass', { cause: err })
}
}
async function check (dispatchIndex: number): Promise<boolean> {
- logger.log('checking results from compute pass')
+ LOG: logger.log('checking results from compute pass')
try {
await resultBuffers[dispatchIndex].mapAsync(GPUMapMode.READ)
resultViews[dispatchIndex] = new DataView(resultBuffers[dispatchIndex].getMappedRange().slice(0))
resultBuffers[dispatchIndex].unmap()
- logger.log('OUTPUT', resultViews[dispatchIndex])
+ LOG: logger.log('OUTPUT', resultViews[dispatchIndex])
if (resultViews[dispatchIndex] == null) throw new Error('failed to get data from resultBuffer.')
return !!resultViews[dispatchIndex].getUint32(0, true)
} catch (err) {
- logger.log(err)
+ LOG: logger.log(err)
throw new Error('failed to read results from compute pass', { cause: err })
}
}
* Map CPU buffer to GPU, read results to static result object, and unmap.
*/
function read (dispatchIndex: number): { work: bigint, difficulty: bigint } {
- logger.log('reading results from compute pass')
+ LOG: logger.log('reading results from compute pass')
try {
if (resultViews[dispatchIndex] == null) throw new Error('failed to get data from result view')
return {
difficulty: resultViews[dispatchIndex].getBigUint64(16, true)
}
} catch (err) {
- logger.log(err)
+ LOG: logger.log(err)
throw new Error('failed to read results from compute pass', { cause: err })
}
}
throw new Error('timed out')
}, 60_000)
logger.groupStart('NanoPow WebGPU work_generate')
- logger.log('generating')
+ LOG: logger.log('generating')
let found = false
let result: { [key: string]: bigint } = {}
let isFirstRetry = false
}
// Only retry once here, allow errors to propagate out to the consumer
isFirstRetry = !isFirstRetry
- logger.log(err)
+ LOG: logger.log(err)
if (!isFirstRetry) {
throw new Error('failed to restore device', { cause: err })
}
function log (work: bigint, hash: bigint, difficulty: bigint): void {
logger.groupStart('NanoPow CPU work_validate')
- logger.log('NanoPow CPU work_validate', 'work', bigintToHex(work, 16))
- logger.log('NanoPow CPU work_validate', 'hash', bigintToHex(hash, 64))
- logger.log('NanoPow CPU work_validate', 'difficulty', bigintToHex(difficulty, 16))
- logger.log('NanoPow CPU work_validate', 'result', bigintToHex(result, 16))
+ LOG: logger.log('NanoPow CPU work_validate', 'work', bigintToHex(work, 16))
+ LOG: logger.log('NanoPow CPU work_validate', 'hash', bigintToHex(hash, 64))
+ LOG: logger.log('NanoPow CPU work_validate', 'difficulty', bigintToHex(difficulty, 16))
+ LOG: logger.log('NanoPow CPU work_validate', 'result', bigintToHex(result, 16))
logger.groupEnd('NanoPow CPU work_validate')
}