In this notebook, we will simulate the evolution of a simplified blockchain system on which Zeth is deployed in order to better educate the choice of the various protocol parameters (number of input notes, number of output notes, depth of the Merkle tree etc). This notebook can be used for further experiments (using parameters which are not yet documented).
This notebook is focused around some key questions regarding the blockchain state growth under different configurations. This is particularly important since the growth of the blockchain state is a key factor that impacts the number of nodes on the distributed system (it drives the HW requirements for existing nodes on the network as well as affects how easy it is for new nodes to join the network (i.e. sync a new node)). In other words, as the number of nodes on a blockchain "boils down to convenience", we are interested to see how convenient (easy/fast/cheap) it is to validate on a blockchain under various network assumptions. Studying the state growth provides valuable hints with that regard. Nevertheless, the reader is reminded that, by the very essence of modeling, we make several simplifying assumptions in the section below that will thus ignore various aspects of a "real-life" running system.
Hopefully this one is somewhat useful...
This notebook is structured around some key open questions that aim to better understand the impact of Zeth on a blockchain system. Likewise, in a future work, we will be investigasting how well the privacy-preserving scalability solutions Zecale performs in term of both data compression and TPS.
After how much time does the Zeth merkle tree become full for a given Merkle tree depth?
How does the chain state size compare when only Zeth transactions are used, as opposed to the case where only "plain" EOA-to-EOA Ethereum transactions are used?
What is the gas cost per byte for EoA-to-EoA transactions and for Zeth transactions?
After how much time does the chain data become higher that 1TB? (1TB is the max storage of the latest XPS-15 laptop. We use this threshold as an indicator to track after how much time running a node becomes inconvenient and requires some "specialized" HW)
What is the impact of Zeth on the TPS of the system?
How well does Zecale compress the state (compared to "vanilla Zeth")? Furthermore:
This question is answered in another notebook dedicated to Zecale
# cadCAD configuration modules
from cadCAD.configuration.utils import config_sim
from cadCAD.configuration import Experiment
from cadCAD import configs
# cadCAD simulation engine modules
from cadCAD.engine import ExecutionMode, ExecutionContext
from cadCAD.engine import Executor
# Analysis and plotting modules
import pandas as pd
import plotly
pd.options.plotting.backend = "plotly"
# Misc
## Pretty print function
from pprint import pprint
# Numpy
import numpy as np
We assume that the Zeth state ($\zeta_z$) is only made of the following:
Importantly, we do not account for the storage cost of the Zeth contracts (one time operation carried out at initialization time) and their various storage constants (i.e. constant protocol parameters) etc.
We assume that the blockchain state ($\zeta_b$) is only made of:
Some of these assumptions are not strictly necessary, but that's helpful to make them for now, to further simplify the system and remove any potential unexpected moving pieces
data
Note: At the time of writing, the go-ethereum client uses the LevelDB database which compresses with Snappy. Other databases may be used by other clients however. For instance, the openethereum client uses Rocksdb which compression can be further configured to use lz4 for instance (though Snappy is kept as default). See also the documentation of Turbo-Geth which proposes an alternative to go-ethereum to organise the persistent data in its database.
Below are the parameters that remain constant across simulations.
Below are the parameters that may change across (and during) simulations.
These are the constants initialization values that do not vary across executions
Each Zeth transaction mined adds:
We first start by tracking the blockchain state growth when only plain "EoA-to-EoA" transactions are carried out. Then, we model Zeth with different protocol parameters to see how the blockchain state size grows under different conditions, as well as track the rate at which the Merkle tree of Zeth notes commitments is filled.
We use A/B testing and "Parameters Sweep" simulations to study the state growth under different blockchain configurations (block gas limit etc.) and Zeth configurations (Merkle tree depth, JSIN/JSOUT etc.):
data
) EoA-to-EoA transactions are mined. This simulation uses "Parameters Sweep" to simulate the system under various blockchain configurations.All these simulations are deterministic (no MC runs) and represent 24h worth of data. Since no random runs are employed, the simulation results can be cached into a file to avoid multiple (expensive) runs of the model's simulations.
Before pursuing with the simulation, it is worth clarifying how the input dataset has been obtained.
Ideally, in order to determine the gas cost of a state transition, one may want to use the blockchain network's gas table along with the set of opcodes defining the state transition in order to come up with a deterministic formula that computes the cost of the smart-contract call. However, such approach is not sufficient to properly determine the cost of a state transition, since several opcodes (such as SSTORE
) have different costs depending on the smart-contract's state (i.e. depending if empty storage slots are initiliazed or simply re-written).
As a consequence, and to ease the process, the following data (transactions gas cost and byte-size) are obtained via empirical experiments, during which a set of transactions are fired on a test network. The results below are obtained via the arithmetic mean of a simulation's results.
Importantly, certain Zeth configurations (i.e. certain curve selections: BLS12_377
and BW6_761
) necessitate extensions to the EVM in order to support curve operations (point addition, scalar multiplication) and pairings for remarkable pairing groups. As such, Zeth related simulations have been carried out on an extended version of ganache-cli.
We use some Ethereum mainnet data as basis to determine values for the blockchain-related variables and constants.
# Size of a "standard" raw (i.e. rlp encoded) EoA-to-EoA transaction (in bytes)
# Here, we assume that no extra `data` is set in the transaction. We obtain this value
# by taking the arithmetic mean of the size of a few "plain" EoA-to-EoA transactions
# (i.e. without additional `data`).
ETHTXSIZE = 111
# See https://github.com/ethereum/go-ethereum/blob/v1.10.1/core/types/receipt.go#L48
# and
# https://github.com/ethereum/go-ethereum/blob/v1.10.1/core/types/receipt.go#L92-L97
# Receipts for succesful EoA-to-EoA transactions will be of the form:
# ["0x5208520852085208",[],"0x1"], leading to RLP encodings of the form:
# 0xcb885208520852085208c001, which are 12 bytes long.
ETH_RECEIPT_SIZE = 12
# Approximate size of an Ethereum block header (in bytes)
# See: https://ethereum.github.io/yellowpaper/paper.pdf and
# https://github.com/ethereum/go-ethereum/blob/v1.10.1/core/types/bloom9.go#L32-L38
# for reference
BLOCKHEADERSIZE = 508 # 32 + 32 + 20 + 32 + 32 + 32 + 256 + 32 + 32 + 8
# Intrinsic gas cost of a transaction
DGAS = 21000
# Compression ratio for Snappy on the chain state
# See: https://github.com/google/snappy#performance
COMPRESSION_RATIO = 1.5
# The block gas limit and block time below are obtained as the median
# of the "Value" column from the following datasets provided by Etherscan.io:
# - https://etherscan.io/chart/blocktime (exported on 11/03/2021 into a file named `export-BlockTime.csv`)
# - https://etherscan.io/chart/gaslimit (exported on 11/03/2021 into a file named `export-GasLimit.csv`)
# More precisely, the values were obtained by running the following commands:
# ```python
# csv_result = pd.read_csv('export-GasLimit.csv')
# MEDIAN_BLOCKGASLIMIT = math.ceil(csv_result["Value"].median())
#
# csv_result = pd.read_csv('export-BlockTime.csv')
# MEDIAN_BLOCKTIME = math.ceil(csv_result["Value"].median())
# ```
MAINNET_MEDIAN_BLOCKGASLIMIT = 7996822
MAINNET_MEDIAN_BLOCKTIME = 15
PRECOMPILED_CURVES = {}
# See Ethereum Istanbul gas table:
# https://github.com/ethereum/go-ethereum/blob/master/params/protocol_params.go
PRECOMPILED_CURVES['BN254'] = {'ECADDCOST': 150, 'ECMULCOST': 6000, 'ECPAIRBASECOST': 45000, 'ECPAIRPERPOINTCOST': 34000}
# Gas table extension obtained during the early Zecale simulations (June 2020).
# WARNING: The values of the parameters below need to be refined (new software benchmarks need to be carried out.)
PRECOMPILED_CURVES['BLS12_377'] = {'ECADDCOST': 300, 'ECMULCOST': 12000, 'ECPAIRBASECOST': 90000, 'ECPAIRPERPOINTCOST': 68000}
pprint(PRECOMPILED_CURVES)
{'BLS12_377': {'ECADDCOST': 300, 'ECMULCOST': 12000, 'ECPAIRBASECOST': 90000, 'ECPAIRPERPOINTCOST': 68000}, 'BN254': {'ECADDCOST': 150, 'ECMULCOST': 6000, 'ECPAIRBASECOST': 45000, 'ECPAIRPERPOINTCOST': 34000}}
#########################
# Benchmark dataset #
#########################
# Data obtained via the `singleton_deterministic_agent.sh` Zeth script
# ran under different Zeth configurations. The data used below is obtained
# as the arithmetic mean of all the transactions fired by the bot script above
# on a local testnet (and on the Autonity Bakerloo Testnet).
# Note: In order to have a more flexible set of simulation scripts to use against
# our local test network (without additional tooling), it is desirable for the issue
# https://github.com/trufflesuite/ganache-core/issues/135
# to be tackled and integrated into clearmatics/ganache-cli.
# (`eth_getRawTransactionByHash` is already available in Geth and Autonity).
#########
# TODO: #
#########
#
# - Consider moving this to an external CSV file that we load here.
# - Gather mode data points for the various settings of interest AND/OR
# consider computing some of these data points as part of the model
# from the system's parameters (e.g. gas cost/size of txs etc.)
#########
# - The documented sizes are the sizes (in bytes) of the Zeth JSON transaction objects.
#metrics_df = pd.DataFrame(
# [
# ["BN254", 32, 2, 2, 3090, 1315520],
# ["BLS12_377", 32, 2, 2, 3603, 1353261]
# # Switch JSOUT to 3 (e.g. to pay a Relay with an output note)
# #["BN254", 32, 2, 3, XX, XX], # TODO
# #["BLS12_377", 32, 2, 3, XX, XX] # TODO
# ],
# columns=['curve', 'mk_depth', 'jsin', 'jsout', 'zeth_tx_size', 'zeth_tx_gcost']
#)
# Approximate size (in bytes) for RLP encoded Zeth transaction receipt
# Follows the specified storage encoding of a receipt
# https://github.com/ethereum/go-ethereum/blob/v1.10.1/core/types/receipt.go#L92-L97
#
# This value has been obtained by:
# 1. Inspecting Zeth transaction receipts, such as:
# {
# "blockHash":"0xa1ca015b7b7472f6a4a649890fb8d6cd7a85955e03e3d1b8603b2fa819c14071",
# "blockNumber":"0x56b73",
# "contractAddress":null,
# "cumulativeGasUsed":"0x1b2cd3",
# "from":"0xee0c66a2c570b0331c5bb1991124ed0529d11c4f",
# "gasUsed":"0x1b2cd3",
# "logs":[
# {
# "address":"0x26895344ba95f7a9762a5a4f871b5d5202115039",
# "topics":[
# "0x36ed7c3f2ecfb5a5226c478b034d33144c060afe361be291e948f861dcddc618"
# ],
# "data":"0x14ce028fa1e1df2d8c3b298a0659da99ae576eabd78e50607180755382193d2091f11ae060b100db666d01db0e8ab71b423192d9a1a7e75363af0ce5444ea1f96f8d3a4610c44a20545020204f0b19623b1abbb1784eb87c101ae398da0378351786c6efd30b72af6e1d7f875a8351872a4657dacb778b186d8a93e914df34490c3badefa35ee7b3a27f1b191c66bf9c066c8199452ed8f5c5f747f0997f627700000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000098ebf17ba5c8801a93c71f110a2a000f952fe24d018991295ae4c1b384b29dcd662cca2d57afc4c57dadcc1304122888ba13b6531cdc0afd4bb48bac8011d4c1c01bf04f27920e50b1792b6a6713644412c28b52b7f9142bd2d9dd59297fd7a546e4511b32f0d3eb3cbf19fd65374d0a55cd171b4b2d5b342802ab2788e91b5837087db3d0944f45ec011ba6b9723bbc24bf98f8ee1b732750000000000000000000000000000000000000000000000000000000000000000000000000000000981c1743cfcd668e4683946881b81ba9a69f79bcc87a5176df49c19aebfbf33d7d789be4f6de07bf66375aa208f5954a2cc41e0137e5a07239f97944e6f982493cb127c977091738c687532c7e3548394194ffa448b7e59b1222ae9d4bd9d969a6cf53b501be95300bb7b4a21bad83ffc33bc4140108d458f9b07847e7e7b3f38f27439322754d4a54e976549b40b8b10dab0a8d5ce89689050000000000000000",
# "blockNumber":"0x56b73",
# "transactionHash":"0xb4e683d7bbf4709fe7eb59fcd9041b1b90ab36266790a224d47ef894f0afa703",
# "transactionIndex":"0x0",
# "blockHash":"0xa1ca015b7b7472f6a4a649890fb8d6cd7a85955e03e3d1b8603b2fa819c14071",
# "logIndex":"0x0",
# "removed":false
# }
# ],
# "logsBloom":"0x00000000000000400000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000",
# "status":"0x1",
# "to":"0x26895344ba95f7a9762a5a4f871b5d5202115039",
# "transactionHash":"0xb4e683d7bbf4709fe7eb59fcd9041b1b90ab36266790a224d47ef894f0afa703",
# "transactionIndex":"0x0"
# }
#
# 2. Following the structure of the storage encoding of a receipt
# https://github.com/ethereum/go-ethereum/blob/v1.10.1/core/types/receipt.go#L92-L97
# and removing redundant fields from the JSON receipt, to obtain something like:
# ["0x1b2cd3", ["0x26895344ba95f7a9762a5a4f871b5d5202115039",["0x36ed7c3f2ecfb5a5226c478b034d33144c060afe361be291e948f861dcddc618"],"0x14ce028fa1e1df2d8c3b298a0659da99ae576eabd78e50607180755382193d2091f11ae060b100db666d01db0e8ab71b423192d9a1a7e75363af0ce5444ea1f96f8d3a4610c44a20545020204f0b19623b1abbb1784eb87c101ae398da0378351786c6efd30b72af6e1d7f875a8351872a4657dacb778b186d8a93e914df34490c3badefa35ee7b3a27f1b191c66bf9c066c8199452ed8f5c5f747f0997f627700000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000098ebf17ba5c8801a93c71f110a2a000f952fe24d018991295ae4c1b384b29dcd662cca2d57afc4c57dadcc1304122888ba13b6531cdc0afd4bb48bac8011d4c1c01bf04f27920e50b1792b6a6713644412c28b52b7f9142bd2d9dd59297fd7a546e4511b32f0d3eb3cbf19fd65374d0a55cd171b4b2d5b342802ab2788e91b5837087db3d0944f45ec011ba6b9723bbc24bf98f8ee1b732750000000000000000000000000000000000000000000000000000000000000000000000000000000981c1743cfcd668e4683946881b81ba9a69f79bcc87a5176df49c19aebfbf33d7d789be4f6de07bf66375aa208f5954a2cc41e0137e5a07239f97944e6f982493cb127c977091738c687532c7e3548394194ffa448b7e59b1222ae9d4bd9d969a6cf53b501be95300bb7b4a21bad83ffc33bc4140108d458f9b07847e7e7b3f38f27439322754d4a54e976549b40b8b10dab0a8d5ce89689050000000000000000","0x56b73","0xb4e683d7bbf4709fe7eb59fcd9041b1b90ab36266790a224d47ef894f0afa703","0x0","0xa1ca015b7b7472f6a4a649890fb8d6cd7a85955e03e3d1b8603b2fa819c14071","0x0","0x0"],"0x1"]
#
# 3. which can then be RLP encoded into something like:
# "0xf9030b831b2cd3f903039426895344ba95f7a9762a5a4f871b5d5202115039e1a036ed7c3f2ecfb5a5226c478b034d33144c060afe361be291e948f861dcddc618b9028014ce028fa1e1df2d8c3b298a0659da99ae576eabd78e50607180755382193d2091f11ae060b100db666d01db0e8ab71b423192d9a1a7e75363af0ce5444ea1f96f8d3a4610c44a20545020204f0b19623b1abbb1784eb87c101ae398da0378351786c6efd30b72af6e1d7f875a8351872a4657dacb778b186d8a93e914df34490c3badefa35ee7b3a27f1b191c66bf9c066c8199452ed8f5c5f747f0997f627700000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000098ebf17ba5c8801a93c71f110a2a000f952fe24d018991295ae4c1b384b29dcd662cca2d57afc4c57dadcc1304122888ba13b6531cdc0afd4bb48bac8011d4c1c01bf04f27920e50b1792b6a6713644412c28b52b7f9142bd2d9dd59297fd7a546e4511b32f0d3eb3cbf19fd65374d0a55cd171b4b2d5b342802ab2788e91b5837087db3d0944f45ec011ba6b9723bbc24bf98f8ee1b732750000000000000000000000000000000000000000000000000000000000000000000000000000000981c1743cfcd668e4683946881b81ba9a69f79bcc87a5176df49c19aebfbf33d7d789be4f6de07bf66375aa208f5954a2cc41e0137e5a07239f97944e6f982493cb127c977091738c687532c7e3548394194ffa448b7e59b1222ae9d4bd9d969a6cf53b501be95300bb7b4a21bad83ffc33bc4140108d458f9b07847e7e7b3f38f27439322754d4a54e976549b40b8b10dab0a8d5ce8968905000000000000000083056b73a0b4e683d7bbf4709fe7eb59fcd9041b1b90ab36266790a224d47ef894f0afa70300a0a1ca015b7b7472f6a4a649890fb8d6cd7a85955e03e3d1b8603b2fa819c14071000001"
#
# TODO: Add the receipt size to the metrics_df below (as it depends on the config, most notably on the JSOUT)
ZETH_RECEIPT_SIZE = 785
# - The documented sizes are the sizes (in bytes) of the Zeth RAW (RLP encoded) transaction objects.
metrics_df = pd.DataFrame(
[
["BN254", 32, 2, 2, 1335, 1315520],
["BLS12_377", 32, 2, 2, 1557, 1353261] # (approx. obtained as 3603/3090 * 1335)
# Switch JSOUT to 3 (e.g. to pay a Relay with an output note)
#["BN254", 32, 2, 3, XX, XX], # TODO
#["BLS12_377", 32, 2, 3, XX, XX] # TODO
],
columns=['curve', 'mk_depth', 'jsin', 'jsout', 'zeth_tx_size', 'zeth_tx_gcost']
)
def get_benchmark_data(curve, mk_depth, jsin, jsout):
"""
Function that queries the benchmark dataset to retrieve the data points
relevant to the given model configuration.
"""
result_df = metrics_df.query(
'curve == @curve and\
mk_depth == @mk_depth and\
jsin == @jsin and\
jsout == @jsout'
)
# Assert that the retrieved df has only 1 line
# (to avoid data inconsistency on duplicated lines)
assert result_df.shape[0] == 1, "[ERROR] Wrong number of data points in benchmark data"
zeth_tx_size = result_df.iloc[0]['zeth_tx_size']
zeth_tx_gcost = result_df.iloc[0]['zeth_tx_gcost']
return (zeth_tx_size, zeth_tx_gcost)
# Test the query function
size, gas = get_benchmark_data("BN254", 32, 2, 2)
print("Result size: {} and gas: {}".format(size, gas))
Result size: 1335 and gas: 1315520
###############################
# State variables #
###############################
# Genesis state: the same is used for both A/B testing simulations
genesis_state = {
# All sets are initialized with the empty set (they have no elements)
'MKLS_cardinality': 0,
'NS_cardinality': 0,
'RS_cardinality': 0,
# The size of the blockchain is assumed to be 0 at starting time
'B_size': 0,
'B_txcount': 0
}
The set of parameters (of interest) used for the A/B(/C) testing and "Parameter Sweep" simulation of Zeth is defined below.
##############################
# Simulation configuration #
##############################
# Array of params used during the "Parameter Sweeps" Simulation
# to simulate under different blockchain configuration assumptions.
BLOCKCHAIN_PARAMS = [
# Arbitrary set of blockchain config params
# (from big blocks mined "slowly" to smaller blocks mined "frequently")
{ 'bglim': 25000000, 'btimetrgt': 15 },
{ 'bglim': 12500000, 'btimetrgt': 5 },
{ 'bglim': 5000000, 'btimetrgt': 1 },
# Ethereum mainnet median data
{ 'bglim': MAINNET_MEDIAN_BLOCKGASLIMIT, 'btimetrgt': MAINNET_MEDIAN_BLOCKTIME }
]
# Simulate a system where only plain "EoA-to-EoA" transactions are
# carried out (no smart contract deployed)
system_params_A = {
'chain' : BLOCKCHAIN_PARAMS,
}
# Below, we simulate different Zeth configurations (using different curves)
# on different blockchain configurations.
system_params_B = {
'chain' : BLOCKCHAIN_PARAMS,
'zeth' : [
# Test all the blockchain configs with this Zeth config
{ 'curve': "BN254", 'mkdepth': 32, 'jsin': 2, 'jsout': 2 },
]
}
system_params_C = {
'chain' : BLOCKCHAIN_PARAMS,
'zeth' : [
# Test all the blockchain configs with this Zeth config
{ 'curve': "BLS12_377", 'mkdepth': 32, 'jsin': 2, 'jsout': 2 },
]
}
#############################
# Policy functions #
#############################
#
# Manage time on the system
#
# We need to simulate the various block time targets
# If btimetrgt = 1, a block is added a each time step
# If btimetrgt = 5, a block is added every 5 time steps
# If btimetrgt = 15, a block is added every 15 time steps
def p_is_add_block(params, substep, state_history, previous_state):
"""
Function that determines whether we need to mine a block at this time step or not
"""
# At t = 0, this condition will be true for all block intervals
# hence, producing the genesis block
if previous_state['timestep'] % params['chain']['btimetrgt'] == 0:
return ({'add_block': True})
return ({'add_block': False})
# Computes the block size (bytes) for Ethereum (EoA-to-EoA) only txs
def p_get_ethereum_block_size_bytes(params, substep, state_history, previous_state):
nb_txs = params['chain']['bglim'] // DGAS
# Account for the snappy compression in the state DB
block_size = (nb_txs * ETHTXSIZE + BLOCKHEADERSIZE + ETH_RECEIPT_SIZE) / COMPRESSION_RATIO
return ({'block_size': block_size, 'number_txs': nb_txs})
# Computes the block size (bytes) for Zeth only txs
# - Returns the block size and the number of txs in the block
def p_get_zeth_block_size_bytes(params, substep, state_history, previous_state):
# Retrieve Zeth benchmark data
curve = params['zeth']['curve']
mkdepth = params['zeth']['mkdepth']
jsin = params['zeth']['jsin']
jsout = params['zeth']['jsout']
zeth_tx_size, zeth_tx_gas = get_benchmark_data(curve, mkdepth, jsin, jsout)
nb_txs = params['chain']['bglim'] // zeth_tx_gas
# Account for the snappy compression in the state DB
block_size = (nb_txs * zeth_tx_size + BLOCKHEADERSIZE + ZETH_RECEIPT_SIZE) / COMPRESSION_RATIO
return ({'block_size': block_size, 'number_txs': nb_txs})
#################################
# Simple state update functions #
#################################
#
# - `params`: Python dictionary containing the system parameters
# - `substep`: Integer value representing a step within a single timestep
# - `state_history`: Python list of all previous states
# - `previous_state`: Python dictionary that defines what the state of the system was at the previous timestep or substep
# - `policy_input`: Python dictionary of signals or actions from policy functions
def s_update_B_size(params, substep, state_history, previous_state, policy_input):
"""
Update the size of the blockchain B
"""
new_value = previous_state['B_size'] + policy_input['add_block'] * policy_input['block_size']
return 'B_size', new_value
def s_update_B_txcount(params, substep, state_history, previous_state, policy_input):
"""
Update the size of the blockchain B
"""
new_value = previous_state['B_txcount'] + policy_input['add_block'] * policy_input['number_txs']
return 'B_txcount', new_value
# None are specific to simulation A
def s_update_MKLS_cardinality(params, substep, state_history, previous_state, policy_input):
"""
Update the cardinality of the set MKLS
"""
new_value = previous_state['MKLS_cardinality'] + policy_input['add_block'] * policy_input['number_txs'] * params['zeth']['jsout']
return 'MKLS_cardinality', new_value
def s_update_NS_cardinality(params, substep, state_history, previous_state, policy_input):
"""
Update the cardinality of the set NS
"""
new_value = previous_state['NS_cardinality'] + policy_input['add_block'] * policy_input['number_txs'] * params['zeth']['jsin']
return 'NS_cardinality', new_value
def s_update_RS_cardinality(params, substep, state_history, previous_state, policy_input):
"""
Update the cardinality of the set RS
"""
new_value = previous_state['RS_cardinality'] + policy_input['add_block'] * policy_input['number_txs'] * params['zeth']['jsin']
return 'RS_cardinality', new_value
partial_state_update_blocks_A = [
{
'policies': {
'is_add_block': p_is_add_block,
'get_ethereum_block_size_bytes': p_get_ethereum_block_size_bytes
},
# Update all these variables in parallel
'variables': {
'B_size': s_update_B_size,
'B_txcount': s_update_B_txcount
}
}
]
# Partial State Update Block shared by both simulation B and simulation C
partial_state_update_blocks_B_and_C = [
{
'policies': {
'is_add_block': p_is_add_block,
'get_zeth_block_size_bytes': p_get_zeth_block_size_bytes
},
# Update all these variables in parallel
'variables': {
'MKLS_cardinality': s_update_MKLS_cardinality,
'NS_cardinality': s_update_NS_cardinality,
'RS_cardinality': s_update_RS_cardinality,
'B_size': s_update_B_size,
'B_txcount': s_update_B_txcount
}
}
]
# For multiple MC runs, we can use the state['run'] variable that allows to get the run number
MONTE_CARLO_RUNS = 1
SIMULATION_TIMESTEPS = 86400 # Number of seconds in a day: 60*60*24
sim_config_A = config_sim({
'N': MONTE_CARLO_RUNS,
'T': range(SIMULATION_TIMESTEPS),
'M': system_params_A
})
sim_config_B = config_sim({
'N': MONTE_CARLO_RUNS,
'T': range(SIMULATION_TIMESTEPS),
'M': system_params_B
})
sim_config_C = config_sim({
'N': MONTE_CARLO_RUNS,
'T': range(SIMULATION_TIMESTEPS),
'M': system_params_C
})
# Print the configuration structure for the param sweep simulation
def print_config_and_system_params(sim_config, system_params):
print('sim_config: ')
pprint(sim_config)
print(' ')
print('system_params: ')
pprint(system_params_A)
print(' === Simulation A: ===\n')
print_config_and_system_params(sim_config_A, system_params_A)
print('\n=== Simulation B: ===\n')
print_config_and_system_params(sim_config_B, system_params_B)
print('\n=== Simulation C: ===\n')
print_config_and_system_params(sim_config_C, system_params_C)
=== Simulation A: === sim_config: [{'M': {'chain': {'bglim': 25000000, 'btimetrgt': 15}}, 'N': 1, 'T': range(0, 86400)}, {'M': {'chain': {'bglim': 12500000, 'btimetrgt': 5}}, 'N': 1, 'T': range(0, 86400)}, {'M': {'chain': {'bglim': 5000000, 'btimetrgt': 1}}, 'N': 1, 'T': range(0, 86400)}, {'M': {'chain': {'bglim': 7996822, 'btimetrgt': 15}}, 'N': 1, 'T': range(0, 86400)}] system_params: {'chain': [{'bglim': 25000000, 'btimetrgt': 15}, {'bglim': 12500000, 'btimetrgt': 5}, {'bglim': 5000000, 'btimetrgt': 1}, {'bglim': 7996822, 'btimetrgt': 15}]} === Simulation B: === sim_config: [{'M': {'chain': {'bglim': 25000000, 'btimetrgt': 15}, 'zeth': {'curve': 'BN254', 'jsin': 2, 'jsout': 2, 'mkdepth': 32}}, 'N': 1, 'T': range(0, 86400)}, {'M': {'chain': {'bglim': 12500000, 'btimetrgt': 5}, 'zeth': {'curve': 'BN254', 'jsin': 2, 'jsout': 2, 'mkdepth': 32}}, 'N': 1, 'T': range(0, 86400)}, {'M': {'chain': {'bglim': 5000000, 'btimetrgt': 1}, 'zeth': {'curve': 'BN254', 'jsin': 2, 'jsout': 2, 'mkdepth': 32}}, 'N': 1, 'T': range(0, 86400)}, {'M': {'chain': {'bglim': 7996822, 'btimetrgt': 15}, 'zeth': {'curve': 'BN254', 'jsin': 2, 'jsout': 2, 'mkdepth': 32}}, 'N': 1, 'T': range(0, 86400)}] system_params: {'chain': [{'bglim': 25000000, 'btimetrgt': 15}, {'bglim': 12500000, 'btimetrgt': 5}, {'bglim': 5000000, 'btimetrgt': 1}, {'bglim': 7996822, 'btimetrgt': 15}]} === Simulation C: === sim_config: [{'M': {'chain': {'bglim': 25000000, 'btimetrgt': 15}, 'zeth': {'curve': 'BLS12_377', 'jsin': 2, 'jsout': 2, 'mkdepth': 32}}, 'N': 1, 'T': range(0, 86400)}, {'M': {'chain': {'bglim': 12500000, 'btimetrgt': 5}, 'zeth': {'curve': 'BLS12_377', 'jsin': 2, 'jsout': 2, 'mkdepth': 32}}, 'N': 1, 'T': range(0, 86400)}, {'M': {'chain': {'bglim': 5000000, 'btimetrgt': 1}, 'zeth': {'curve': 'BLS12_377', 'jsin': 2, 'jsout': 2, 'mkdepth': 32}}, 'N': 1, 'T': range(0, 86400)}, {'M': {'chain': {'bglim': 7996822, 'btimetrgt': 15}, 'zeth': {'curve': 'BLS12_377', 'jsin': 2, 'jsout': 2, 'mkdepth': 32}}, 'N': 1, 'T': range(0, 86400)}] system_params: {'chain': [{'bglim': 25000000, 'btimetrgt': 15}, {'bglim': 12500000, 'btimetrgt': 5}, {'bglim': 5000000, 'btimetrgt': 1}, {'bglim': 7996822, 'btimetrgt': 15}]}
# Carry out the simulations
# Clear any prior configs
del configs[:]
# Create new experiment
experiment = Experiment()
# Append Simulation A config (only EoA-to-EoA transactions hit the chain)
experiment.append_configs(
initial_state = genesis_state,
partial_state_update_blocks = partial_state_update_blocks_A,
sim_configs = sim_config_A
)
# Append Simulation B config (only Zeth transactions hit the chain: BN254, MK depth 32, JSIN=JSOUT=2)
experiment.append_configs(
initial_state = genesis_state,
partial_state_update_blocks = partial_state_update_blocks_B_and_C,
sim_configs = sim_config_B
)
# Append Simulation C config (only Zeth transactions hit the chain: BLS12_377, MK depth 32, JSIN=JSOUT=2)
experiment.append_configs(
initial_state = genesis_state,
partial_state_update_blocks = partial_state_update_blocks_B_and_C,
sim_configs = sim_config_C
)
# Get relation between the:
# - Simulation ID
# - Subset ID
# - Run ID
# And the actual user-defined configurations.
# This is particularly useful to better understand the simulation results in the next section of the notebook.
for i in range(len(configs)):
#pprint(configs[i].__dict__)
print(configs[i].sim_config)
{'N': 1, 'T': range(0, 86400), 'M': {'chain': {'bglim': 25000000, 'btimetrgt': 15}}, 'subset_id': 0, 'subset_window': deque([0, None], maxlen=2), 'simulation_id': 0, 'run_id': 0} {'N': 2, 'T': range(0, 86400), 'M': {'chain': {'bglim': 12500000, 'btimetrgt': 5}}, 'subset_id': 1, 'subset_window': deque([0, None], maxlen=2), 'simulation_id': 0, 'run_id': 1} {'N': 3, 'T': range(0, 86400), 'M': {'chain': {'bglim': 5000000, 'btimetrgt': 1}}, 'subset_id': 2, 'subset_window': deque([0, None], maxlen=2), 'simulation_id': 0, 'run_id': 2} {'N': 4, 'T': range(0, 86400), 'M': {'chain': {'bglim': 7996822, 'btimetrgt': 15}}, 'subset_id': 3, 'subset_window': deque([0, None], maxlen=2), 'simulation_id': 0, 'run_id': 3} {'N': 1, 'T': range(0, 86400), 'M': {'chain': {'bglim': 25000000, 'btimetrgt': 15}, 'zeth': {'curve': 'BN254', 'mkdepth': 32, 'jsin': 2, 'jsout': 2}}, 'subset_id': 0, 'subset_window': deque([0, None], maxlen=2), 'simulation_id': 1, 'run_id': 0} {'N': 2, 'T': range(0, 86400), 'M': {'chain': {'bglim': 12500000, 'btimetrgt': 5}, 'zeth': {'curve': 'BN254', 'mkdepth': 32, 'jsin': 2, 'jsout': 2}}, 'subset_id': 1, 'subset_window': deque([0, None], maxlen=2), 'simulation_id': 1, 'run_id': 1} {'N': 3, 'T': range(0, 86400), 'M': {'chain': {'bglim': 5000000, 'btimetrgt': 1}, 'zeth': {'curve': 'BN254', 'mkdepth': 32, 'jsin': 2, 'jsout': 2}}, 'subset_id': 2, 'subset_window': deque([0, None], maxlen=2), 'simulation_id': 1, 'run_id': 2} {'N': 4, 'T': range(0, 86400), 'M': {'chain': {'bglim': 7996822, 'btimetrgt': 15}, 'zeth': {'curve': 'BN254', 'mkdepth': 32, 'jsin': 2, 'jsout': 2}}, 'subset_id': 3, 'subset_window': deque([0, None], maxlen=2), 'simulation_id': 1, 'run_id': 3} {'N': 1, 'T': range(0, 86400), 'M': {'chain': {'bglim': 25000000, 'btimetrgt': 15}, 'zeth': {'curve': 'BLS12_377', 'mkdepth': 32, 'jsin': 2, 'jsout': 2}}, 'subset_id': 0, 'subset_window': deque([0, None], maxlen=2), 'simulation_id': 2, 'run_id': 0} {'N': 2, 'T': range(0, 86400), 'M': {'chain': {'bglim': 12500000, 'btimetrgt': 5}, 'zeth': {'curve': 'BLS12_377', 'mkdepth': 32, 'jsin': 2, 'jsout': 2}}, 'subset_id': 1, 'subset_window': deque([0, None], maxlen=2), 'simulation_id': 2, 'run_id': 1} {'N': 3, 'T': range(0, 86400), 'M': {'chain': {'bglim': 5000000, 'btimetrgt': 1}, 'zeth': {'curve': 'BLS12_377', 'mkdepth': 32, 'jsin': 2, 'jsout': 2}}, 'subset_id': 2, 'subset_window': deque([0, None], maxlen=2), 'simulation_id': 2, 'run_id': 2} {'N': 4, 'T': range(0, 86400), 'M': {'chain': {'bglim': 7996822, 'btimetrgt': 15}, 'zeth': {'curve': 'BLS12_377', 'mkdepth': 32, 'jsin': 2, 'jsout': 2}}, 'subset_id': 3, 'subset_window': deque([0, None], maxlen=2), 'simulation_id': 2, 'run_id': 3}
# Hack to use the cached simulation results by default
CACHED_SIMULATION = True
If you have already carried out the simulation, cached its results, and simply want to plot the simulation results, please jump to this step (and do not execute the boxes below).
# If this box is executed, then we run the full simulations
# and thus won't be plotting from the cached results
CACHED_SIMULATION = False
# Simulation A
exec_mode = ExecutionMode()
exec_context = ExecutionContext(context=exec_mode.multi_mode)
simulation = Executor(exec_context=exec_context, configs=configs)
raw_result, tensor_field, sessions = simulation.execute()
___________ ____ ________ __ ___/ / ____/ | / __ \ / ___/ __` / __ / / / /| | / / / / / /__/ /_/ / /_/ / /___/ ___ |/ /_/ / \___/\__,_/\__,_/\____/_/ |_/_____/ by cadCAD Execution Mode: multi_proc Configuration Count: 3 Dimensions of the first simulation: (Timesteps, Params, Runs, Vars) = (86400, 1, 4, 5) Execution Method: parallelize_simulations Execution Mode: parallelized Total execution time: 2405.26s
simulation_result = pd.DataFrame(raw_result)
simulation_result
MKLS_cardinality | NS_cardinality | RS_cardinality | B_size | B_txcount | simulation | subset | run | substep | timestep | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0.000000e+00 | 0 | 0 | 0 | 1 | 0 | 0 |
1 | 0 | 0 | 0 | 8.840667e+04 | 1190 | 0 | 0 | 1 | 1 | 1 |
2 | 0 | 0 | 0 | 8.840667e+04 | 1190 | 0 | 0 | 1 | 1 | 2 |
3 | 0 | 0 | 0 | 8.840667e+04 | 1190 | 0 | 0 | 1 | 1 | 3 |
4 | 0 | 0 | 0 | 8.840667e+04 | 1190 | 0 | 0 | 1 | 1 | 4 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
1036807 | 57600 | 57600 | 57600 | 3.485952e+07 | 28800 | 2 | 3 | 4 | 1 | 86396 |
1036808 | 57600 | 57600 | 57600 | 3.485952e+07 | 28800 | 2 | 3 | 4 | 1 | 86397 |
1036809 | 57600 | 57600 | 57600 | 3.485952e+07 | 28800 | 2 | 3 | 4 | 1 | 86398 |
1036810 | 57600 | 57600 | 57600 | 3.485952e+07 | 28800 | 2 | 3 | 4 | 1 | 86399 |
1036811 | 57600 | 57600 | 57600 | 3.485952e+07 | 28800 | 2 | 3 | 4 | 1 | 86400 |
1036812 rows × 10 columns
#from tabulate import tabulate
#print(tabulate(simulation_result, headers='keys', tablefmt='psql'))
# Cache the simulation results
compression_opts = dict(method='zip', archive_name='simulation_result.csv')
simulation_result.to_csv('simulation_result.zip', index=False, compression=compression_opts)
If you wish to plot the results of a past simulation (without re-running the full set of simulations above), please start here.
# If we use cached simulation results
if CACHED_SIMULATION:
print("Loading cached simulation results...")
compression_opts = dict(method='zip', archive_name='simulation_result.csv')
simulation_result = pd.read_csv('simulation_result.zip', compression=compression_opts)
print("Loading completed!")
# Copy the simulation data in a new data frame
df = simulation_result.copy()
df_simulation_sweep_A = df[df['simulation'] == 0]
df_simulation_sweep_B = df[df['simulation'] == 1]
df_simulation_sweep_C = df[df['simulation'] == 2]
df.head(5)
MKLS_cardinality | NS_cardinality | RS_cardinality | B_size | B_txcount | simulation | subset | run | substep | timestep | |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0.000000 | 0 | 0 | 0 | 1 | 0 | 0 |
1 | 0 | 0 | 0 | 88406.666667 | 1190 | 0 | 0 | 1 | 1 | 1 |
2 | 0 | 0 | 0 | 88406.666667 | 1190 | 0 | 0 | 1 | 1 | 2 |
3 | 0 | 0 | 0 | 88406.666667 | 1190 | 0 | 0 | 1 | 1 | 3 |
4 | 0 | 0 | 0 | 88406.666667 | 1190 | 0 | 0 | 1 | 1 | 4 |
import plotly.express as px
# Multiple plots for each `subset` (i.e. for each system configuration during the "Param Sweep" simulation)
px.line(
df,
x='timestep',
y=['B_size'],
facet_row='subset', # Each row is a blockchain config (and zeth config if applicable)
facet_col='simulation',# Columns = Zeth txs (simulation 0), EoA-to-EoA (simulation 1)
color='subset',
title='Growth of the Blockchain state under various protocol configurations (Facets view)',
labels=dict(timestep="Timesteps (sec)", value="Chain size (bytes)", subset="Configuration")
)