sim.base_models

The classes that make up the core simulator.

View Source
"""
The classes that make up the core simulator.
"""
import math
from enum import Enum

from loguru import logger

from typing import List, Dict

from sim import util
from sim.network_util import get_delay, Region


class Item:
    """Represents objects that can be transmitted over a network (e.g. blocks, messages)."""

    def __init__(self, sender_id: str, size: float):
        """
        Create an Item object.
        * sender_id (str): Id of the sender node. Can be used as a return address.
        * size (float): size of the item in bytes.
        """
        self.id = util.generate_uuid()
        self.size = size
        self.sender_id = sender_id


class Block(Item):
    """Represents a Bitcoin block."""

    def __init__(self, creator, prev_id: str, height: int):
        """
        Create a Block object.

        * miner (`Node`): Node that created the block.
        * prev_id (str): Id of the block this block was mined on top of.
        * height (int): Height of the block in the blockchain.
        """
        super().__init__(None, 0)
        self.prev_id = prev_id
        self.miner = creator.name
        self.created_at = creator.timestamp
        self.height = height
        self.size = 0
        self.tx_count = 0
        self.transactions = []
        self.reward: Reward = None

    def add_tx(self, tx):
        self.transactions.append(tx)
        self.size += tx.size
        self.tx_count += 1

    def has_tx(self, tx):
        return tx in self.transactions

    def __getstate__(self):
        state = self.__dict__.copy()
        del state['sender_id']
        del state['size']
        return state

    def __setstate__(self, state):
        self.__dict__.update(state)

    def __str__(self) -> str:
        return f'BLOCK (id:{self.id}, prev: {self.prev_id})'


class Packet:
    """Wrapper class for transmitting `Item` objects over the network."""

    def __init__(self, payload: Item):
        """
        Create a Packet object.
        * payload (`Item`): Item contained in the packet.
        """
        self.payload = payload
        self.reveal_at = 0


class Node:
    """Represents the participants in the blockchain system."""

    def __init__(self, iter_seconds, name, region, timestamp=0):
        """
        Create a Node object.
        * region (`sim.util.Region`): Geographic region of the node.
        * timestamp (int): Initial timestamp of the node. Defaults to zero.
        """
        self.id = util.generate_uuid()
        self.name = name
        self.timestamp = timestamp
        self.region = region
        self.iter_seconds = iter_seconds

        self.blockchain: Dict[str, Block] = dict()
        """A dictionary that stores `BTCBlock` ids as keys and `BTCBlock`s as values."""

        self.inbox: Dict[int, List[Packet]] = dict()
        """Node's inbox with simulation timestamps as keys and lists of `Item`s to be consumed at that timestamp as values."""

        self.ins: Dict[str, Node] = dict()
        """Dictionary storing incoming connections. Keys are `Node` ids and values are `Node`s."""

        self.outs: Dict[str, Node] = dict()
        """Dictionary storing outgoing connections. Keys are `Node` ids and values are `Node`s."""

        self.last_reveal_times: Dict[str, int] = dict()
        """
        Dictionary with node ids as keys and integers as values. Values correspond to the reveal time of the last message sent to the node with the given id.
        
        This is used to simulate links that can only  transmit one message at a time. A new message starts transmission only after the previous one has been received.
        """

    def __getstate__(self):
        """Return state values to be pickled."""
        state = self.__dict__.copy()
        # Remove the unpicklable entries.
        del state['ins']
        del state['outs']
        del state['inbox']
        del state['timestamp']
        return state

    def __str__(self) -> str:
        return self.name

    def __setstate__(self, state):
        """Restore state from the unpickled state values."""
        self.__dict__.update(state)

    def step(self, seconds: float) -> List[Item]:
        """
        Perform one simulation step. Increments its timestamp by 1 and returns the list of `Item` objects to act on in that step.
        * seconds (float): How many real-time seconds one simulation step corresponds to.
        """
        self.timestamp += 1
        try:
            return [packet.payload for packet in self.inbox.pop(self.timestamp)]
        except KeyError:
            return []

    def reset(self):
        """
        Reset node state back to simulation start, deleting connections as well.
        """
        self.timestamp = 0
        self.blockchain = dict()
        self.inbox = dict()
        self.ins = dict()
        self.outs = dict()
        self.last_reveal_times = dict()

    def send_to(self, node, item: Item):
        """
        Send an item to a specific node. Can be used to respond to messages.
        * node (`sim.base_models.Node`): Target node.
        * item (`sim.base_models.Item`): Item to send.
        """
        packet = Packet(item)
        delay = get_delay(self.region, node.region, item.size) / self.iter_seconds
        reveal_time = math.ceil(max(self.timestamp, self.last_reveal_times.get(node.id, 0)) + delay)
        self.last_reveal_times[node.id] = reveal_time
        packet.reveal_at = reveal_time
        try:
            node.inbox[packet.reveal_at].append(packet)
        except KeyError:
            node.inbox[packet.reveal_at] = [packet]

    def connect(self, *argv):
        """
        Establish an outgoing connection to one or more nodes.
        * argv (`sim.base_models.Node`+): Node(s) to establish connections with.
        """
        for node in argv:
            self.outs[node.id] = node
            node.ins[self.id] = self

    def print_blockchain(self, head: Block = None):
        logger.warning(f'{self.name}')
        logger.warning(f'\tBLOCKCHAIN:')
        for block in self.blockchain.values():
            logger.warning(f'\t\t{block}')


class Reward:
    def __init__(self, node: Node, value: int):
        self.value = value
        self.timestamp = node.timestamp
        self.node = node
#   class Item:
View Source
class Item:
    """Represents objects that can be transmitted over a network (e.g. blocks, messages)."""

    def __init__(self, sender_id: str, size: float):
        """
        Create an Item object.
        * sender_id (str): Id of the sender node. Can be used as a return address.
        * size (float): size of the item in bytes.
        """
        self.id = util.generate_uuid()
        self.size = size
        self.sender_id = sender_id

Represents objects that can be transmitted over a network (e.g. blocks, messages).

#   Item(sender_id: str, size: float)
View Source
    def __init__(self, sender_id: str, size: float):
        """
        Create an Item object.
        * sender_id (str): Id of the sender node. Can be used as a return address.
        * size (float): size of the item in bytes.
        """
        self.id = util.generate_uuid()
        self.size = size
        self.sender_id = sender_id

Create an Item object.

  • sender_id (str): Id of the sender node. Can be used as a return address.
  • size (float): size of the item in bytes.
#   class Block(Item):
View Source
class Block(Item):
    """Represents a Bitcoin block."""

    def __init__(self, creator, prev_id: str, height: int):
        """
        Create a Block object.

        * miner (`Node`): Node that created the block.
        * prev_id (str): Id of the block this block was mined on top of.
        * height (int): Height of the block in the blockchain.
        """
        super().__init__(None, 0)
        self.prev_id = prev_id
        self.miner = creator.name
        self.created_at = creator.timestamp
        self.height = height
        self.size = 0
        self.tx_count = 0
        self.transactions = []
        self.reward: Reward = None

    def add_tx(self, tx):
        self.transactions.append(tx)
        self.size += tx.size
        self.tx_count += 1

    def has_tx(self, tx):
        return tx in self.transactions

    def __getstate__(self):
        state = self.__dict__.copy()
        del state['sender_id']
        del state['size']
        return state

    def __setstate__(self, state):
        self.__dict__.update(state)

    def __str__(self) -> str:
        return f'BLOCK (id:{self.id}, prev: {self.prev_id})'

Represents a Bitcoin block.

#   Block(creator, prev_id: str, height: int)
View Source
    def __init__(self, creator, prev_id: str, height: int):
        """
        Create a Block object.

        * miner (`Node`): Node that created the block.
        * prev_id (str): Id of the block this block was mined on top of.
        * height (int): Height of the block in the blockchain.
        """
        super().__init__(None, 0)
        self.prev_id = prev_id
        self.miner = creator.name
        self.created_at = creator.timestamp
        self.height = height
        self.size = 0
        self.tx_count = 0
        self.transactions = []
        self.reward: Reward = None

Create a Block object.

  • miner (Node): Node that created the block.
  • prev_id (str): Id of the block this block was mined on top of.
  • height (int): Height of the block in the blockchain.
#   def add_tx(self, tx):
View Source
    def add_tx(self, tx):
        self.transactions.append(tx)
        self.size += tx.size
        self.tx_count += 1
#   def has_tx(self, tx):
View Source
    def has_tx(self, tx):
        return tx in self.transactions
#   class Packet:
View Source
class Packet:
    """Wrapper class for transmitting `Item` objects over the network."""

    def __init__(self, payload: Item):
        """
        Create a Packet object.
        * payload (`Item`): Item contained in the packet.
        """
        self.payload = payload
        self.reveal_at = 0

Wrapper class for transmitting Item objects over the network.

#   Packet(payload: sim.base_models.Item)
View Source
    def __init__(self, payload: Item):
        """
        Create a Packet object.
        * payload (`Item`): Item contained in the packet.
        """
        self.payload = payload
        self.reveal_at = 0

Create a Packet object.

  • payload (Item): Item contained in the packet.
#   class Node:
View Source
class Node:
    """Represents the participants in the blockchain system."""

    def __init__(self, iter_seconds, name, region, timestamp=0):
        """
        Create a Node object.
        * region (`sim.util.Region`): Geographic region of the node.
        * timestamp (int): Initial timestamp of the node. Defaults to zero.
        """
        self.id = util.generate_uuid()
        self.name = name
        self.timestamp = timestamp
        self.region = region
        self.iter_seconds = iter_seconds

        self.blockchain: Dict[str, Block] = dict()
        """A dictionary that stores `BTCBlock` ids as keys and `BTCBlock`s as values."""

        self.inbox: Dict[int, List[Packet]] = dict()
        """Node's inbox with simulation timestamps as keys and lists of `Item`s to be consumed at that timestamp as values."""

        self.ins: Dict[str, Node] = dict()
        """Dictionary storing incoming connections. Keys are `Node` ids and values are `Node`s."""

        self.outs: Dict[str, Node] = dict()
        """Dictionary storing outgoing connections. Keys are `Node` ids and values are `Node`s."""

        self.last_reveal_times: Dict[str, int] = dict()
        """
        Dictionary with node ids as keys and integers as values. Values correspond to the reveal time of the last message sent to the node with the given id.
        
        This is used to simulate links that can only  transmit one message at a time. A new message starts transmission only after the previous one has been received.
        """

    def __getstate__(self):
        """Return state values to be pickled."""
        state = self.__dict__.copy()
        # Remove the unpicklable entries.
        del state['ins']
        del state['outs']
        del state['inbox']
        del state['timestamp']
        return state

    def __str__(self) -> str:
        return self.name

    def __setstate__(self, state):
        """Restore state from the unpickled state values."""
        self.__dict__.update(state)

    def step(self, seconds: float) -> List[Item]:
        """
        Perform one simulation step. Increments its timestamp by 1 and returns the list of `Item` objects to act on in that step.
        * seconds (float): How many real-time seconds one simulation step corresponds to.
        """
        self.timestamp += 1
        try:
            return [packet.payload for packet in self.inbox.pop(self.timestamp)]
        except KeyError:
            return []

    def reset(self):
        """
        Reset node state back to simulation start, deleting connections as well.
        """
        self.timestamp = 0
        self.blockchain = dict()
        self.inbox = dict()
        self.ins = dict()
        self.outs = dict()
        self.last_reveal_times = dict()

    def send_to(self, node, item: Item):
        """
        Send an item to a specific node. Can be used to respond to messages.
        * node (`sim.base_models.Node`): Target node.
        * item (`sim.base_models.Item`): Item to send.
        """
        packet = Packet(item)
        delay = get_delay(self.region, node.region, item.size) / self.iter_seconds
        reveal_time = math.ceil(max(self.timestamp, self.last_reveal_times.get(node.id, 0)) + delay)
        self.last_reveal_times[node.id] = reveal_time
        packet.reveal_at = reveal_time
        try:
            node.inbox[packet.reveal_at].append(packet)
        except KeyError:
            node.inbox[packet.reveal_at] = [packet]

    def connect(self, *argv):
        """
        Establish an outgoing connection to one or more nodes.
        * argv (`sim.base_models.Node`+): Node(s) to establish connections with.
        """
        for node in argv:
            self.outs[node.id] = node
            node.ins[self.id] = self

    def print_blockchain(self, head: Block = None):
        logger.warning(f'{self.name}')
        logger.warning(f'\tBLOCKCHAIN:')
        for block in self.blockchain.values():
            logger.warning(f'\t\t{block}')

Represents the participants in the blockchain system.

#   Node(iter_seconds, name, region, timestamp=0)
View Source
    def __init__(self, iter_seconds, name, region, timestamp=0):
        """
        Create a Node object.
        * region (`sim.util.Region`): Geographic region of the node.
        * timestamp (int): Initial timestamp of the node. Defaults to zero.
        """
        self.id = util.generate_uuid()
        self.name = name
        self.timestamp = timestamp
        self.region = region
        self.iter_seconds = iter_seconds

        self.blockchain: Dict[str, Block] = dict()
        """A dictionary that stores `BTCBlock` ids as keys and `BTCBlock`s as values."""

        self.inbox: Dict[int, List[Packet]] = dict()
        """Node's inbox with simulation timestamps as keys and lists of `Item`s to be consumed at that timestamp as values."""

        self.ins: Dict[str, Node] = dict()
        """Dictionary storing incoming connections. Keys are `Node` ids and values are `Node`s."""

        self.outs: Dict[str, Node] = dict()
        """Dictionary storing outgoing connections. Keys are `Node` ids and values are `Node`s."""

        self.last_reveal_times: Dict[str, int] = dict()
        """
        Dictionary with node ids as keys and integers as values. Values correspond to the reveal time of the last message sent to the node with the given id.
        
        This is used to simulate links that can only  transmit one message at a time. A new message starts transmission only after the previous one has been received.
        """

Create a Node object.

  • region (sim.util.Region): Geographic region of the node.
  • timestamp (int): Initial timestamp of the node. Defaults to zero.
#   blockchain: Dict[str, sim.base_models.Block]

A dictionary that stores BTCBlock ids as keys and BTCBlocks as values.

#   inbox: Dict[int, List[sim.base_models.Packet]]

Node's inbox with simulation timestamps as keys and lists of Items to be consumed at that timestamp as values.

#   ins: Dict[str, sim.base_models.Node]

Dictionary storing incoming connections. Keys are Node ids and values are Nodes.

#   outs: Dict[str, sim.base_models.Node]

Dictionary storing outgoing connections. Keys are Node ids and values are Nodes.

#   last_reveal_times: Dict[str, int]

Dictionary with node ids as keys and integers as values. Values correspond to the reveal time of the last message sent to the node with the given id.

This is used to simulate links that can only transmit one message at a time. A new message starts transmission only after the previous one has been received.

#   def step(self, seconds: float) -> List[sim.base_models.Item]:
View Source
    def step(self, seconds: float) -> List[Item]:
        """
        Perform one simulation step. Increments its timestamp by 1 and returns the list of `Item` objects to act on in that step.
        * seconds (float): How many real-time seconds one simulation step corresponds to.
        """
        self.timestamp += 1
        try:
            return [packet.payload for packet in self.inbox.pop(self.timestamp)]
        except KeyError:
            return []

Perform one simulation step. Increments its timestamp by 1 and returns the list of Item objects to act on in that step.

  • seconds (float): How many real-time seconds one simulation step corresponds to.
#   def reset(self):
View Source
    def reset(self):
        """
        Reset node state back to simulation start, deleting connections as well.
        """
        self.timestamp = 0
        self.blockchain = dict()
        self.inbox = dict()
        self.ins = dict()
        self.outs = dict()
        self.last_reveal_times = dict()

Reset node state back to simulation start, deleting connections as well.

#   def send_to(self, node, item: sim.base_models.Item):
View Source
    def send_to(self, node, item: Item):
        """
        Send an item to a specific node. Can be used to respond to messages.
        * node (`sim.base_models.Node`): Target node.
        * item (`sim.base_models.Item`): Item to send.
        """
        packet = Packet(item)
        delay = get_delay(self.region, node.region, item.size) / self.iter_seconds
        reveal_time = math.ceil(max(self.timestamp, self.last_reveal_times.get(node.id, 0)) + delay)
        self.last_reveal_times[node.id] = reveal_time
        packet.reveal_at = reveal_time
        try:
            node.inbox[packet.reveal_at].append(packet)
        except KeyError:
            node.inbox[packet.reveal_at] = [packet]

Send an item to a specific node. Can be used to respond to messages.

#   def connect(self, *argv):
View Source
    def connect(self, *argv):
        """
        Establish an outgoing connection to one or more nodes.
        * argv (`sim.base_models.Node`+): Node(s) to establish connections with.
        """
        for node in argv:
            self.outs[node.id] = node
            node.ins[self.id] = self

Establish an outgoing connection to one or more nodes.

#   def print_blockchain(self, head: sim.base_models.Block = None):
View Source
    def print_blockchain(self, head: Block = None):
        logger.warning(f'{self.name}')
        logger.warning(f'\tBLOCKCHAIN:')
        for block in self.blockchain.values():
            logger.warning(f'\t\t{block}')
#   class Reward:
View Source
class Reward:
    def __init__(self, node: Node, value: int):
        self.value = value
        self.timestamp = node.timestamp
        self.node = node
#   Reward(node: sim.base_models.Node, value: int)
View Source
    def __init__(self, node: Node, value: int):
        self.value = value
        self.timestamp = node.timestamp
        self.node = node