Crates.io | commonware-estimator |
lib.rs | commonware-estimator |
version | 0.0.62 |
created_at | 2025-07-17 14:34:05.511969+00 |
updated_at | 2025-09-10 18:52:21.836449+00 |
description | Simulate mechanism performance under realistic network conditions. |
homepage | https://commonware.xyz |
repository | https://github.com/commonwarexyz/monorepo/tree/main/examples/estimator |
max_upload_size | |
id | 1757585 |
size | 272,833 |
Simulate mechanism performance under realistic network conditions.
commonware-estimator
shortens the time from idea to data when experimenting with new mechanisms. With a basic DSL, you can simulate the performance of arbitrary mechanisms under realistic network conditions (AWS region-to-region latencies from cloudping.co).
Key features:
With built-in handling for message passing (proposes, broadcasts, replies), waiting/collecting thresholds, and compound conditional expressions, it's suitable for testing new consensus algorithms or other broadcast-based protocols.
Build and run the simulator using Cargo:
cargo run -- [OPTIONS]
<TASK>
(required): Path to the .lazy file defining the simulation behavior (e.g., minimmit.lazy
).
--distribution <DISTRIBUTION>
(required): Specify the distribution of peers across regions with optional bandwidth limits:
<region>:<count>
(unlimited bandwidth)<region>:<count>:<egress>/<ingress>
(asymmetric bandwidth)<region>:<count>:<bandwidth>
(symmetric bandwidth)Bandwidth is in bytes per second. Examples:
us-east-1:10,eu-west-1:5
(no bandwidth limits)us-east-1:3:1000/500,eu-west-1:2:2000
(with bandwidth limits)Regions must match AWS regions from the latency data (e.g., us-east-1, eu-west-1).
--reload
(optional flag): Download fresh latency data from cloudping.co instead of using embedded data.
# Basic usage without bandwidth limits
cargo run -- hotstuff.lazy --distribution us-east-1:3,eu-west-1:2
# With bandwidth limits (asymmetric: 1000 B/s egress, 500 B/s ingress for us-east-1; symmetric: 2000 B/s for eu-west-1)
cargo run -- hotstuff.lazy --distribution us-east-1:3:1000/500,eu-west-1:2:2000
# With message sizes for more realistic bandwidth simulation
cargo run -- simplex_with_sizes.lazy --distribution us-east-1:3:1000,eu-west-1:2:2000
The first example runs simulations with 5 peers (3 in us-east-1, 2 in eu-west-1) without bandwidth constraints. The second adds bandwidth limits, and the third uses message sizes for more realistic simulations.
For each possible proposer (peer index), the simulator prints:
wait
and collect
commands.collect
).wait
).Finally, it prints averaged results across all simulations.
# HotStuff
## Send PREPARE
propose{0}
## Reply to proposer PREPARE with VOTE(PREPARE)
wait{0, threshold=1, delay=(0.1,1)}
[proposer] mean: 0.00ms (stdv: 0.00ms) | median: 0.00ms
[eu-west-1] mean: 20.90ms (stdv: 16.69ms) | median: 33.00ms
[us-east-1] mean: 15.27ms (stdv: 15.56ms) | median: 5.00ms
[all] mean: 17.52ms (stdv: 16.26ms) | median: 5.00ms
reply{1}
## Collect VOTE(PREPARE) from 67% of the network and then broadcast (PRECOMMIT, QC_PREPARE)
collect{1, threshold=67%, delay=(0.1,1)}
[proposer] mean: 69.80ms (stdv: 1.83ms) | median: 69.00ms
propose{1}
## Reply to proposer (PRECOMMIT, QC_PREPARE) with VOTE(PRECOMMIT)
wait{1, threshold=1, delay=(0.1,1)}
[proposer] mean: 70.80ms (stdv: 1.83ms) | median: 70.00ms
[eu-west-1] mean: 91.80ms (stdv: 16.23ms) | median: 101.00ms
[us-east-1] mean: 85.40ms (stdv: 16.30ms) | median: 76.00ms
[all] mean: 87.96ms (stdv: 16.57ms) | median: 77.00ms
reply{2}
## Collect VOTE(PRECOMMIT) from 67% of the network and then broadcast (COMMIT, QC_PRECOMMIT)
collect{2, threshold=67%, delay=(0.1,1)}
[proposer] mean: 139.60ms (stdv: 2.87ms) | median: 140.00ms
propose{3}
## Wait for proposer (COMMIT, QC_PRECOMMIT)
wait{3, threshold=1, delay=(0.1,1)}
[proposer] mean: 140.60ms (stdv: 2.87ms) | median: 141.00ms
[eu-west-1] mean: 161.20ms (stdv: 16.59ms) | median: 170.00ms
[us-east-1] mean: 155.67ms (stdv: 16.09ms) | median: 146.00ms
[all] mean: 157.88ms (stdv: 16.52ms) | median: 150.00ms
The DSL is a plain text file where each non-empty line represents a command or compound expression. Commands are executed sequentially by each simulated peer, but blocking commands (wait
and collect
) pause until their conditions are met. Compound expressions using logical operators are evaluated based on the current state of each peer. Empty lines are ignored.
key=value
pairs, separated by commas.5
) or percentages (e.g., 80%
). Percentages are relative to the total number of peers.delay=(<message_delay>,<completion_delay>)
, where delays are floats in milliseconds (e.g., (0.1,1)
). The message delay is incurred for each processed message and completion delay is incurred once after the threshold is met.id
(u32) for tracking messages.propose
or collect
).wait
/collect
.&&
) has higher precedence than OR (||
). Use parentheses to override.wait
or collect
has a per-message delay and it is used in an AND or OR expression, the delay (in milliseconds) is applied for each check on an incoming message (i.e. the delay is additive).All message commands (propose
, broadcast
, reply
) support an optional size
parameter to specify message size in bytes. This allows for more realistic simulations when combined with bandwidth limits:
size
parameter, messages are 4 bytes (just the message ID)Example with different message sizes:
# 1KB block proposal
propose{0, size=1024}
# Small 64-byte votes
reply{1, size=64}
# Medium 200-byte certificates
broadcast{2, size=200}
propose{
id
: Unique message identifier (u32).size
(optional): Message size in bytes. If not specified, defaults to 4 bytes (just the ID).propose{0}
(4-byte message)propose{0, size=1024}
(1KB message)broadcast{
id
: Unique message identifier (u32).size
(optional): Message size in bytes. If not specified, defaults to 4 bytes (just the ID).broadcast{1}
(4-byte message)broadcast{1, size=100}
(100-byte message)reply{
id
: Unique message identifier (u32).size
(optional): Message size in bytes. If not specified, defaults to 4 bytes (just the ID).reply{2}
(4-byte message)reply{2, size=64}
(64-byte message)collect{
id
: Message ID to collect.threshold
: Count (e.g., 5
) or percentage (e.g., 80%
).delay
(optional): Sleeps msg_delay
milliseconds before checking, and comp_delay
milliseconds after threshold met.collect{1, threshold=80%, delay=(0.1,1)}
wait{
collect
.wait{0, threshold=40%}
The DSL supports complex conditional logic using AND (&&
) and OR (||
) operators, along with parentheses for grouping.
&&
)command1 && command2
wait{1, threshold=1} && wait{2, threshold=1}
||
)command1 || command2
wait{1, threshold=67%} || wait{2, threshold=1}
&&
) has higher precedence than OR (||
)wait{1, threshold=1} || wait{2, threshold=1} && wait{3, threshold=1}
wait{1, threshold=1} || (wait{2, threshold=1} && wait{3, threshold=1})
(wait{1, threshold=1} || wait{2, threshold=1}) && wait{3, threshold=1}
To simulate the performance of HotStuff, Simplicity, and Minimmit on an Alto-like Network, run the following commands:
# Basic simulation without bandwidth constraints
cargo run -- --distribution us-west-1:5,us-east-1:5,eu-west-1:5,ap-northeast-1:5,eu-north-1:5,ap-south-1:5,sa-east-1:5,eu-central-1:5,ap-northeast-2:5,ap-southeast-2:5 hotstuff.lazy
# With realistic bandwidth limits (1 Gbps symmetric)
cargo run -- --distribution us-west-1:5:125000000,us-east-1:5:125000000,eu-west-1:5:125000000,ap-northeast-1:5:125000000,eu-north-1:5:125000000,ap-south-1:5:125000000,sa-east-1:5:125000000,eu-central-1:5:125000000,ap-northeast-2:5:125000000,ap-southeast-2:5:125000000 simplex_with_sizes.lazy
# With asymmetric bandwidth (varying by region to simulate different network conditions)
cargo run -- --distribution us-west-1:5:200000000/100000000,us-east-1:5:200000000/100000000,eu-west-1:5:150000000/75000000,ap-northeast-1:5:100000000/50000000 minimmit.lazy