Home Reference Source Test

src/main/generic/consensus/base/transaction/TransactionCache.js

/**
 * @typedef {object} BlockDescriptor
 * @property {Hash} hash
 * @property {Hash} prevHash
 * @property {Array.<Hash>} transactionHashes
 */

class TransactionCache {
    /**
     * @param {InclusionHashSet.<Hash>} [transactionHashes]
     * @param {Array.<BlockDescriptor>} [blockOrder]
     */
    constructor(transactionHashes = null, blockOrder = []) {
        /** @type {InclusionHashSet.<Hash>} */
        this._transactionHashes = transactionHashes ? transactionHashes : new InclusionHashSet(txHash => txHash.toBase64());
        /** @type {Array.<BlockDescriptor>} */
        this._blockOrder = blockOrder;
    }

    /**
     * @param {Transaction} transaction
     * @returns {boolean}
     */
    containsTransaction(transaction) {
        return this._transactionHashes.contains(transaction.hash());
    }

    /**
     * @param {Block} block
     * @returns {BlockDescriptor}
     * @private
     */
    static _getBlockDescriptor(block) {
        return {
            hash: block.hash(),
            prevHash: block.prevHash,
            transactionHashes: block.transactions.map(tx => tx.hash())
        };
    }

    /**
     * @param {Block} block
     */
    pushBlock(block) {
        Assert.that(!this.head || block.prevHash.equals(this.head.hash), 'Not a successor of head');
        const blockDescriptor = TransactionCache._getBlockDescriptor(block);

        this._blockOrder.push(blockDescriptor);
        this._transactionHashes.addAll(blockDescriptor.transactionHashes);

        if (this._blockOrder.length > Policy.TRANSACTION_VALIDITY_WINDOW) {
            this.shiftBlock();
        }
    }

    shiftBlock() {
        const blockDescriptor = this._blockOrder.shift();
        if (blockDescriptor) {
            this._transactionHashes.removeAll(blockDescriptor.transactionHashes);
        }
    }

    /**
     * @param {Block} block
     * @returns {number}
     */
    revertBlock(block) {
        const blockDescriptorFromOrder = this._blockOrder.pop();
        // If there is a block to remove
        if (blockDescriptorFromOrder) {
            Assert.that(blockDescriptorFromOrder.hash.equals(block.hash()), 'Invalid block to revert');
            this._transactionHashes.removeAll(blockDescriptorFromOrder.transactionHashes);
        }

        return this.missingBlocks;
    }

    /**
     * @param {Array.<Block>} blocks
     */
    prependBlocks(blocks) {
        if (blocks.length + this._blockOrder.length > Policy.TRANSACTION_VALIDITY_WINDOW) {
            throw new Error('Exceeding transaction cache size');
        }
        Assert.that(!this.tail || blocks.length === 0 || this.tail.prevHash.equals(blocks[blocks.length - 1].hash()), 'Not a predecessor of tail');
        const blockDescriptors = blocks.map(block => TransactionCache._getBlockDescriptor(block));
        this._blockOrder.unshift(...blockDescriptors);
        blockDescriptors.forEach(b => this._transactionHashes.addAll(b.transactionHashes));
    }

    /** @type {number} */
    get missingBlocks() {
        return Policy.TRANSACTION_VALIDITY_WINDOW - this._blockOrder.length;
    }

    /** @type {InclusionHashSet.<Transaction>} */
    get transactions() {
        return this._transactionHashes;
    }

    /**
     * @returns {TransactionCache}
     */
    clone() {
        return new TransactionCache(/** @type {InclusionHashSet.<Hash>} */ this._transactionHashes.clone(), this._blockOrder.slice());
    }

    /**
     * @returns {boolean}
     */
    isEmpty() {
        return this._blockOrder.length === 0;
    }

    /** @type {?BlockDescriptor} */
    get head() {
        if (this._blockOrder.length === 0) return null;
        return this._blockOrder[this._blockOrder.length - 1];
    }

    /** @type {?BlockDescriptor} */
    get tail() {
        if (this._blockOrder.length === 0) return null;
        return this._blockOrder[0];
    }
}
Class.register(TransactionCache);