Home Reference Source Test

src/main/generic/consensus/base/blockchain/ChainDataStore.js

class ChainDataStore {
    /**
     * @param {JungleDB} jdb
     */
    static initPersistent(jdb) {
        const chainStore = jdb.createObjectStore('ChainData', {
            codec: new ChainDataStoreCodec(),
            enableLruCache: ChainDataStore.CHAINDATA_CACHING_ENABLED,
            lruCacheSize: ChainDataStore.CHAINDATA_CACHE_SIZE
        });
        ChainDataStore._createIndexes(chainStore);

        jdb.createObjectStore('Block', {
            codec: new BlockStoreCodec(),
            enableLruCache: ChainDataStore.BLOCKS_CACHING_ENABLED,
            lruCacheSize: ChainDataStore.BLOCKS_CACHE_SIZE,
            rawLruCacheSize: ChainDataStore.BLOCKS_RAW_CACHE_SIZE
        });
    }

    /**
     * @param {JungleDB} jdb
     * @returns {ChainDataStore}
     */
    static getPersistent(jdb) {
        const chainStore = jdb.getObjectStore('ChainData');
        const blockStore = jdb.getObjectStore('Block');
        return new ChainDataStore(chainStore, blockStore);
    }

    /**
     * @returns {ChainDataStore}
     */
    static createVolatile() {
        const chainStore = JDB.JungleDB.createVolatileObjectStore({ codec: new ChainDataStoreCodec() });
        const blockStore = JDB.JungleDB.createVolatileObjectStore({ codec: new BlockStoreCodec() });
        ChainDataStore._createIndexes(chainStore);
        return new ChainDataStore(chainStore, blockStore);
    }

    /**
     * @param {IObjectStore} chainStore
     * @private
     */
    static _createIndexes(chainStore) {
        chainStore.createIndex('height', ['_height'], { lmdbKeyEncoding: JDB.JungleDB.NUMBER_ENCODING, leveldbKeyEncoding: JDB.JungleDB.NUMBER_ENCODING });
    }

    /**
     * @param {IObjectStore} chainStore
     * @param {IObjectStore} blockStore
     */
    constructor(chainStore, blockStore) {
        /** @type {IObjectStore} */
        this._chainStore = chainStore;
        /** @type {IObjectStore} */
        this._blockStore = blockStore;
    }

    /**
     * @param {Hash} key
     * @param {boolean} [includeBody]
     * @returns {Promise.<?ChainData>}
     */
    async getChainData(key, includeBody = false) {
        /** @type {ChainData} */
        let chainData = await this._chainStore.get(key.toBase64());

        // Do not modify object from store, since it might be cached
        if (chainData) {
            chainData = chainData.shallowCopy();
        }

        if (!chainData || !includeBody) {
            return chainData;
        }

        const block = await this._blockStore.get(key.toBase64());
        if (block && block.isFull()) {
            chainData.head._body = block.body;
        }

        return chainData;
    }

    /**
     * @param {Hash} key
     * @param {ChainData} chainData
     * @param {boolean} [includeBody]
     * @returns {Promise.<void>}
     */
    putChainData(key, chainData, includeBody = true) {
        // Do not modify object from store, since it might be cached
        const cleanChainData = chainData.shallowCopy();
        cleanChainData.head._body = null;

        if (this._chainStore instanceof JDB.Transaction) {
            this._chainStore.putSync(key.toBase64(), cleanChainData);
            if (includeBody && chainData.head.isFull()) {
                this._blockStore.putSync(key.toBase64(), chainData.head);
            }
            return Promise.resolve(true);
        }

        if (includeBody && chainData.head.isFull()) {
            const chainTx = this._chainStore.synchronousTransaction();
            chainTx.putSync(key.toBase64(), cleanChainData);
            const blockTx = this._blockStore.synchronousTransaction();
            blockTx.putSync(key.toBase64(), chainData.head);
            return JDB.JungleDB.commitCombined(chainTx, blockTx);
        }

        return this._chainStore.put(key.toBase64(), cleanChainData);
    }

    /**
     * @param {Hash} key
     * @param {ChainData} chainData
     * @param {boolean} [includeBody]
     * @returns {void}
     */
    putChainDataSync(key, chainData, includeBody = true) {
        // Do not modify object from store, since it might be cached
        const cleanChainData = chainData.shallowCopy();
        cleanChainData.head._body = null;

        Assert.that(this._chainStore instanceof JDB.Transaction);
        this._chainStore.putSync(key.toBase64(), cleanChainData);
        if (includeBody && chainData.head.isFull()) {
            this._blockStore.putSync(key.toBase64(), chainData.head);
        }
    }

    /**
     * @param {Hash} key
     * @param {boolean} [includeBody]
     * @returns {?Block}
     */
    async getBlock(key, includeBody = false) {
        if (includeBody) {
            const block = await this._blockStore.get(key.toBase64());
            if (block) {
                return block;
            }
        }

        const chainData = await this._chainStore.get(key.toBase64());
        return chainData ? chainData.head : null;
    }

    /**
     * @param {Hash} key
     * @param {boolean} [includeForks]
     * @returns {Promise.<?Uint8Array>}
     */
    async getRawBlock(key, includeForks = false) {
        /** @type {ChainData} */
        const chainData = await this._chainStore.get(key.toBase64());
        if (!chainData || (!chainData.onMainChain && !includeForks)) {
            return null;
        }

        const block = await this._blockStore.get(key.toBase64(), { raw: true });
        if (block) {
            return new Uint8Array(block);
        }

        return null;
    }

    /**
     * @param {number} height
     * @param {boolean} [includeBody]
     * @returns {Promise.<?ChainData>}
     */
    async getChainDataAt(height, includeBody = false) {
        /** @type {Array.<ChainData>} */
        const candidates = await this._chainStore.values(JDB.Query.eq('height', height));
        if (!candidates || !candidates.length) {
            return undefined;
        }

        for (const chainData of candidates) {
            if (chainData.onMainChain) {
                // Do not modify object from store, since it might be cached
                const finalChainData = chainData.shallowCopy();
                if (includeBody) {
                    // eslint-disable-next-line no-await-in-loop
                    const block = await this._blockStore.get(chainData.head.hash().toBase64());
                    if (block) {
                        finalChainData.head._body = block.body;
                    }
                }
                return finalChainData;
            }
        }

        return null;
    }

    /**
     * @param {number} height
     * @param {boolean} [includeBody]
     * @returns {Promise.<?Block>}
     */
    async getBlockAt(height, includeBody = false) {
        const chainData = await this.getChainDataAt(height, includeBody);
        return chainData ? chainData.head : null;
    }

    /**
     * @param {number} height
     * @param {boolean} [lower]
     * @returns {Promise.<?Block>}
     */
    async getNearestBlockAt(height, lower = true) {
        const index = this._chainStore.index('height');
        /** @type {Array.<ChainData>} */
        const candidates = lower ?
            await index.maxValues(JDB.KeyRange.upperBound(height)) :
            await index.minValues(JDB.KeyRange.lowerBound(height));
        if (!candidates || !candidates.length) {
            return undefined;
        }

        for (const chainData of candidates) {
            if (chainData.onMainChain) {
                return chainData.head;
            }
        }

        return null;
    }

    // /**
    //  * @param {number} startHeight
    //  * @param {number} [count]
    //  * @param {boolean} [forward]
    //  * @returns {Promise.<Array.<Block>>}
    //  */
    // async getBlocks(startHeight, count = 500, forward = true) {
    //     if (count <= 0) {
    //         return [];
    //     }
    //     if (!forward) {
    //         startHeight = startHeight - count;
    //     }
    //     /** @type {Array.<ChainData>} */
    //     let candidates = await this._chainStore.values(JDB.Query.within('height', startHeight, startHeight + count - 1));
    //     candidates = candidates
    //         .filter(chainData => chainData.onMainChain)
    //         .map(chainData => chainData.head);
    //     const sortNumber = forward ? ((a, b) => a.height - b.height) : ((a, b) => b.height - a.height);
    //     candidates.sort(sortNumber);
    //     return candidates;
    // }

    /**
     * @param {Hash} startBlockHash
     * @param {number} [count]
     * @param {boolean} [forward]
     * @returns {Promise.<Array.<Block>>}
     */
    getBlocks(startBlockHash, count = 500, forward = true) {
        if (count <= 0) {
            return Promise.resolve([]);
        }

        if (forward) {
            return this.getBlocksForward(startBlockHash, count);
        } else {
            return this.getBlocksBackward(startBlockHash, count);
        }
    }

    /**
     * @param {Hash} startBlockHash
     * @param {number} count
     * @returns {Promise.<Array.<Block>>}
     */
    async getBlocksForward(startBlockHash, count = 500) {
        /** @type {ChainData} */
        let chainData = await this._chainStore.get(startBlockHash.toBase64());
        if (!chainData) {
            return [];
        }

        const blocks = [];
        while (blocks.length < count && chainData.mainChainSuccessor) {
            chainData = await this._chainStore.get(chainData.mainChainSuccessor.toBase64());
            if (!chainData) {
                return blocks;
            }
            blocks.push(chainData.head);
        }
        return blocks;
    }

    /**
     * @param {Hash} startBlockHash
     * @param {number} count
     * @param {boolean} includeBody
     * @returns {Promise.<Array.<Block>>}
     */
    async getBlocksBackward(startBlockHash, count = 500, includeBody = false) {
        const getBlock = includeBody
            ? key => this._blockStore.get(key)
            : key => this._chainStore.get(key).then(data => data.head);

        /** @type {ChainData} */
        const chainData = await this._chainStore.get(startBlockHash.toBase64());
        if (!chainData) {
            return [];
        }

        /** @type {Block} */
        let block = chainData.head;
        const blocks = [];
        while (blocks.length < count && block.height > 1) {
            block = await getBlock(block.prevHash.toBase64());
            if (!block) {
                return blocks;
            }
            blocks.push(block);
        }
        return blocks;
    }

    /**
    /**
     * @returns {Promise.<Hash|undefined>}
     */
    async getHead() {
        const key = await this._chainStore.get('main');
        return key ? Hash.fromBase64(key) : undefined;
    }

    /**
     * @param {Hash} key
     * @returns {Promise.<void>}
     */
    setHead(key) {
        return this._chainStore.put('main', key.toBase64());
    }

    /**
     * @param {Hash} key
     * @returns {void}
     */
    setHeadSync(key) {
        Assert.that(this._chainStore instanceof JDB.SynchronousTransaction);
        this._chainStore.putSync('main', key.toBase64());
    }

    /**
     * @param {boolean} [enableWatchdog]
     * @returns {ChainDataStore}
     */
    transaction(enableWatchdog = true) {
        const chainTx = this._chainStore.transaction(enableWatchdog);
        const blockTx = this._blockStore.transaction(enableWatchdog);
        return new ChainDataStore(chainTx, blockTx);
    }

    /**
     * @param {boolean} [enableWatchdog]
     * @returns {ChainDataStore}
     */
    synchronousTransaction(enableWatchdog = true) {
        const chainTx = this._chainStore.synchronousTransaction(enableWatchdog);
        const blockTx = this._blockStore.synchronousTransaction(enableWatchdog);
        return new ChainDataStore(chainTx, blockTx);
    }

    /**
     * @returns {Promise}
     */
    commit() {
        if (this._chainStore instanceof JDB.Transaction) {
            return JDB.JungleDB.commitCombined(this._chainStore, this._blockStore);
        }
        return Promise.resolve();
    }

    /**
     * @returns {Promise}
     */
    abort() {
        return Promise.all([this._chainStore.abort(), this._blockStore.abort()]);
    }

    /**
     * @returns {ChainDataStore}
     */
    snapshot() {
        const chainSnapshot = this._chainStore.snapshot();
        const blockSnapshot = this._blockStore.snapshot();
        return new ChainDataStore(chainSnapshot, blockSnapshot);
    }

    /**
     * @returns {Promise}
     */
    truncate() {
        if (this._chainStore instanceof JDB.Transaction) {
            this._chainStore.truncateSync();
            this._blockStore.truncateSync();
            return Promise.resolve(true);
        }

        const chainTx = this._chainStore.transaction();
        chainTx.truncateSync();
        const blockTx = this._blockStore.transaction();
        blockTx.truncateSync();
        return JDB.JungleDB.commitCombined(chainTx, blockTx);
    }

    /** @type {Array.<JDB.Transaction>} */
    get txs() {
        if (this._chainStore instanceof JDB.Transaction) {
            return [this._chainStore, this._blockStore];
        }
        return [];
    }
}
ChainDataStore.CHAINDATA_CACHING_ENABLED = true;
ChainDataStore.CHAINDATA_CACHE_SIZE = 5000;
ChainDataStore.BLOCKS_CACHING_ENABLED = true;
ChainDataStore.BLOCKS_CACHE_SIZE = 0;
ChainDataStore.BLOCKS_RAW_CACHE_SIZE = 500;
Class.register(ChainDataStore);

/**
 * @implements {ICodec}
 */
class ChainDataStoreCodec {
    /**
     * @param {*} obj The object to encode before storing it.
     * @returns {*} Encoded object.
     */
    encode(obj) {
        return typeof obj === 'string' ? obj : obj.toObj();
    }

    /**
     * @param {*} obj The object to decode.
     * @param {string} key The object's primary key.
     * @returns {*} Decoded object.
     */
    decode(obj, key) {
        return typeof obj === 'string' ? obj : ChainData.fromObj(obj, key);
    }

    /**
     * @type {{encode: function(val:*):*, decode: function(val:*):*, buffer: boolean, type: string}|void}
     */
    get valueEncoding() {
        return JDB.JungleDB.JSON_ENCODING;
    }
}

/**
 * @implements {ICodec}
 */
class BlockStoreCodec {
    /**
     * @param {*} obj The object to encode before storing it.
     * @returns {*} Encoded object.
     */
    encode(obj) {
        return obj.serialize();
    }

    /**
     * @param {*} obj The object to decode.
     * @param {string} key The object's primary key.
     * @returns {*} Decoded object.
     */
    decode(obj, key) {
        const block = Block.unserialize(new SerialBuffer(obj));
        block.header._hash = Hash.fromBase64(key);
        return block;
    }

    /**
     * @type {{encode: function(val:*):*, decode: function(val:*):*, buffer: boolean, type: string}|void}
     */
    get valueEncoding() {
        return JDB.JungleDB.BINARY_ENCODING;
    }
}