polkadot-js/client

View on GitHub
packages/client-chains/src/index.ts

Summary

Maintainability
A
0 mins
Test Coverage
// Copyright 2017-2019 @polkadot/client-chains authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

import { Chainspec } from '@polkadot/chainspec/types';
import { Config } from '@polkadot/client/types';
import { BlockDb, StateDb } from '@polkadot/client-db/types';
import { ExecutorInterface } from '@polkadot/client-wasm/types';
import { Header } from '@polkadot/types/interfaces';
import { ChainInterface, ChainGenesis } from './types';

import storage from '@polkadot/api-metadata/storage/static';
import ChainDbs from '@polkadot/client-db';
import createRuntime from '@polkadot/client-runtime';
import { BlockData } from '@polkadot/client-types';
import Executor from '@polkadot/client-wasm';
import { assert, compactStripLength, formatNumber, hexToU8a, logger, u8aToHex } from '@polkadot/util';
import { trieRoot } from '@polkadot/trie-hash';
import { createType } from '@polkadot/types';

import Loader from './loader';

const l = logger('chain');

export default class Chain implements ChainInterface {
  public readonly blocks: BlockDb;

  public readonly chain: Chainspec;

  public readonly executor: ExecutorInterface;

  public readonly genesis: ChainGenesis;

  public readonly state: StateDb;

  private config: Config;

  private dbs: ChainDbs;

  public constructor (config: Config) {
    const chain = new Loader(config);

    this.config = config;
    this.dbs = new ChainDbs(config, chain);
    this.chain = chain.chain;
    this.blocks = this.dbs.blocks;
    this.state = this.dbs.state;
    this.genesis = this.initGenesis();

    const bestHash = this.blocks.bestHash.get();
    const bestNumber = this.blocks.bestNumber.get();
    const logGenesis = bestNumber.isZero()
      ? ''
      : `(genesis ${u8aToHex(this.genesis.block.hash, 48)})`;

    l.log(`${this.chain.name}, #${formatNumber(bestNumber)}, ${u8aToHex(bestHash, 48)} ${logGenesis}`);

    const runtime = createRuntime(this.state);

    this.executor = new Executor(config, this.blocks, this.state, runtime);
  }

  public stop (): void {
    this.dbs.close();
  }

  private initGenesis (): ChainGenesis {
    const bestHash = this.blocks.bestHash.get();
    const hasBest = !!bestHash && !!bestHash.length;

    if (!hasBest || this.config.sync === 'light') {
      return this.createGenesis(!hasBest);
    }

    const bestBlock = this.getBlock(bestHash);

    return this.initGenesisFromBest(bestBlock.header);
    // return this.rollbackBlock(bestBlock.header, true, false);
  }

  private initGenesisFromBest (bestHeader: Header, rollback = true): ChainGenesis {
    const hexState = u8aToHex(bestHeader.stateRoot, 48);

    l.log(`Initializing from state ${hexState}`);

    this.state.setRoot(bestHeader.stateRoot);

    assert(u8aToHex(this.state.db.getRoot(), 48) === hexState, `Unable to move state to ${hexState}`);

    const genesisHash = this.blocks.hash.get(0);

    if (!genesisHash || !genesisHash.length) {
      return this.rollbackBlock(bestHeader, rollback);
    }

    const genesisBlock = this.getBlock(genesisHash);

    return {
      block: genesisBlock,
      code: this.getCode()
    };
  }

  private rollbackBlock (bestHeader: Header, rollback: boolean, isLogging = true): ChainGenesis {
    const prevHash = bestHeader.parentHash;
    const prevNumber = bestHeader.number.unwrap().subn(1);

    if (rollback && prevNumber.gtn(1)) {
      if (isLogging) {
        l.warn(`Unable to validate root, moving to block #${prevNumber.toString()}, ${u8aToHex(prevHash, 48)}`);
      }

      const prevBlock = this.getBlock(prevHash);

      this.blocks.db.transaction((): boolean => {
        this.blocks.bestHash.set(prevHash);
        this.blocks.bestNumber.set(prevBlock.header.number.unwrap());

        return true;
      });

      return this.initGenesisFromBest(prevBlock.header, false);
    }

    throw new Error('Unable to retrieve genesis hash, aborting');
  }

  private getBlock (headerHash: Uint8Array): BlockData {
    const data = this.blocks.blockData.get(headerHash);

    if (!data || !data.length) {
      throw new Error(`Unable to retrieve block ${u8aToHex(headerHash)}`);
    }

    return new BlockData(data);
  }

  private getCode (): Uint8Array {
    const code = this.state.db.get(
      compactStripLength(storage.substrate.code())[1]
    );

    if (!code || !code.length) {
      throw new Error('Unable to retrieve genesis code');
    }

    return code;
  }

  private createGenesis (setBest: boolean): ChainGenesis {
    this.createGenesisState();

    const genesis = this.createGenesisBlock();

    if (setBest) {
      this.blocks.db.transaction((): boolean => {
        this.blocks.bestHash.set(genesis.block.hash);
        this.blocks.bestNumber.set(0);
        this.blocks.blockData.set(genesis.block.toU8a(), genesis.block.hash);
        this.blocks.hash.set(genesis.block.hash, 0);

        return true;
      });
    }

    return genesis;
  }

  private createGenesisBlock (): ChainGenesis {
    const header = createType('Header', {
      stateRoot: this.state.db.getRoot(),
      extrinsicsRoot: trieRoot([]),
      parentHash: new Uint8Array(32)
    });
    const block = new BlockData({
      hash: header.hash,
      header
    });

    return {
      block,
      code: this.getCode()
    };
  }

  private createGenesisState (): void {
    const { genesis: { raw } } = this.chain;

    this.state.db.transaction((): boolean => {
      Object.entries(raw).forEach(([key, value]): void =>
        this.state.db.put(
          hexToU8a(key),
          hexToU8a(value)
        )
      );

      return true;
    });
  }
}