Exemple #1
0
def add_block():
    block_dict = request.values.to_dict()
    block = Block(**block_dict)
    chain = BlockChain()
    last_block = chain.get_last_block()
    if ProofOfWork(block, last_block).is_block_valid():
        chain.add_block(block)

    return jsonify(received=True)
Exemple #2
0
def mine_block(args):
    blockchain = BlockChain()
    utxo_set = UTXOSet(blockchain)

    utxo_tx = UTXOTx(args.From, args.to, args.amount, utxo_set)
    coin_base_tx = CoinBaseTx(blockchain.address)
    blockchain.add_block([coin_base_tx, utxo_tx])
    utxo_set.update(blockchain.last_block)

    print('Done!')
Exemple #3
0
def cli_sending(From, To, amount):
    blk_chain = BlockChain()
    if not blk_chain.Verify_Block_Chain():
        print("Your database may be damaged. Cannot open this database.")
        return
    tx = transaction.NewUTXOTransaction(From, To, amount, blk_chain)
    if tx == None:
        print("Error: not enough funds")
        return
    blk_chain.add_block(tx)
    print("From: {f}\nTo: {t}\nAmount: {a}".format(f=From, t=To, a=amount))
Exemple #4
0
def create_chain_from_dump(chain_dump):
    gen_blockchain = BlockChain()
    gen_blockchain.build_genesis()
    for idx, block_data in enumerate(chain_dump):
        if idx == 0:
            continue  # skip genesis block
        block = Block(block_data["index"], block_data["transactions"],
                      block_data["timestamp"], block_data["previous_hash"],
                      block_data["nonce"])
        proof = block_data['hash']
        added = gen_blockchain.add_block(block, proof)
        if not added:
            raise Exception("The chain dump is tampered")

    return gen_blockchain
Exemple #5
0
    def create_chain_from_dump(self,chain_dump):
        new_blockchain = BlockChain(consts.difficulty)
        for idx, block_data in enumerate(chain_dump):
            block = Block(block_data["id"],
                        block_data["transactions"],
                        block_data["timestamp"],
                        block_data["previous_hash"],
                        block_data["nonce"])
            block.hash = block_data['hash']

            if idx > 0:
                added = new_blockchain.add_block(block)
                if not added:
                    raise Exception("The chain dump is tampered!!")
            else:  # the block is a genesis block, no verification needed
                new_blockchain.chain.append(block)

        return new_blockchain
Exemple #6
0
def create_chain_from_dump(chain_dump):
    blockchain = BlockChain()
    for index, block_data in enumerate(chain_dump):
        block = Block(
            block_data["index"],
            block_data["transactions"],
            block_data["timestamp"],
            block_data["previous_hash"]
        )
        proof = block_data["hash"]
        if index > 0:
            added = blockchain.add_block(block, proof)
            if not added:
                raise Exception("The chain dump is tampered!!")
            else:  # the block is a genesis block, no verification needed
                blockchain.chain.append(block)
    return blockchain

    pass
class BlockChainTests(unittest.TestCase):
    def setUp(self):
        self.chain = BlockChain()

    def test_is_valid_genesis_correctness(self):
        genesis = self.chain.get_latest_block()
        fake_genesis = Block(
            0,
            hashlib.sha256(
                str(round(time.time() * 1000)).encode('utf-8') +
                b"The Genesis block"), None, int(round(time.time() * 1000)),
            "The Genesis block")

        self.assertTrue(self.chain.is_valid_genesis(genesis))
        self.assertFalse(self.chain.is_valid_genesis(fake_genesis))

    def test_is_valid_new_block_correctness(self):
        timestamp = str(round(time.time() * 1000))
        previous_block = self.chain.get_latest_block()
        correct_block = Block(
            1,
            self.chain.calculate_hash(1, previous_block.hash, timestamp,
                                      "The correct block"),
            self.chain.get_latest_block().hash, int(round(time.time() * 1000)),
            "The correct block")
        incorrect_index = Block(
            2,
            self.chain.calculate_hash(2, previous_block.hash, timestamp,
                                      "The incorrect block"),
            self.chain.get_latest_block().hash, int(round(time.time() * 1000)),
            "The incorrect block")
        incorrect_hash = Block(
            1,
            self.chain.calculate_hash(
                1,
                hashlib.sha256('hi'.encode('utf-8')).hexdigest(), timestamp,
                "The correct block"),
            self.chain.get_latest_block().hash, int(round(time.time() * 1000)),
            "The correct block")

        self.assertTrue(
            self.chain.is_valid_new_block(correct_block,
                                          self.chain.get_latest_block()))
        self.assertFalse(
            self.chain.is_valid_new_block(incorrect_index,
                                          self.chain.get_latest_block()))
        self.assertFalse(
            self.chain.is_valid_new_block(incorrect_hash,
                                          self.chain.get_latest_block()))

    def test_generate_next_block_correctness(self):
        next_block = self.chain.generate_next_block('new data')
        previous_block = self.chain.get_latest_block()

        self.assertEqual(next_block.data, 'new data')
        self.assertEqual(next_block.index, previous_block.index + 1)
        self.assertEqual(
            next_block.hash,
            self.chain.calculate_hash(next_block.index, previous_block.hash,
                                      next_block.timestamp, next_block.data))

    def test_adding_correct_block(self):
        next_block = self.chain.generate_next_block('new data')
        self.chain.add_block(next_block)

        latest_block = self.chain.get_latest_block()

        self.assertEqual(len(self.chain), 2)
        self.assertEqual(latest_block.index, next_block.index)
        self.assertEqual(latest_block.hash, next_block.hash)
        self.assertEqual(latest_block.data, next_block.data)
        self.assertEqual(latest_block.timestamp, next_block.timestamp)
        self.assertEqual(latest_block.previous_hash, next_block.previous_hash)

    def test_adding_incorrect_block(self):
        next_block = self.chain.generate_next_block('new data')
        self.chain.add_block(next_block)

        incorrect_block = Block(
            2,
            hashlib.sha256('wrong'.encode('utf-8')).hexdigest(),
            next_block.hash, int(round(time.time() * 1000)),
            "Not correct block")
        self.chain.add_block(incorrect_block)

        self.assertEqual(len(self.chain), 2)
Exemple #8
0
class Peer(threading.Thread):
    """Peer class. A separate thread for a peer, which simulates all the functionalities of a peer. """
    def __init__(self, pid, get_delay, gen_block):
        threading.Thread.__init__(self)
        self.pid = pid
        self._get_delay = get_delay
        self._connected_peers_ptrs = {}

        self._semaphore = threading.Semaphore(0)
        self._queue = Queue.Queue()
        self._recvd_or_sent = defaultdict(set)  # obj id to set of senders

        # Block
        self._blockchain = BlockChain(gen_block, self.pid)
        self._block_timer = None
        # the random no denotes the computation power of the peer. lower the random no, higher the comp. power.
        self._block_gen_mean = Parameters.block_gen_mean * (random.uniform(
            0.5, 1.0))
#(2)**(-int(pid[2:]))#

    def add_connected_peer(self, peer_id, receiver_func_ptr):
        self._connected_peers_ptrs[peer_id] = receiver_func_ptr

    def gen_transaction(self):
        """
      Generates transactions after expovariant intervals
      Spawned as a new thread, which keeps running in parallel.
    """
        while True:
            waiting_time = random.expovariate(1.0 / Parameters.txn_gen_mean)
            # Sleep for waiting_time
            time.sleep(waiting_time)

            # Sanity check
            if Parameters.num_peers <= 1:
                print "Too few peers"
                continue

            # Randomly generate receiver
            self_id_int = int(self.pid[2:])
            receiver = "P_" + str(
                random.choice(
                    range(0, self_id_int) +
                    range(self_id_int + 1, Parameters.num_peers)))

            # create txn, message
            curr_balance = self._blockchain.get_current_balance()
            amt = random.randint(0, curr_balance)
            t = Transaction(self.pid, receiver, amt)
            msg = Message(t, self.pid, False)

            # Put message in queue to be processed
            self._queue.put(msg)
            self._semaphore.release()

    def _gen_block(self):
        """gen_block helper"""
        block = self._blockchain.generate_block()
        msg = Message(block, self.pid, True)
        self._queue.put(msg)
        self._semaphore.release()
        print Parameters.a[
            self.
            pid] + "Block generated ", block.id, " by peer ", self.pid, " having ", len(
                block.transactions), " txns" + Parameters.a[Parameters.MAX]
        self.gen_block()

    def gen_block(self):
        """
      Generates new block at expovarient intervals
      Creates timer for calling _gen_block
      Timer object can be cancelled on receiving a new block
      _gen_block again calls this function on successfully creating block
    """
        waiting_time = random.expovariate(1.0 / (self._block_gen_mean))  # Tk
        self._block_timer = threading.Timer(waiting_time, self._gen_block)
        self._block_timer.start()

    def receive_message(self, message):
        """Every connected peer has a ptr to this function"""
        self._queue.put(message)
        self._semaphore.release()

    def process_message(self, message):
        """Process message from queue"""

        # add to received objects
        msg_set = self._recvd_or_sent[message.content.id]
        msg_set.add(message.sender)
        new_message = Message(message.content, self.pid, message.is_block)

        # print "Processing message id {} by peer {} sent by {}".format(message.content.id, self.pid, message.sender)
        # Process as per type of message
        if not message.is_block:
            self._blockchain.add_transaction(message.content)
        else:
            if self._blockchain.add_block(message.content):
                self._block_timer.cancel()
                self.gen_block()
            self._blockchain.print_longest_chain()

        # send to connected peers, with conditions
        for p in self._connected_peers_ptrs:
            if p not in msg_set:
                # send to this!
                msg_set.add(p)
                p_recv_ptr = self._connected_peers_ptrs[p]
                delay = self._get_delay(self.pid, p, message.is_block)
                new_message.send(p_recv_ptr, delay)

    def run(self):
        """Thread run"""
        print "Starting Peer ", self.pid
        thread.start_new_thread(self.gen_transaction, ())
        self.gen_block()
        # Process messages from queue
        while True:
            self._semaphore.acquire()
            self.process_message(self._queue.get())

    def write_to_file(self):
        """ Helper function to write the peer's details & its block tree to a file. """
        write_string = ""
        write_string += "Peer ID : " + self.pid + "\n"
        write_string += self._blockchain.write_to_file()
        # print write_string
        return write_string

    """ Helper functions for visualizing the block tree of the peer. """

    def render(self):
        postfix = "(" + self.get_postorder_string() + ")" + self.pid
        return postfix

    # Render helper
    def get_postorder_string(self):
        b_chain = self._blockchain._all_blocks.values()
        tree = {}
        for b in b_chain:
            if b.previous not in tree:
                tree[b.previous] = []
            tree[b.previous].append(b.id)
        return self.get_postorder("B_1", tree)

    # Render helper
    def get_postorder(self, cur, tree):
        sub_ans = ""
        if cur in tree.keys():
            sub_ans = "("
            for i in tree[cur]:
                sub_ans += self.get_postorder(i, tree) + ","
            sub_ans = sub_ans[:-1]
            sub_ans += (")" + cur)
        else:
            sub_ans = cur
        return sub_ans
Exemple #9
0
class Node(object):
    __slots__ = [
        "nodes", "connections", "app", "chain", "session", "addr", "port"
    ]
    MASTER = ("localhost", DEFAULT_PORT)  # FIX localhost to real ip

    def __init__(self, addr, port=DEFAULT_PORT, *, master=False):
        self.addr = addr
        self.port = port

        self.nodes = [Node.MASTER]
        self.connections = []
        self.app = web.Application()
        self.chain = BlockChain()
        self.session = None

        if master == False:
            loop = asyncio.get_event_loop()
            loop.run_until_complete(self._init())

        # just for check with web-browser
        self.app.router.add_get("/query_node_list", self.response_node_list)
        self.app.router.add_get("/query_block_chain",
                                self.response_block_chain)
        # blockchain node do
        self.app.router.add_get("/ws", self.ws_handler)

    async def _init(self):
        await self.query_nodes()
        await self.connect_to_peers(self.nodes)
        await self.register_node()

    async def register_node(self):
        msg = json.dumps({
            'type': MessageType.REGISTER_ME,
            'nodeinfo': (self.addr, self.port)
        })
        await self.broadcast(msg)

    async def query_nodes(self):
        if self.session == None:
            self.session = aiohttp.ClientSession()
        ws = await self.session.ws_connect(
            "http://{}:{}/ws".format(*Node.MASTER))
        await ws.send_str(json.dumps({'type': MessageType.REQUEST_NODE_LIST}))
        msg = await ws.receive()
        self.nodes = json.loads(msg.data)
        await ws.close()

    # just for check
    async def response_block_chain(self, request):
        return web.Response(text=self.chain.json(),
                            content_type="application/json")

    async def response_node_list(self, request):
        return web.Response(text=json.dumps(self.nodes, indent=2),
                            content_type="application/json")

    # blockchain do
    async def connect_to_peers(self, peers):
        if self.session == None:
            self.session = aiohttp.ClientSession()
        for peer in peers:
            if peer[0] == self.addr and peer[1] == self.port:
                continue
            conn = await self.session.ws_connect(
                "http://{}:{}/ws".format(*peer))
            self.connections.append(conn)

    async def broadcast(self, msg):
        for peer in self.connections:
            await peer.send_str(msg)

    async def ws_handler(self, request):
        ws = web.WebSocketResponse()
        await ws.prepare(request)

        async for msg in ws:
            if msg.type == aiohttp.WSMsgType.TEXT:
                msg = json.loads(msg.data)
                if msg["type"] == MessageType.REGISTER_ME:
                    # 새로운 노드 연결
                    node = msg["nodeinfo"]
                    await self.connect_to_peers([node])
                    self.nodes.append(node)
                elif msg["type"] == MessageType.REQUEST_NODE_LIST:
                    # 노드 목록 알려줌
                    await ws.send_json(self.nodes)
                elif msg["type"] == MessageType.REQUEST_MAKE_BLOCK:
                    # 블록 생성함
                    await asyncio.sleep(random.randint(3,
                                                       10))  # 해시 푸는 속도라고 가정하자
                    tr_info = msg["data"]
                    _lastblock = self.chain.get_latest_block()

                    new_block = Block(_lastblock.index + 1, tr_info["data"],
                                      tr_info["timestamp"], _lastblock.hash,
                                      '')
                    new_block.calculate()
                    if new_block.index > self.chain.latest_block.index:
                        self.chain.add_block(new_block)
                        # 컨펌 받기
                        await self.broadcast(
                            json.dumps({
                                "type": MessageType.REQUEST_CONFIRM_BLOCK,
                                "chain": self.chain.json()
                            }))
                        await ws.send_str("[+] Well done !")

                elif msg["type"] == MessageType.REQUEST_CONFIRM_BLOCK:
                    # 블록 생성 컨펌해줌
                    blocks = [
                        Block.from_dict(j) for j in json.loads(msg["chain"])
                    ]
                    if len(blocks) > self.chain.length:
                        if BlockChain.is_valid_chain(blocks):
                            self.chain.blocks = blocks
                    else:
                        pass

            elif msg.type == aiohttp.WSMsgType.Error:
                print('ws connection closed with exception ', ws.exception())
        return ws
Exemple #10
0
from block import Block
from blockchain import BlockChain

if __name__ == '__main__':
    block_chain = BlockChain()
    block_chain.add_block("第一个交易是转账10块")
    block_chain.add_block("第二个交易是转账20")
    block_chain.print_chain()
Exemple #11
0
class Scrooge():
    """
    Class representing the maintainer Scrooge who will be monitoring every progress in his network and responsible for creating `Coin`s and verify transactions.
    This class possesses a private and a public key for its own authentications and 
    """
    def __init__(self, users):
        self.__private_key, self.public_key = gen_keys()
        self.blockchain = BlockChain()
        self.users = users
        self.current_building_block = Block()
        self.last_block = None
        self.init()

    def process_transaction(self, transaction, sender_pubk):
        verified_check = self.verify_transaction(transaction, sender_pubk)
        double_spending_check = self.is_double_spending(transaction)
        if not verified_check:
            logger('|TRANSACTION REJECTED\t-\tInvalid Transaction|')
            return
        if double_spending_check:
            logger('|TRANSACTION REJECTED\t-\tDouble Spending Detected|\n|From User \t\t\t\t'+str(sender_pubk.__hash__())+'|')
            return
        transaction_previous_hash = self.retrieve_transaction_previous_hash(transaction)
        transaction.add_prev_transaction(transaction_previous_hash)
        self.add_transaction_to_block(transaction)

    def retrieve_transaction_previous_hash(self, transaction):
        previous_hashes = []
        for coin in transaction.coins:
            previous_hash = self.blockchain.retrieve_coin_previous_transaction(coin)
            if previous_hash != None:
                previous_hashes.append(previous_hash) 
        if len(previous_hashes) == 0:
            previous_hashes = None  
        elif len(previous_hashes) <= 1:
            previous_hashes = previous_hashes[0]      
        return previous_hashes

    def create_coins(self, amount, user_id):
        return [Coin(user_id) for _ in range(amount)]

    def init(self):
        self.log_users()
        # self.last_transaction = None
        for user in self.users:
            user_id = user.id
            coins = self.create_coins(10, 'scrooge')
            transaction = Transaction(self.public_key, coins, user_id, genre="create")
            if self.__sign(transaction):
                self.process_transaction(transaction, self.public_key)
    
    def log_users(self):
        logger('='*64+'\nUsers Report:\n'+'='*64)
        string_users = ''
        for user in self.users:
            string_users += user.to_string(self)
        logger(string_users+'\n'+'='*64)
    
    def add_coin_to_user(self, transaction):
        receiver_pubk = transaction.receiver
        transaction_coins = transaction.coins
        for user in self.users:
            if user.id == receiver_pubk:
                user.add_transaction(transaction_coins)
                break

    def add_transaction_to_block(self, transaction):
        if not self.current_building_block.add_transaction(transaction):
            self.last_block = self.current_building_block
            self.publish_block(self.current_building_block)
        
    def publish_block(self, block):
        self.blockchain.add_block(block)
        self.current_building_block = Block(transactions=[], hash_prev_block=self.last_block.id)
        for transaction in block.transactions:
            self.add_coin_to_user(transaction)
        self.log_users()



    def verify_transaction(self, transaction, sender_pk):
        return verify(sender_pk, transaction.signature, transaction.hash_val)

    def is_double_spending(self, transaction):
        return self.current_building_block.is_double_spending(transaction)
    
    def sign_last_block(self):
        self.__sign(self.current_building_block)

    def __sign(self, obj):
        if(isinstance(obj, Transaction)):
            try:
                transaction_content = get_hash(obj)
                signature = sign(self.__private_key, transaction_content)
                obj.add_signing(signature)
                obj.add_hash(transaction_content)
                return True
            except:
                return False
        else:
            try:
                block_content = get_hash(obj)
                signature = sign(self.__private_key, block_content)
                obj.add_signing(signature)
                return True
            except:
                return False
Exemple #12
0
class Node(object):
    def __init__(self):
        self.chain = BlockChain()  # 初始情况,默认先查看本地文件
        self.neighbors = set()  # 邻接点
        self.transactions = []  # 交易的集合
        self.new_block = None  # 新的区块,用于挖矿
        self.pk, self.sk = self.get_key()  # 产生结点的公钥和私钥
        self.port = None

        self.add_neighbors()  # 初始化邻居节点

    def add_neighbors(self):
        """
        添加邻居结点
        :return:<None>
        """
        for peers in PEERS:
            if self.port == str(peers).split(':')[2]:  # 不能添加自己的地址
                continue
            self.neighbors.add(peers)

    @staticmethod
    def get_key():
        """
        获取公钥和私钥,注意字符串和byte之间的转化
        :return: <pk,sk>
        """
        pk = None
        sk = None
        if not os.path.exists('/node_key.json'):  # 不存在就新建
            with open('node_key.json', 'w') as json_file:
                sk = SigningKey.generate(curve=NIST384p)
                pk = sk.get_verifying_key()
                msg = {"pk": str(pk), "sk": str(sk.to_string())}
                json_file.write(json.dumps(msg, sort_keys=True))
        else:
            with open('node_key.json') as json_file:  # 存在直接读取
                msg = json_file.read()
                pk = msg['pk'].encode("utf8")  # 转化成bytes
                sk1 = msg['vk'].encode()
                sk = SigningKey.from_string(sk1, curve=NIST384p)
        return pk, sk

    def add_new_neighbor(self, address):
        """
        添加新的邻居结点
        :param address: url地址
        :return: <None>
        """
        parsed_url = urlparse(address)
        if parsed_url.netloc or parsed_url.path:
            self.neighbors.add(address)
        else:
            raise ValueError('Invalid URL')

    def broadcast_transaction(self, transaction):
        """
        向邻接结点广播新的交易
        :return: <None>
        """
        for url in self.neighbors:
            try:
                requests.get(url=url + "/", timeout=0.2)  # 0.2秒的延迟等待,否则就当做掉线处理
                requests.post(url=url + "/receive_transaction",
                              data=json.dump(transaction, sort_keys=True))
            except:
                self.neighbors.remove(url)  # 删除掉线的结点
                print("node" + url + " not online !")

    def broadcast_new_block(self, block):
        """
        向其它结点广播挖出的区块
        :param block: 新的区块
        :return: <None>
        """
        for url in self.neighbors:
            try:
                requests.get(url=url + "/", timeout=0.2)  # 0.2秒的延迟等待,否则就当做掉线处理
                requests.post(url=url + "/get_mined_block",
                              data=json.dump(block, sort_keys=True))
                return True
            except:
                self.neighbors.remove(url)  # 删除掉线的结点
                print("node" + url + " not online !")
                return False

    def add_new_transaction(self, transaction):
        """
        根据签名合法性添加新的交易
        :return: <bool>
        """
        pk_string = str(transaction['sender'])
        signature = transaction['signature']
        message = json.dump(transaction['message'], sort_keys=True)
        # 签名正确,而且不是重复的交易
        if is_valid_transaction(
                pk_string, message,
                signature) and transaction not in self.transactions:
            self.broadcast_transaction(transaction)
            self.transactions.append(transaction)
            return True
        return False

    def add_new_block(self, block):
        """
        添加新的区块,为了验证其它区块挖矿的合理性
        :param block: block
        :return: <bool>
        """
        if proof_of_work(self.chain.last_block, block):
            # 处理交易,并且添加新的区块
            self.chain.last_block.transaction.append(self.transactions)
            self.transactions.clear()
            self.chain.add_block(block)
            return True
        return False

    def get_new_chain(self):
        """
        对于新上线的结点,需要获得最长的有效的区块链
        :return: <None>
        """
        for url in self.neighbors:
            try:
                reponse = requests.get(url=url + "/chain",
                                       timeout=0.2)  # 获取区块链的数据
            except:
                self.neighbors.remove(url)  # 删除掉线的结点
                print("node" + url + " not online !")
                continue
            # length = reponse['length']
            chain = reponse['chain']
            # 最长的有效区块链
            self.chain.resolve_conflicts(chain)

    def mine(self):
        """
        产生新的区块,相当于挖矿
        :return: 新的区块
        """
        # 挖矿之前,需要先和其它结点达成共识
        self.get_new_chain()

        nonce = 0
        last_block = self.chain[-1]
        while not proof_of_work(last_block, nonce):
            nonce += 1

        message = {
            "receiver": self.pk,  # 发给自己
            "amount": 1,  # 奖励的数目
            "data": "a new block",  # 一些其他的信息
        }
        transaction = {
            "sender": "0",  # 发送者为0
            "signature": "0",  # 签名为0
            "message": message  # 自定义的消息
        }
        msg = {
            "index": len(self.chain.blocks),
            "time_stamp": time(),
            "previous_hash": last_block.current_hash,
            "nonce": nonce,
            "data": "",
            "transaction": transaction
        }
        # 挖出新的区块后,需要把交易追加到最后一个区块中,同时清空交易缓存
        last_block = self.chain.last_block
        last_block.transactions.append(self.transactions)
        transaction.clear()
        # 追加新的区块
        block = Block(msg)
        self.chain.add_block(block)
        return block
Exemple #13
0
def mine_block():
    chain = BlockChain()
    block = mine(chain.get_last_block())
    print block.__dict__
    if chain.add_block(block):
        chain.broadcast(block)
Exemple #14
0
# print (block)
# print (block.hash_is_valid(block.hash()))

# print (block.hash(1))
# print (block.hash_is_valid(block.hash(1)))

# # Script 2 : Mining

# block = Block(data='Transaction 100 catcoins from Alice to Bob')
# block.mine()
# print(block)

# # Script 3 : Generate block chain

chain = BlockChain()
print(chain)

block = Block(data="Txn 100 catcoins from Alice to Bob")
chain.add_block(block)
print(chain)

# # Script 4 : Chaining
ts = time.time()
for i in range(6):
    new_block = Block(i)
    new_block.mine()
    chain.add_block(new_block)
print("====")
print("Total Duration: {0:.2f} seconds".format(time.time() - ts))

chain.print()