Extending SBS
SBS is built to be changed. This page shows how to make the most common changes. Each recipe points to the concept page that explains the part you change.
A map of the code
All the simulator code is in src/Simulator:
| Folder or file | What it holds |
|---|---|
Blockchain.py |
The entry point and the command-line flags. |
Parameters.py |
The loaded config, as global dictionaries such as Parameters.network. |
Engine |
The event queue, the events and the main loop (see The simulation engine). |
Chain |
The node, blocks, transactions and the network model (see Nodes, blocks and transactions and The network). |
Chain/Consensus |
The consensus protocols, one folder each (see Consensus). |
Chain/Reconfiguration |
The configuration chain and the centralised method (see Reconfiguration). |
Manager |
The Manager and its system events (see The Manager). |
Utils |
Metrics, snapshots, the printed output and other helpers. |
Run all commands from src/Simulator.
Add a parameter
- Add the parameter, with a comment, to a group in
base.yaml. - If scenario runs need it too, add it to
scenario.yaml. - Read it in the code through its group, such as
Parameters.application["my_parameter"].
--set works for the new parameter straight away.
See Configuration.
Add a consensus protocol
The easiest way to start is to copy PBFT, which is the simplest protocol.
In this example, the new protocol is called MyPBFT.
1. Copy the folder.
Copy Chain/Consensus/PBFT to Chain/Consensus/MyPBFT.
Rename each file from PBFT_* to MyPBFT_*.
Then replace PBFT with MyPBFT in every file of the new folder.
This renames the class, its NAME, its imports and its settings in Parameters.
2. Register the protocol. Four places outside the folder need to know about it.
In Chain/Consensus/Protocols.py, add it to PROTOCOLS:
from Chain.Consensus.MyPBFT.MyPBFT_state import MyPBFT
PROTOCOLS = {
PBFT.NAME: PBFT,
BigFoot.NAME: BigFoot,
Tendermint.NAME: Tendermint,
MyPBFT.NAME: MyPBFT,
}
In Parameters.py, add a place for its settings, and load them in load_params_from_config:
In base.yaml and scenario.yaml, add the path to its config file to the consensus group:
3. Run it.
The results count the blocks of each protocol, so the new name shows up at once.
To let the random reconfiguration pick it too, add it to reconfiguration.random_configuration.protocols.
4. Change the logic. Now you can change the copy into your own protocol. Each file has one job:
| File | What to change |
|---|---|
MyPBFT_state.py |
The protocol's data on a node, how a round starts, and how the proposer is chosen. handle_event sends each event type to its handler. |
MyPBFT_transition.py |
What the node does when it gets each type of message. |
MyPBFT_messages.py |
How the node builds and sends each type of message. |
MyPBFT_timeouts.py |
What happens when a round times out. |
MyPBFT_config.yaml |
The protocol's own settings. |
Keep these rules from PBFT:
- The class must implement the interface in
ConsensusProtocol.py. - Put
"CP": state.NAMEin the payload of every event. When a node switches protocol, SBS drops the events of the old one. - Call
self.node.update(time)when a new round starts and when a round times out. This is where a node applies a new configuration (see Reconfiguration). - Use the shared round change in
Rounds.py, unless your protocol changes rounds in another way.
Add a system event
A system event lets the Manager change the simulation at a given time. The steps are on The Manager page. This example raises the transaction rate at a set time, to model a burst of users.
First, add two parameters to the application group of base.yaml:
# Raise the transaction rate to burst_tx_per_sec at this time (-1 = no burst)
burst_at: -1
burst_tx_per_sec: 300
Then write the event in a new file, Manager/SystemEvents/BurstEvents.py:
from Parameters import Parameters
from Engine.Event import SystemEvent
from Utils import Report
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from Manager.Manager import Manager
def schedule_burst_event(manager: "Manager") -> None:
"""Schedules the start of the burst"""
time = Parameters.application["burst_at"]
event = SystemEvent(time=time, payload={"type": "burst"})
manager.sim.q.add_event(event)
def handle_burst_event(manager: "Manager", event: SystemEvent) -> None:
"""Raises the transaction rate for the rest of the run"""
Parameters.application["tx_per_sec"] = Parameters.application["burst_tx_per_sec"]
Report.print_update(event.time, "BURST", "tx rate", f"{Parameters.application['tx_per_sec']} tx/s")
Then connect it in Manager/Manager.py.
Import the file:
Schedule the event in init_system_events:
And handle it in handle_system_event:
Now run it with a burst at 150 seconds:
SymBChainSim | PBFT | 16 nodes | 300 s | seed 1837413
0 / 300 s 0 blocks 0 tx
100 / 300 s 66 blocks 11,865 tx
[BURST] 150.0 s tx rate 300 tx/s
200 / 300 s 121 blocks 25,369 tx
300 / 300 s 160 blocks 41,619 tx
RESULTS (mean over 16 nodes)
Blocks 160 (PBFT 160)
Transactions 41,619 confirmed, 19,190 pending
Throughput 139.9 tx/s
Mean latency 12.77 s
Mean block time 1.86 s
Mean block size 0.63 MB
Decentralisation (Gini) 0.15 (0 = perfectly decentralised)
Snapshots → Outputs/Snapshots/snapshot.json (plot with ScenarioGenerationAndVisualisation/snapshot_visualisation.ipynb)
Finished in 2.6 s
PBFT cannot keep up with 300 transactions per second. So the pending transactions and the latency grow.
When the change takes effect
The transactions are created every application.tx_interval seconds, for the next interval.
So the new rate starts with the next batch of transactions.
This event happens once. To repeat an event, schedule the next one at the end of its handler, as the fault events do.
Add a reconfiguration option
SBS can reconfigure the protocol, the block size and the block time. To add another parameter:
- Add it to the first configuration, in
ConfigurationBlock.genesis_block. - Add it to
ReconfigurationState.configuration, the configuration a node currently uses. - Apply it in
ReconfigurationState.try_apply_configuration. The node calls this only at safe points, so think about when your change is safe. If the protocol must restart after the change, returnTrue. - In the code that uses the parameter, read it from
node.reconfiguration_state.configurationinstead ofParameters. Each node can then use a different value while the change spreads. - Add it to the decision method, for example
create_random_configuration_block, and add its options toreconfiguration.random_configuration.
The code is in Chain/Reconfiguration.
Add a decision method
The centralised method picks each configuration at random. You can replace it in two ways:
- Keep the event, change the choice. Replace the random choice in
create_random_configuration_blockwith your own method, such as a heuristic or a reinforcement learning agent. - Write a new event. Write a system event that creates a
ConfigurationBlockand sends it withpropagate_configuration_block. Use it in place of the reconfiguration event inManager.init_system_events.
In both cases, the nodes spread and apply the new configuration in the same way. See Reconfiguration.
Add a metric
See Adding a metric.
Check your change
- Compare the output. With the same seed and config, SBS gives the same results every time. Run the same command before and after your change, and compare the outputs. If your change should not affect the results, they must be identical.
- Step through events. With
--debug-at TIME, the run switches to debugging mode at that time. It prints the event queue of every node, and it waits for you before each event. - Read the log.
--set simulation.logging_level=DEBUGprints a line for each step of each node. The log also goes tosrc/Outputs/Logs/log.txt.