Skip to content

Latest commit

 

History

9 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Consensus

CI C++17 tests simulations licence

A C++17 simulator for gossip consensus over a trust graph.

Nodes follow each other in a directed graph, gossip transactions for a fixed number of synchronous rounds, and end holding whatever they have accepted. Agreement is the fraction of honest nodes that end on an identical set. Some of the nodes are adversaries.

The engine is configurable across node count, graph shape, connection density, adversary share and behaviour, packet loss, transaction spread, rounds and seed. Node strategies and adversaries are pluggable, by name, either as a C++ file dropped into plugins/ or as JavaScript written in the browser. The same sources compile to a native binary, a test suite, a batch experiment pipeline and a WebAssembly front end, so the interactive page and the measured results cannot disagree.

What it measures

Four strategies against five adversaries, over the full parameter grid, twenty repetitions per cell.

strategy dead spam selfish late dump chatty dump
flood, believe everything 0.999 0.999 0.999 0.372 0.437
blacklist, write off silent followees 0.999 0.999 0.999 0.999 0.455
corroborate, k = 2 independent proposers 0.385 0.386 0.569 0.160 0.209
quarantine, blacklist + deadline rule 0.999 0.999 0.999 0.999 0.999

Agreement among honest nodes, mean of 540 runs per cell. Read the first column first: an adversary that never transmits is not an attack, and every strategy that scores well only there has been measured against nothing.

defence matrix

What the network looks like

Eight graph shapes, all generated by the engine itself and drawn straight from its output. Blue circles are honest nodes, red squares adversaries.

topologies

The same attack, run against two strategies, one round at a time. Node area is how much of the knowable transaction set that node holds; a dashed ring at the end marks an honest node that finished outside the majority.

a run, round by round

Try it

web/ is an interactive front end: set the parameters, watch the run, edit the graph by hand and run it again.

make wasm && python3 -m http.server -d web 8080

Then open localhost:8080. make wasm-single instead produces results/consensus.html, the same app folded into a single file with the WebAssembly inlined, for when only one file can be handed over.

Adjustable: node count, graph shape, connection density, packet loss, adversary share and behaviour, transaction count and how widely transactions start out, rounds and seed. Honest nodes can be a mixed population, for example 60% flooding and 40% quarantine, and the panel reports which population ended up on the majority answer. The graph itself is editable: drag a node, link one node to another, remove a node, cut an edge, right-click to change what a node runs, or upload a graph of your own, and the run repeats on exactly that graph.

Write your own strategy in the page. The editor takes JavaScript and runs it as part of the simulation, as an honest node or as an adversary. You can type it, upload a .js file, or drop one on the editor, and download it again. You implement one decision function and the engine supplies the bookkeeping: who has been silent, how many distinct followees have proposed a transaction, how many rounds remain. The quarantine rule written that way scores the same as the compiled C++ version on the same graph. A browser has no C++ compiler and there is no server behind this, so a .cpp upload cannot run on the page; C++ stays the path for anything that goes into the measured results, through plugins/.

The engine is this project's C++ compiled to WebAssembly, not a JavaScript rewrite, so the page cannot drift away from the measured results. tools/wasm_parity.py runs configurations through both the browser engine and the native binary and requires every field to match exactly; CI enforces it on every push.

Hosting. .github/workflows/pages.yml publishes it to GitHub Pages.

Results

  • results/summary.md is the numeric record: every claim as a number with a confidence interval.
  • docs/findings.md is the full analysis, with each number traced to the program that produces it.
  • results/report.html is the same analysis as a single page with the figures embedded.
  • results/network.html replays recorded runs as an animated network.
  • docs/design.md records the design decisions and what was done to check the engine is correct.

Two results worth pulling out, because both run against intuition:

  • A denser graph makes an undefended network worse, not better. More links spread honest transactions further, and they spread an adversary's dump further too, and the second effect wins.
  • Corroboration is a trap. Requiring two independent proposers before accepting a transaction scores 0.385 with no effective attacker present. The rule cannot bootstrap: early on nothing has two proposers yet, so nothing is accepted, so nothing ever gets a second one.

A one-factor-at-a-time study over seven parameters, 12,000 runs, locates where each strategy stops working rather than scoring it at a single point. It adds three findings: quarantine needs at least six rounds to be worth anything, at 70% packet loss plain flooding beats both defences and the ranking reverses outright, and a ring holds every strategy near 0.10 for reasons that are structural rather than adversarial.

Build and run

Needs a C++17 compiler and OpenSSL 3. Nothing else for the library, the tests or the experiments; only the figures need Python.

make test          # 68 tests across the ledger, the chain and consensus
make run           # the two demo programs
make experiments   # every experiment, writing results/data/*.csv
make figures       # 20 figures plus results/summary.md   (needs Python)
make viz           # results/network.html, the network replay (needs Python)
make wasm          # web/engine.wasm, the browser engine (needs Emscripten)
make viz-custom    # the same page, built from your own settings; see below
make all-in-one    # all of the above, about 40 s from clean

For the figures:

python3 -m venv venv && ./venv/bin/pip install -r tools/requirements.txt
make figures PYTHON=./venv/bin/python

OpenSSL is found via Homebrew, then pkg-config, then the system default. Override with make OPENSSL_DIR=/path/to/openssl.

Testing your own strategy

Strategies and adversaries are looked up by name, and anything linked into the binary can register one. Dropping a file into plugins/ is enough: the build compiles it, and the name becomes available to every experiment, to the scorer and to the replay page with nothing else edited.

cp plugins/example_quorum.cpp plugins/my_idea.cpp
$EDITOR plugins/my_idea.cpp
make
./build/strategy_check --list

Score it against every adversary, over the full parameter grid with repetitions, next to a baseline on identical seeds:

./build/strategy_check --strategy my_idea --compare flood
adversary      consensus              coverage               distinct   ms/run
dead           0.999 +/- 0.000        0.919 +/- 0.004             1.1     1.63
late_dump      0.372 +/- 0.021        0.940 +/- 0.003            33.0     1.72
chatty_dump    0.437 +/- 0.022        0.957 +/- 0.002            29.0     1.28

Then watch it run:

make viz-custom TRACE='--scenario my_idea:chatty_dump --scenario flood:chatty_dump'

plugins/README.md has the interface contract and the rules the harness enforces. The worked example in plugins/example_quorum.cpp is a strategy that sounds reasonable and measurably fails, which is the more useful thing to show: read the benign column first, because a defence that scores badly against an adversary doing nothing at all has broken the network rather than defended it.

The network replay

make viz builds a self-contained page that replays recorded runs. Each dot travelling an edge is one node broadcasting to a follower: green carries a transaction the receiver did not have, grey is redundant, amber was ignored because the receiver had written that sender off. Node size is how much of the knowable transaction set the node holds, and at the end every honest node that disagrees with the majority carries a dashed ring. Five scenarios share one random graph and one role assignment, so only the strategy and the adversary change between them. The page also shows the current state of the test suite.

Those scenarios use 40 nodes rather than 100, with parameters chosen so the mechanism is visible at that size. They illustrate a mechanism, one run each. They are not the evidence; the measured results are in results/summary.md.

Running it with your own settings

make viz-custom rebuilds the same page from whatever you pass in TRACE:

make viz-custom TRACE='--nodes 25 --p-mal 0.45 --rounds 10 --scenario flood:chatty_dump --scenario quarantine:chatty_dump'

Then open results/network.html. Every scenario in one build shares the same graph and the same role assignment, so only the strategy and the adversary differ between them and the comparison is like for like.

option meaning default
--nodes N nodes in the graph 40
--tx N transactions in circulation 80
--p-graph F chance a given follow edge exists 0.15
--p-mal F chance a node is an adversary 0.30
--p-tx F chance a node starts holding a given transaction 0.05
--rounds N rounds of gossip 12
--seed N random seed 20260824
--scenario S:A strategy and adversary, repeatable the five presets

Strategies are flood, blacklist, corroborate, quarantine. Adversaries are dead, selfish, spam, late_dump, chatty_dump. Run ./build/export_trace --help for the same list.

Two things to keep in mind when choosing settings. The page draws one dot per delivered broadcast per round, roughly nodes x (nodes - 1) x p_graph x rounds in total, and past a few tens of thousands it stops being followable; the exporter warns when your settings cross that line. And a late dump only does damage when an adversary can be the sole holder of a transaction, so if p_tx is high enough that everything is widely held, the attack will correctly show no effect.

The ledger

The consensus engine gossips bare integers, which is the right model for studying propagation and the wrong one for anything else. include/ledger/ holds the part that treats a transaction as a real object: SHA-256 hashing, RSA/SHA-256 signatures over a fixed byte layout, a UTXO pool, transaction validation against it, and a fork-aware block chain that keeps one UTXO pool per branch. It is measured on its own terms in docs/findings.md: what a single greedy pass over a batch costs against selecting to a fixed point, and what a chain that never prunes costs in memory and in mining time.

Layout

include/ledger/      crypto, transactions, UTXO pool, blocks, the chain
include/consensus/   trust-graph consensus: strategies, adversaries, harness
src/                 implementations
tests/               68 self-registering tests; tests/framework.h is the runner
plugins/             drop a .cpp here to add your own strategy; see its README
wasm/                the C entry points the browser app calls
web/                 the interactive app; engine.wasm is this C++, compiled
experiments/         one program per question, each writing a CSV
tools/               plotting, aggregation, and page generation
results/data/        raw output, one row per simulation rather than per cell
results/figures/     20 figures, PNG and SVG
docs/                design notes and the full findings

Design decisions worth knowing

Where two behaviours are both defensible, both are implemented and the choice sits behind a named option, so the difference can be measured rather than argued about.

Option Off On
HandlePolicy Greedy, one pass, order-dependent FixedPoint, maximal
BlockChain::Options::pruneBlocks every block ever added is retained only blocks that can still be extended
BlockChain::Options::prunePool mined transactions stay pending forever mined transactions leave the pool

BlockChain::Options::legacy() turns both forms of pruning off, which is the cheaper code and the more expensive run. experiments/chain_memory.cpp prices the difference.

Scope and limitations

Worth knowing before reading the numbers.

  • The quarantine strategy is evaluated on attacks written for this project. The adversaries, the defence and the metric are all mine, and the defence was designed after seeing how the attacks won. That result is in-sample and there is no held-out attack. The negative results, flooding's collapse and corroboration degrading agreement with no attacker present, do not depend on which defence was invented.
  • The model cannot equivocate. Each node sends one set to all followers per round, so an adversary cannot tell different peers different things. Results speak to withhold-and-dump attacks within this model, not to Byzantine agreement in general.
  • Everything is synthetic. Generated graphs, 100 nodes, 500 transactions, no churn, no delay, and no loss unless it is switched on. Transactions in the consensus model are bare integers with no cryptography attached.
  • The chain memory figures are estimates computed from sizeof, not measured resident memory.
  • The engine cross-check is narrow. The slow, obvious implementation used to validate the bitset engine covers the flooding strategy only.
  • The spam column is not a result. Fabricated transaction ids are never delivered in this model, so a spamming adversary is silent by construction and that column is identical to dead for reasons of modelling rather than measurement. It is listed so the tautology is visible.

docs/findings.md carries the full list.

Licence

MIT. See LICENSE.

About

A C++ simulator for gossip consensus over a trust graph. Configurable networks, pluggable node strategies and adversaries, a reproducible experiment pipeline, and an interactive front end compiled from the same sources to WebAssembly.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages