Home Reference Source Test

src/main/generic/miner/Miner.js

/**
 * @deprecated
 */
class Miner extends Observable {
    /**
     * @param {BaseChain} blockchain
     * @param {Accounts} accounts
     * @param {Mempool} mempool
     * @param {Time} time
     * @param {Address} minerAddress
     * @param {Uint8Array} [extraData=new Uint8Array(0)]
     *
     * @listens Mempool#transaction-added
     * @listens Mempool#transaction-ready
     */
    constructor(blockchain, accounts, mempool, time, minerAddress, extraData = new Uint8Array(0)) {
        super();
        /** @type {BaseChain} */
        this._blockchain = blockchain;
        /** @type {Accounts} */
        this._accounts = accounts;
        /** @type {Mempool} */
        this._mempool = mempool;
        /** @type {Time} */
        this._time = time;
        /**
         * @protected
         * @type {BlockProducer}
         */
        this._producer = new BlockProducer(blockchain, accounts, mempool, time);
        /** @type {Address} */
        this._address = minerAddress;
        /** @type {Uint8Array} */
        this._extraData = extraData;

        /**
         * Number of hashes computed since the last hashrate update.
         * @type {number}
         * @private
         */
        this._hashCount = 0;

        /**
         * Timestamp of the last hashrate update.
         * @type {number}
         * @private
         */
        this._lastHashrate = 0;

        /**
         * Hashrate computation interval handle.
         * @private
         */
        this._hashrateWorker = null;

        /**
         * The current hashrate of this miner.
         * @type {number}
         * @private
         */
        this._hashrate = 0;

        /**
         * The last hash counts used in the moving average.
         * @type {Array.<number>}
         * @private
         */
        this._lastHashCounts = [];

        /**
         * The total hashCount used in the current moving average.
         * @type {number}
         * @private
         */
        this._totalHashCount = 0;

        /**
         * The time elapsed for the last measurements used in the moving average.
         * @type {Array.<number>}
         * @private
         */
        this._lastElapsed = [];

        /**
         * The total time elapsed used in the current moving average.
         * @type {number}
         * @private
         */
        this._totalElapsed = 0;

        /** @type {MinerWorkerPool} */
        this._workerPool = new MinerWorkerPool();

        const cores = PlatformUtils.hardwareConcurrency;
        this.threads = Math.ceil(cores / 2);
        if (cores === 1) this.throttleAfter = 2;
        this._workerPool.on('share', (obj) => this.onWorkerShare(obj));
        this._workerPool.on('no-share', (obj) => this.onWorkerShare(obj));

        /**
         * Flag indicating that the mempool has changed since we started mining the current block.
         * @type {boolean}
         * @private
         */
        this._mempoolChanged = false;

        /** @type {boolean} */
        this._restarting = false;

        /** @type {number} */
        this._lastRestart = 0;

        /** @type {boolean} */
        this._submittingBlock = false;

        /** @type {number} */
        this._shareCompact = 0;

        /** @type {boolean} */
        this._shareCompactSet = false;

        /** @type {number} */
        this._numBlocksMined = 0;

        /** @type {boolean} */
        this._activeConfigChanges = false;
        /** @type {boolean} */
        this._pendingConfigChanges = false;

        if (this._mempool) {
            // Listen to changes in the mempool which evicts invalid transactions
            // after every blockchain head change and then fires 'transactions-ready'
            // when the eviction process finishes. Restart work on the next block
            // with fresh transactions when this fires.
            this._mempool.on('transactions-ready', () => this._startWork());

            // Immediately start processing transactions when they come in.
            this._mempool.on('transaction-added', () => this._mempoolChanged = true);
        }
    }

    startWork() {
        if (this.working) {
            return;
        }

        // Initialize hashrate computation.
        this._hashCount = 0;
        this._lastElapsed = [];
        this._lastHashCounts = [];
        this._totalHashCount = 0;
        this._totalElapsed = 0;
        this._lastHashrate = Date.now();
        this._hashrateWorker = setInterval(() => this._updateHashrate(), 1000);
        this._retry = 0;

        // Tell listeners that we've started working.
        this.fire('start', this);

        // Kick off the mining process.
        this._startWork().catch(Log.w.tag(Miner));
    }

    async _startWork() {
        // XXX Needed as long as we cannot unregister from transactions-ready events.
        if (!this.working || this._restarting) {
            return;
        }
        try {
            this._lastRestart = Date.now();
            this._restarting = true;
            this._mempoolChanged = false;

            // Construct next block.
            this._retry = 0;
            const block = await this.getNextBlock();
            if (block === null) {
                this._stopWork();
                return;
            }

            Log.d(Miner, `Starting work on block #${block.header.height} / ${block.minerAddr.toUserFriendlyAddress()} / ${BufferUtils.toBase64(block.body.extraData)} with ${block.isFull() ? block.transactionCount : '(set by pool)'} transactions (${this._hashrate} H/s)`);

            this._workerPool.startMiningOnBlock(block, this._shareCompactSet ? this._shareCompact : undefined).catch(Log.w.tag(Miner));
        } catch (e) {
            Log.e(Miner, e);
            Log.w(Miner, 'Failed to start work, retrying in 100ms');
            this._stopWork();
            setTimeout(() => this.startWork(), 100);
        } finally {
            this._restarting = false;
        }
    }

    /**
     * @param {{hash: Hash, nonce: number, block: Block}} obj
     */
    async onWorkerShare(obj) {
        this._hashCount += this._workerPool.noncesPerRun;
        if (obj.block && obj.block.prevHash.equals(this._blockchain.headHash)) {
            Log.d(Miner, () => `Received share: ${obj.nonce} / ${obj.hash.toHex()}`);
            if (!this._submittingBlock) {
                obj.block.header.nonce = obj.nonce;

                let blockValid = false;
                if (obj.block.isFull() && BlockUtils.isProofOfWork(obj.hash, obj.block.target)) {
                    this._submittingBlock = true;
                    if (await obj.block.header.verifyProofOfWork()) {
                        this._numBlocksMined++;
                        blockValid = true;

                        // Tell listeners that we've mined a block.
                        this.fire('block-mined', obj.block, this);

                        // Push block into blockchain.
                        if ((await this._blockchain.pushBlock(obj.block)) < 0) {
                            this._submittingBlock = false;
                            this._startWork().catch(Log.w.tag(Miner));
                            return;
                        } else {
                            this._submittingBlock = false;
                        }
                    } else {
                        Log.d(Miner, `Ignoring invalid share: ${await obj.block.header.pow()}`);
                    }
                }

                this.fire('share', obj.block, blockValid, this);
            }
        }
        if (this._mempoolChanged && this._lastRestart + Miner.MIN_TIME_ON_BLOCK < Date.now()) {
            this._startWork().catch(Log.w.tag(Miner));
        }
    }

    /**
     * @param {Address} [address]
     * @param {Uint8Array} [extraData]
     * @return {Promise.<Block>}
     */
    async getNextBlock(address = this._address, extraData = this._extraData) {
        this._retry++;
        try {
            return this._producer.getNextBlock(address, extraData);
        } catch (e) {
            // Retry up to three times.
            if (this._retry <= 3) return this.getNextBlock(address, extraData);
            throw e;
        }
    }

    /**
     * @fires Miner#stop
     */
    stopWork() {
        this._stopWork();
    }

    /**
     * @fires Miner#stop
     * @private
     */
    _stopWork() {
        // TODO unregister from blockchain head-changed events.
        if (!this.working) {
            return;
        }

        clearInterval(this._hashrateWorker);
        this._hashrateWorker = null;
        this._hashrate = 0;
        this._lastElapsed = [];
        this._lastHashCounts = [];
        this._totalHashCount = 0;
        this._totalElapsed = 0;

        // Tell listeners that we've stopped working.
        this._workerPool.stop();
        this.fire('stop', this);

        Log.d(Miner, 'Stopped work');
    }

    /**
     * @fires Miner#hashrate-changed
     * @private
     */
    _updateHashrate() {
        const elapsed = (Date.now() - this._lastHashrate) / 1000;
        const hashCount = this._hashCount;
        // Enable next measurement.
        this._hashCount = 0;
        this._lastHashrate = Date.now();

        // Update stored information on moving average.
        this._lastElapsed.push(elapsed);
        this._lastHashCounts.push(hashCount);
        this._totalElapsed += elapsed;
        this._totalHashCount += hashCount;

        if (this._lastElapsed.length > Miner.MOVING_AVERAGE_MAX_SIZE) {
            const oldestElapsed = this._lastElapsed.shift();
            const oldestHashCount = this._lastHashCounts.shift();
            this._totalElapsed -= oldestElapsed;
            this._totalHashCount -= oldestHashCount;
        }

        this._hashrate = Math.round(this._totalHashCount / this._totalElapsed);

        // Tell listeners about our new hashrate.
        this.fire('hashrate-changed', this._hashrate, this);
    }

    startConfigChanges() {
        this._activeConfigChanges = true;
        this._pendingConfigChanges = false;
    }

    finishConfigChanges() {
        if (this._pendingConfigChanges) {
            this._startWork().catch(Log.w.tag(Miner));
        }
        this._activeConfigChanges = false;
    }

    /** @type {Address} */
    get address() {
        return this._address;
    }

    /** @type {Address} */
    set address(addr) {
        if (addr && !addr.equals(this._address)) {
            this._address = addr;
            if (!this._activeConfigChanges) {
                this._startWork().catch(Log.w.tag(Miner));
            } else {
                this._pendingConfigChanges = true;
            }
        }
    }

    /** @type {boolean} */
    get working() {
        return !!this._hashrateWorker;
    }

    /** @type {number} */
    get hashrate() {
        return this._hashrate;
    }

    /** @type {number} */
    get threads() {
        return this._workerPool.poolSize;
    }

    /**
     * @param {number} threads
     */
    set threads(threads) {
        this._workerPool.poolSize = threads;
    }

    /** @type {number} */
    get throttleWait() {
        return this._workerPool.cycleWait;
    }

    /**
     * @param {number} throttleWait
     */
    set throttleWait(throttleWait) {
        this._workerPool.cycleWait = throttleWait;
    }

    /** @type {number} */
    get throttleAfter() {
        return this._workerPool.runsPerCycle;
    }

    /**
     * @param {number} throttleAfter
     */
    set throttleAfter(throttleAfter) {
        this._workerPool.runsPerCycle = throttleAfter;
    }

    /** @type {Uint8Array} */
    get extraData() {
        return this._extraData;
    }

    /** @type {Uint8Array} */
    set extraData(extra) {
        if (!BufferUtils.equals(extra, this._extraData)) {
            this._extraData = extra;
            if (!this._activeConfigChanges) {
                this._startWork().catch(Log.w.tag(Miner));
            } else {
                this._pendingConfigChanges = true;
            }
        }
    }

    get shareCompact() {
        if (!this._shareCompactSet) {
            return undefined;
        } else {
            return this._shareCompact;
        }
    }

    /** @type {number} */
    set shareCompact(targetCompact) {
        if (!targetCompact) {
            this._shareCompactSet = false;
        } else {
            this._shareCompact = targetCompact;
            this._shareCompactSet = true;
        }
    }

    /** @type {number} */
    get numBlocksMined() {
        return this._numBlocksMined;
    }
}

Miner.MIN_TIME_ON_BLOCK = 10000;
Miner.MOVING_AVERAGE_MAX_SIZE = 10;
Class.register(Miner);