Home Reference Source Repository

src/lib/dagchain/dagchain.js

import { DAGNode } from 'ipld-dag-pb';
import { decode, encode } from 'bs58';
import EventEmitter from 'events';
import { chain, difficulty, getUnspent, longestChain, lastBlock, newGenesisDAGNode } from './dagchain-interface';
import { DAGBlock, createDAGNode, validate } from './dagblock';
import { read, write } from 'crypto-io-fs';
import { join } from 'path';
import { genesis, network, genesisCID, localIndex, localCurrent, localDAGAddress, localDAGMultiaddress, networks } from './../../params';
import { debug, hashFromMultihash, multihashFromHex, succes, log } from './../../utils';
import bus from './../bus';
// import ipfsKeepAlive from './../../ipfs-keep-alive';
// import bitswap from 'ipfs-bitswap';
// TODO: implement bitswap
// export const ipfs = (() => new IPFS())();

export class DAGChain extends EventEmitter {
  get link() {
    return this.name.replace('/ipfs/', '')
  }
  get links() {
    return this.node ? this.node.links : [];
  }
  get index() {
    const links = [];
    this.links.forEach(link => {

      links[link.name] = link;
    });
    return links.length > 0 ? links : [];
  }
  constructor({ipfs}) {
    super();
    this.announceBlock = this.announceBlock.bind(this);

    this.chain = chain;
    this.ipfs = ipfs;
  }

  init() {
    return new Promise(async (resolve, reject) => {
      try {
        await this.ipfs.pubsub.subscribe('block-added', this.announceBlock);
        const { hash } = await longestChain();
        this.name = hash || await localDAGMultiaddress();
        this.node = await this.get(this.link);
        log(`Running on the ${network} network`);
        await this.loadChain();
      } catch (error) {
        // TODO: finishe the genesis module
        if (genesis) {
          log(`Creating genesis block on the ${network} network`);
          await this.newDAGChain();
        } else {
          reject(error)
        }
      }
    });
  }
  async resolve(name) {
    return await this.ipfs.name.resolve(name, {recursive: true});
  }

  async get(multihash) {
    return await this.ipfs.object.get(multihash);
  }

  async put(DAGNode) {
    return await this.ipfs.object.put(DAGNode);
  }

  async pin(multihash) {
    return new Promise(async (resolve, reject) => {
      try {
        await this.ipfs.pin.add(multihash);
        resolve();
      } catch (e) {
        reject(e)
      }
    });
  }

  /**
   * addLink
   */
  async addLink(multihash, link) {
    const newDAGChain = await this.ipfs.object.patch.addLink(multihash, link);
    return await this.publish(encode(newDAGChain.multihash));
  }

  async lastLink() {
    try {
      await this.sync();
      const height = Number(this.links.length) - 1;
      for (const link of this.links) {
        if (Number(link.name) === height) {
          return link.multihash;
        }
      }
    } catch (e) {
      console.error('Sync Error::', e);
    }
  }
  // TODO: decentralize
  async lastBlock() {
    const lastLink = await this.lastLink();
    return await new DAGBlock(lastLink);
  }

  /**
   * resolves to the latest chainObject
   */
  async sync() {
    const { hash } = await longestChain();
    this.name = hash || await localDAGMultiaddress();
    this.node = await this.get(this.link);
    return this.link;
  }

  async publish(multihash) {
    const published = await this.ipfs.name.publish(multihash);
    if (this.name) {
      await this.sync();
    }
    await this.pin(published['value']);
    return published['name']  // only needed when creating genesis block
  }

  async resolveBlocks(multihash, index) {
    try {
      let block = await new DAGBlock(encode(multihash));
      if (block.index > index) {
        await this.pin(encode(multihash)); // pin block locally
        console.log(`added block: ${block.index}  ${block.hash}`);
      }
      chain[block.index] = block;
      console.log(`loaded block: ${block.index}  ${block.hash}`);
      if (block.prevHash && block.prevHash.length > 3) {
        return this.resolveBlocks(Buffer.from(`1220${block.prevHash}`, 'hex'), index);
      }
      return;
    } catch (e) {
      console.error(e);
    }
  }

  async syncChain() {
    try {
      bus.emit('syncing', true);
      if (this.index) {
        const { index, prevHash } = await this.localBlock();
        const height = this.index.length - 1;
        let syncCount = height - index;
        const multihash = this.index[height].multihash;
        log(`syncing ${syncCount} blocks`)
        await this.resolveBlocks(multihash, index);
        await this.updateLocals(multihash.toString('hex'), height, this.link);
        await this.publish(this.link);
      }
      bus.emit('syncing', false);
    } catch (e) {
      console.error('syncChain', e);
    }

  }

  async localBlock() {
    try {
      const CID = await read(localCurrent, 'string'); // read local chain state
      const multihash = encode(new Buffer(CID, 'hex'));
      const current = await this.get(multihash);
      const { index } = JSON.parse(current.data.toString());
      debug(`current local block CID ${CID}`);
      debug(`current local dag: ${multihash}`);
      return {
        index,
        multihash,
        CID
      }
    } catch (e) {
      const CID = await this.get(encode(new Buffer(genesisCID, 'hex')));
      await write(localCurrent, CID.multihash.toString('hex'));
      return await this.localBlock();
    }
  }

  async loadChain() {
    await this.syncChain();
    bus.emit('ready', true);
  }

  addBlock(block) {
    return new Promise(async (resolve, reject) => {
      log(`add block: ${block.index}  ${block.hash}`);
      chain[block.index] = block;
      bus.emit('block-added', block);
      await this.pin(multihashFromHex(block.hash)); // pin block locally
      await this.updateLocals(`1220${block.hash}`, block.index, this.link);
      try {
        await this.publish(this.link);
      } catch (e) {
        console.warn(e);
      }
    });
  }
  // TODO: write using ipfs.files.write
  writeLocals(CID, index, DAGAdress) {
    return new Promise(async (resolve, reject) => {
      await write(localIndex, JSON.stringify(index));
      await write(localCurrent, CID);
      await write(localDAGAddress, DAGAdress);
      resolve();
    });
  }

  updateLocals(CID, height, DAGAdress) {
    return new Promise(async (resolve, reject) => {
      try {
        const index = await read(localIndex, 'json');
        index.push([height, CID]);
        await this.writeLocals(CID, index, DAGAdress);
      } catch (error) {
        await this.writeLocals(CID, [[height, CID]], DAGAdress);
      }
      resolve();
    });
  }

  /**
   * Initialize a new chain on the IPFS network
   * Creates creates & saves the genesisBlock to IPFS, blocks are pinned so they aren't removeable on the local side.
   *
   * @param {object} block The genesis block to write
   * @setup PART of Easy setup your own blockchain, more info URL...
   */
  async newDAGChain() {
    try {
      const genesisDAGNode = await newGenesisDAGNode(difficulty());
      const block = await this.put(genesisDAGNode);
      // await this.pin(encode(block.multihash));
      const chainDAG = await this.ipfs.object.new('unixfs-dir');
      const height = JSON.parse(genesisDAGNode.data.toString()).index;
      const newDAGChain = await this.ipfs.object.patch.addLink(chainDAG.multihash, {name: height, size: block.size, multihash: block.multihash});
      const CID = block.multihash.toString('hex'); // The CID as an base58 string
      await this.updateLocals(CID, 0, encode(newDAGChain.multihash))
      succes('genesisBlock created');
      log(`genesisBlock: ${block.data.toString()}`);
      log(`genesis: ${encode(block.data)}\nCID:\t${CID}`);
      log(`DAGChain name ${encode(newDAGChain.multihash)}`);
      return;
    } catch (e) {
      console.error(e);
    }
  }

  async updateLocalChain(block) {
    const dagnode = await new DAGBlock().put(block);
    await this.addLink(this.link, {name: block.index, size: dagnode.size, multihash: dagnode.multihash});
    return;
  }

  // TODO: go with previous block instead off lastBlock
  // TODO: validate on sync ...
  async announceBlock({data, from}) {
      const block = JSON.parse(data.toString());
      try {
        // const previousBlock = await lastBlock(); // test
        await validate(chain[chain.length - 1], block, difficulty(), getUnspent());
        // await this.sync();
        await this.updateLocalChain(block);
        this.addBlock(block); // add to chain
      } catch (error) {
        // TODO: remove publish invalid-block
        this.ipfs.pubsub.publish('invalid-block', new Buffer.from(JSON.stringify(block)));
        bus.emit('invalid-block', block);
        return console.error(error);
      }
    }
}