mirror of https://github.com/zcash/simtfl.git
Compare commits
13 Commits
f534e216ab
...
689931999b
Author | SHA1 | Date |
---|---|---|
Daira Emma Hopwood | 689931999b | |
Daira Emma Hopwood | 70431b16bd | |
Daira Emma Hopwood | a79c5dc461 | |
Daira Emma Hopwood | b5a92154b2 | |
Daira Emma Hopwood | 9f1765e813 | |
Daira Emma Hopwood | e22101d24b | |
Daira Emma Hopwood | 8918b71bdf | |
Daira Emma Hopwood | ffbc4792cf | |
Daira Emma Hopwood | 97c49bd8b0 | |
Daira Emma Hopwood | a4d1f24b43 | |
Daira Emma Hopwood | e28b33c53e | |
Daira Emma Hopwood | f4f90f25f4 | |
Daira Emma Hopwood | 70094bd0d2 |
12
.flake8
12
.flake8
|
@ -1,4 +1,14 @@
|
|||
[flake8]
|
||||
exclude = .git, __pycache__
|
||||
ignore = E302
|
||||
|
||||
# Justifications for each ignored error or warning:
|
||||
# * E302: always requiring two blank lines rather than one between top-level items is too annoying and nitpicky.
|
||||
# * E402: we want imports for tests to go between the main code and the tests.
|
||||
# * W503, W504: these give false positives for the style of avoiding \ at the end of each line by using parens.
|
||||
#
|
||||
# References:
|
||||
# - https://flake8.pycqa.org/en/latest/user/error-codes.html
|
||||
# - https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes
|
||||
ignore = E302, E402, W503, W504
|
||||
|
||||
max-line-length = 120
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
name: tests
|
||||
|
||||
on: pull_request
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install gnome-keyring
|
||||
run: sudo apt-get install gnome-keyring
|
||||
|
||||
- name: Install poetry
|
||||
run: pip install --user poetry
|
||||
|
||||
- name: Install dependencies
|
||||
run: poetry install --no-root
|
||||
|
||||
- name: Run tests
|
||||
# -p '[a-z]*.py' avoids running tests from __init__.py files twice.
|
||||
run: poetry run python -m unittest discover -s simtfl -t . -p '[a-z]*.py' --verbose --buffer
|
31
README.md
31
README.md
|
@ -23,30 +23,25 @@ Note the caveats: *experimental*, *simulator*, *research*, *potential*.
|
|||
|
||||
poetry run demo
|
||||
|
||||
## Programming patterns
|
||||
## Documentation
|
||||
|
||||
The code makes use of the [simpy](https://simpy.readthedocs.io/en/latest/)
|
||||
discrete event simulation library. This means that functions representing
|
||||
processes are implemented as generators, so that the library can simulate
|
||||
timeouts and asynchronous communication (typically faster than real time).
|
||||
Design documentation is under the `doc/` directory:
|
||||
|
||||
We use the convention of putting "(process)" in the doc comment of these
|
||||
functions. They either must use the `yield` construct, *or* return the
|
||||
result of calling another "(process)" function (not both).
|
||||
* [Programming patterns for use of simpy](doc/patterns.md).
|
||||
|
||||
Objects that implement processes typically hold the `simpy.Environment` in
|
||||
an instance variable `self.env`.
|
||||
|
||||
To wait for another process `f()` before continuing, use `yield from f()`.
|
||||
(If it is the last thing to do in a function with no other `yield`
|
||||
statements, `return f()` can be used as an optimization.)
|
||||
|
||||
A "(process)" function that does nothing should `return skip()`, using
|
||||
`simtfl.util.skip`.
|
||||
You can also generate API documentation by running `./gendoc.sh`.
|
||||
The starting point for the generated documentation is <apidoc/simtfl.html>.
|
||||
|
||||
## Contributing
|
||||
|
||||
Please check `poetry run flake8` before submitting a PR.
|
||||
Please use `./check.sh` before submitting a PR. This currently runs `flake8`
|
||||
and the unit tests locally.
|
||||
|
||||
You can use `./check.sh -k <substring>` to run `flake8` and then only tests
|
||||
with names matching the given substring. This will not suppress output to
|
||||
stdout or stderr (but `./check.sh -bk <substring>` will).
|
||||
|
||||
To see other options for running unit tests, use `poetry run python -m unittest -h`.
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
cd -P -- "$(dirname -- "$(command -v -- "$0")")"
|
||||
|
||||
echo Running flake8...
|
||||
poetry run flake8
|
||||
|
||||
echo
|
||||
echo Running unit tests...
|
||||
args="${*:---buffer}"
|
||||
poetry run python -m unittest discover -s simtfl -t . -p '[a-z]*.py' --verbose ${args}
|
|
@ -0,0 +1,20 @@
|
|||
# Programming patterns
|
||||
|
||||
The code makes use of the [simpy](https://simpy.readthedocs.io/en/latest/)
|
||||
discrete event simulation library. This means that functions representing
|
||||
processes are implemented as generators, so that the library can simulate
|
||||
timeouts and asynchronous communication (typically faster than real time).
|
||||
|
||||
We use the convention of putting "(process)" in the doc comment of these
|
||||
functions. They either must use the `yield` construct, *or* return the
|
||||
result of calling another "(process)" function (not both).
|
||||
|
||||
Objects that implement processes typically hold the `simpy.Environment` in
|
||||
an instance variable `self.env`.
|
||||
|
||||
To wait for another process `f()` before continuing, use `yield from f()`.
|
||||
(If it is the last thing to do in a function with no other `yield`
|
||||
statements, `return f()` can be used as an optimization.)
|
||||
|
||||
A "(process)" function that does nothing should `return skip()`, using
|
||||
`simtfl.util.skip`.
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
poetry run pdoc simtfl -o apidoc --no-include-undocumented -d markdown
|
|
@ -17,6 +17,94 @@ mccabe = ">=0.7.0,<0.8.0"
|
|||
pycodestyle = ">=2.11.0,<2.12.0"
|
||||
pyflakes = ">=3.1.0,<3.2.0"
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.2"
|
||||
description = "A very fast and expressive template engine."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
|
||||
{file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=2.0"
|
||||
|
||||
[package.extras]
|
||||
i18n = ["Babel (>=2.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "2.1.3"
|
||||
description = "Safely add untrusted strings to HTML/XML markup."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"},
|
||||
{file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"},
|
||||
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"},
|
||||
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"},
|
||||
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"},
|
||||
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"},
|
||||
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"},
|
||||
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"},
|
||||
{file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"},
|
||||
{file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
|
||||
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
|
||||
{file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"},
|
||||
{file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"},
|
||||
{file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"},
|
||||
{file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"},
|
||||
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"},
|
||||
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"},
|
||||
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"},
|
||||
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"},
|
||||
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"},
|
||||
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"},
|
||||
{file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"},
|
||||
{file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"},
|
||||
{file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"},
|
||||
{file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"},
|
||||
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"},
|
||||
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"},
|
||||
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"},
|
||||
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"},
|
||||
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"},
|
||||
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"},
|
||||
{file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"},
|
||||
{file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"},
|
||||
{file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mccabe"
|
||||
version = "0.7.0"
|
||||
|
@ -29,16 +117,36 @@ files = [
|
|||
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pdoc"
|
||||
version = "14.1.0"
|
||||
description = "API Documentation for Python Projects"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pdoc-14.1.0-py3-none-any.whl", hash = "sha256:e8869dffe21296b3bd5545b28e7f07cae0656082aca43f8915323187e541b126"},
|
||||
{file = "pdoc-14.1.0.tar.gz", hash = "sha256:3a0bd921a05c39a82b1505089eb6dc99d857b71b856aa60d1aca4d9086d0e18c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Jinja2 = ">=2.11.0"
|
||||
MarkupSafe = "*"
|
||||
pygments = ">=2.12.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["black", "hypothesis", "mypy", "pygments (>=2.14.0)", "pytest", "pytest-cov", "pytest-timeout", "ruff", "tox", "types-pygments"]
|
||||
|
||||
[[package]]
|
||||
name = "pycodestyle"
|
||||
version = "2.11.0"
|
||||
version = "2.11.1"
|
||||
description = "Python style guide checker"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pycodestyle-2.11.0-py2.py3-none-any.whl", hash = "sha256:5d1013ba8dc7895b548be5afb05740ca82454fd899971563d2ef625d090326f8"},
|
||||
{file = "pycodestyle-2.11.0.tar.gz", hash = "sha256:259bcc17857d8a8b3b4a2327324b79e5f020a13c16074670f9c8c8f872ea76d0"},
|
||||
{file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"},
|
||||
{file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -53,6 +161,21 @@ files = [
|
|||
{file = "pyflakes-3.1.0.tar.gz", hash = "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.16.1"
|
||||
description = "Pygments is a syntax highlighting package written in Python."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"},
|
||||
{file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
plugins = ["importlib-metadata"]
|
||||
|
||||
[[package]]
|
||||
name = "simpy"
|
||||
version = "4.0.2"
|
||||
|
@ -68,4 +191,4 @@ files = [
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "c164f34aba3ecbda9fc9942d6e2bfad3eb11491c433ee46bdf0262fd8147209f"
|
||||
content-hash = "d9f1f22d9f90f2ad4d00ec2785eee468e847dc1c8c4015615febe6094edf3e62"
|
||||
|
|
|
@ -12,9 +12,11 @@ simpy = "^4"
|
|||
|
||||
[tool.poetry.dev-dependencies]
|
||||
flake8 = "^6"
|
||||
pdoc = "^14"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
demo = "simtfl.demo:run"
|
||||
bc-demo = "simtfl.bc.demo:run"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
This is an experimental simulator for research into a potential [Trailing Finality
|
||||
Layer](https://electriccoin.co/blog/the-trailing-finality-layer-a-stepping-stone-to-proof-of-stake-in-zcash/)
|
||||
for Zcash.
|
||||
|
||||
See the [README](../README.md) for more information.
|
||||
"""
|
|
@ -0,0 +1,147 @@
|
|||
"""
|
||||
Abstractions for best-chain transactions, contexts, and blocks.
|
||||
"""
|
||||
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
|
||||
from ..util import Unique
|
||||
|
||||
|
||||
class BCTransaction:
|
||||
"""A transaction for a best-chain protocol."""
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _TXO:
|
||||
tx: 'BCTransaction'
|
||||
index: int
|
||||
value: int
|
||||
|
||||
def __init__(self, inputs, output_values, fee, issuance=0):
|
||||
"""
|
||||
Constructs a `BCTransaction` with the given inputs, output values, fee,
|
||||
and (if it is a coinbase transaction) issuance.
|
||||
The elements of `inputs` are TXO objects obtained from the `output` method
|
||||
of another `BCTransaction`.
|
||||
For a coinbase transaction, pass `inputs=[]`, and `fee` as a negative value
|
||||
of magnitude equal to the total amount of fees paid by other transactions
|
||||
in the block.
|
||||
"""
|
||||
assert issuance >= 0
|
||||
assert fee >= 0 or len(inputs) == 0
|
||||
assert issuance == 0 or len(inputs) == 0
|
||||
assert all((isinstance(txin, self._TXO) for txin in inputs))
|
||||
assert sum((txin.value for txin in inputs)) + issuance == sum(output_values) + fee
|
||||
self.inputs = inputs
|
||||
self.outputs = [self._TXO(self, i, v) for (i, v) in enumerate(output_values)]
|
||||
self.fee = fee
|
||||
self.issuance = issuance
|
||||
|
||||
def input(self, index):
|
||||
"""Returns the input with the given index."""
|
||||
return self.inputs[index]
|
||||
|
||||
def output(self, index):
|
||||
"""Returns the output with the given index."""
|
||||
return self.outputs[index]
|
||||
|
||||
def is_coinbase(self):
|
||||
"""Returns `True` if this is a coinbase transaction (it has no inputs)."""
|
||||
return len(self.inputs) == 0
|
||||
|
||||
|
||||
class BCContext:
|
||||
"""
|
||||
A context that allows checking transactions for contextual validity in a
|
||||
best-chain protocol.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Constructs an empty `BCContext`."""
|
||||
self.transactions = deque()
|
||||
self.utxo_set = set()
|
||||
self.total_issuance = 0
|
||||
|
||||
def is_valid(self, tx):
|
||||
"""Is `tx` valid in this context?"""
|
||||
return set(tx.inputs).issubset(self.utxo_set)
|
||||
|
||||
def add_if_valid(self, tx):
|
||||
"""
|
||||
If `tx` is valid in this context, add it to the context and return `True`.
|
||||
Otherwise leave the context unchanged and return `False`.
|
||||
"""
|
||||
txins = set(tx.inputs)
|
||||
valid = txins.issubset(self.utxo_set)
|
||||
if valid:
|
||||
self.utxo_set -= txins
|
||||
self.utxo_set |= set(tx.outputs)
|
||||
self.total_issuance += tx.issuance
|
||||
self.transactions.append(tx)
|
||||
|
||||
return valid
|
||||
|
||||
|
||||
class BCBlock:
|
||||
"""A block in a best-chain protocol."""
|
||||
|
||||
def __init__(self, parent, added_score, transactions, allow_invalid=False):
|
||||
"""
|
||||
Constructs a `BCBlock` with the given parent block, score relative to the parent,
|
||||
and transactions.
|
||||
If `allow_invalid` is set, the block need not be valid.
|
||||
Use `parent=None` to construct the genesis block.
|
||||
"""
|
||||
assert all((isinstance(tx, BCTransaction) for tx in transactions))
|
||||
self.parent = parent
|
||||
self.score = (0 if parent is None else self.parent.score) + added_score
|
||||
self.transactions = transactions
|
||||
self.hash = Unique()
|
||||
assert allow_invalid or self.is_noncontextually_valid()
|
||||
|
||||
def is_noncontextually_valid(self):
|
||||
"""
|
||||
Are non-contextual consensus rules satisfied for this block?
|
||||
"""
|
||||
return (
|
||||
len(self.transactions) > 0 and
|
||||
self.transactions[0].is_coinbase() and
|
||||
not any((tx.is_coinbase() for tx in self.transactions[1:])) and
|
||||
sum((tx.fee for tx in self.transactions)) == 0
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BCProtocol:
|
||||
"""A best-chain protocol."""
|
||||
|
||||
Transaction: type[object] = BCTransaction
|
||||
"""The type of transactions for this protocol."""
|
||||
|
||||
Context: type[object] = BCContext
|
||||
"""The type of contexts for this protocol."""
|
||||
|
||||
Block: type[object] = BCBlock
|
||||
"""The type of blocks for this protocol."""
|
||||
|
||||
|
||||
__all__ = ['BCTransaction', 'BCContext', 'BCBlock', 'BCProtocol']
|
||||
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
class TestBC(unittest.TestCase):
|
||||
def test_basic(self):
|
||||
ctx = BCContext()
|
||||
issuance_tx0 = BCTransaction([], [10], 0, issuance=10)
|
||||
assert ctx.add_if_valid(issuance_tx0)
|
||||
genesis = BCBlock(None, 1, [issuance_tx0])
|
||||
assert genesis.score == 1
|
||||
|
||||
issuance_tx1 = BCTransaction([], [6], -1, issuance=5)
|
||||
spend_tx = BCTransaction([issuance_tx0.output(0)], [9], 1)
|
||||
assert ctx.add_if_valid(issuance_tx1)
|
||||
assert ctx.add_if_valid(spend_tx)
|
||||
block1 = BCBlock(genesis, 1, [issuance_tx1, spend_tx])
|
||||
assert block1.score == 2
|
|
@ -0,0 +1,18 @@
|
|||
from . import BCBlock, BCContext, BCTransaction
|
||||
|
||||
def run():
|
||||
"""
|
||||
Runs the demo.
|
||||
"""
|
||||
ctx = BCContext()
|
||||
issuance_tx0 = BCTransaction([], [10], 0, issuance=10)
|
||||
assert ctx.add_if_valid(issuance_tx0)
|
||||
genesis = BCBlock(None, 1, [issuance_tx0])
|
||||
assert genesis.score == 1
|
||||
|
||||
issuance_tx1 = BCTransaction([], [6], -1, issuance=5)
|
||||
spend_tx = BCTransaction([issuance_tx0.output(0)], [9], 1)
|
||||
assert ctx.add_if_valid(issuance_tx1)
|
||||
assert ctx.add_if_valid(spend_tx)
|
||||
block1 = BCBlock(genesis, 1, [issuance_tx1, spend_tx])
|
||||
assert block1.score == 2
|
|
@ -58,14 +58,9 @@ def run():
|
|||
"""
|
||||
Runs the demo.
|
||||
"""
|
||||
env = Environment()
|
||||
network = Network(env, delay=4)
|
||||
network = Network(Environment(), delay=4)
|
||||
for i in range(10):
|
||||
network.add_node(PongNode(i, env, network))
|
||||
network.add_node(PongNode())
|
||||
|
||||
network.add_node(PingNode(10, env, network))
|
||||
|
||||
for i in range(network.num_nodes()):
|
||||
env.process(network.start_node(i))
|
||||
|
||||
env.run()
|
||||
network.add_node(PingNode())
|
||||
network.run_all()
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
"""
|
||||
Base classes for messages.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PayloadMessage:
|
||||
"""
|
||||
A message with an arbitrary payload.
|
||||
"""
|
||||
def __init__(self, payload):
|
||||
"""
|
||||
Constructs a `PayloadMessage` with the given payload.
|
||||
"""
|
||||
self.payload = payload
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.__class__.__name__}({self.payload})"
|
||||
payload: Any
|
||||
"""The payload."""
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
"""
|
||||
Framework for message passing in a network of nodes.
|
||||
"""
|
||||
|
||||
from .util import skip
|
||||
|
||||
|
||||
|
@ -30,16 +34,34 @@ class Network:
|
|||
"""
|
||||
Adds a node with the next available ident.
|
||||
"""
|
||||
ident = self.num_nodes()
|
||||
self.nodes.append(node)
|
||||
node.initialize(ident, self.env, self)
|
||||
|
||||
def start_node(self, ident):
|
||||
"""
|
||||
(process) Start the node with the given ident.
|
||||
"""
|
||||
node = self.node(ident)
|
||||
print(f"T{self.env.now:5d}: starting {node}")
|
||||
print(f"T{self.env.now:5d}: starting {ident:2d}: {node}")
|
||||
return node.run()
|
||||
|
||||
def start_processes(self):
|
||||
"""
|
||||
Start a process for each node.
|
||||
"""
|
||||
print()
|
||||
for i in range(self.num_nodes()):
|
||||
self.env.process(self.start_node(i))
|
||||
|
||||
def run_all(self, *args, **kwargs):
|
||||
"""
|
||||
Convenience method to start a process for each node, then start
|
||||
the simulation. Takes the same arguments as `simpy.Environment.run`.
|
||||
"""
|
||||
self.start_processes()
|
||||
self.env.run(*args, **kwargs)
|
||||
|
||||
def send(self, sender, target, message, delay=None):
|
||||
"""
|
||||
(process) Sends a message to the node with ident `target`, from the node
|
||||
|
|
115
simtfl/node.py
115
simtfl/node.py
|
@ -1,42 +1,57 @@
|
|||
from .util import skip
|
||||
"""
|
||||
Base classes for node implementations.
|
||||
"""
|
||||
|
||||
from collections import deque
|
||||
|
||||
from .util import skip
|
||||
|
||||
|
||||
class PassiveNode:
|
||||
"""
|
||||
A node that sends no messages and does nothing with received messages.
|
||||
This class is intended to be subclassed.
|
||||
A node that processes messages concurrently. By default it sends no
|
||||
messages and does nothing with received messages. This class is
|
||||
intended to be subclassed.
|
||||
|
||||
Inherit from this class directly if all messages are to be processed
|
||||
concurrently without blocking. If messages are to be processed
|
||||
sequentially, it may be easier to inherit from `SequentialNode`.
|
||||
|
||||
Note that the simulation is deterministic regardless of which option
|
||||
is selected.
|
||||
"""
|
||||
def __init__(self, ident, env, network):
|
||||
def initialize(self, ident, env, network):
|
||||
"""
|
||||
Constructs a PassiveNode with the given simpy Environment and network.
|
||||
Initializes a PassiveNode with the given ident, simpy Environment,
|
||||
and network. Nodes are initialized when they are added to a network.
|
||||
"""
|
||||
self.ident = ident
|
||||
self.env = env
|
||||
self.network = network
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.ident:2d}: {self.__class__.__name__}"
|
||||
return f"{self.__class__.__name__}"
|
||||
|
||||
def send(self, target, message):
|
||||
"""
|
||||
(process) This method can be overridden to intercept messages being sent
|
||||
by this node. It should typically call `self.network.send`.
|
||||
by this node. The implementation in this class calls `self.network.send`.
|
||||
"""
|
||||
return self.network.send(self.ident, target, message)
|
||||
|
||||
def receive(self, sender, message):
|
||||
"""
|
||||
(process) This method can be overridden to intercept messages being received
|
||||
by this node. It should typically call `self.handle`.
|
||||
by this node. The implementation in this class calls `self.handle`.
|
||||
"""
|
||||
return self.handle(sender, message)
|
||||
|
||||
def handle(self, message, sender):
|
||||
def handle(self, sender, message):
|
||||
"""
|
||||
(process) Handles a message by doing nothing. Note that the handling of
|
||||
each message, and the `run` method, are in separate simpy processes. That
|
||||
is, yielding here will not block other incoming messages.
|
||||
is, yielding here will not block other incoming messages for a direct
|
||||
subclass of `PassiveNode`.
|
||||
"""
|
||||
return skip()
|
||||
|
||||
|
@ -49,13 +64,15 @@ class PassiveNode:
|
|||
|
||||
class SequentialNode(PassiveNode):
|
||||
"""
|
||||
A node that processes messages sequentially.
|
||||
A node that processes messages sequentially. By default it sends no
|
||||
messages and does nothing with received messages. This class is
|
||||
intended to be subclassed.
|
||||
"""
|
||||
def __init__(self, ident, env, network):
|
||||
def initialize(self, ident, env, network):
|
||||
"""
|
||||
Constructs a SequentialNode with the given simpy Environment and network.
|
||||
Initializes a SequentialNode with the given simpy Environment and network.
|
||||
"""
|
||||
super().__init__(ident, env, network)
|
||||
super().initialize(ident, env, network)
|
||||
self._mailbox = deque()
|
||||
self._wakeup = env.event()
|
||||
|
||||
|
@ -70,6 +87,16 @@ class SequentialNode(PassiveNode):
|
|||
pass
|
||||
return skip()
|
||||
|
||||
def handle(self, sender, message):
|
||||
"""
|
||||
(process) Handles a message by doing nothing. Messages are handled
|
||||
sequentially; that is, handling of the next message will be blocked
|
||||
on this process.
|
||||
"""
|
||||
# This is the same implementation as `PassiveNode`, but the documentation
|
||||
# is different.
|
||||
return skip()
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
(process) Repeatedly handle incoming messages.
|
||||
|
@ -87,3 +114,63 @@ class SequentialNode(PassiveNode):
|
|||
# concurrency.
|
||||
self._wakeup = self.env.event()
|
||||
yield self._wakeup
|
||||
|
||||
|
||||
__all__ = ['PassiveNode', 'SequentialNode']
|
||||
|
||||
from simpy import Environment
|
||||
import unittest
|
||||
|
||||
from .message import PayloadMessage
|
||||
from .network import Network
|
||||
|
||||
|
||||
class PassiveReceiverTestNode(PassiveNode):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.received = deque()
|
||||
|
||||
def handle(self, sender, message):
|
||||
self.received.append((sender, message, self.env.now))
|
||||
yield self.env.timeout(3)
|
||||
|
||||
|
||||
class SequentialReceiverTestNode(SequentialNode):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.received = deque()
|
||||
|
||||
def handle(self, sender, message):
|
||||
self.received.append((sender, message, self.env.now))
|
||||
yield self.env.timeout(3)
|
||||
|
||||
|
||||
class SenderTestNode(PassiveNode):
|
||||
def run(self):
|
||||
for i in range(3):
|
||||
yield from self.send(0, PayloadMessage(i))
|
||||
yield self.env.timeout(1)
|
||||
|
||||
|
||||
class TestFramework(unittest.TestCase):
|
||||
def _test_node(self, receiver_node, expected):
|
||||
network = Network(Environment())
|
||||
network.add_node(receiver_node)
|
||||
network.add_node(SenderTestNode())
|
||||
network.run_all()
|
||||
|
||||
self.assertEqual(list(network.node(0).received), expected)
|
||||
|
||||
def test_passive_node(self):
|
||||
self._test_node(PassiveReceiverTestNode(), [
|
||||
(1, PayloadMessage(0), 1),
|
||||
(1, PayloadMessage(1), 2),
|
||||
(1, PayloadMessage(2), 3),
|
||||
])
|
||||
|
||||
def test_sequential_node(self):
|
||||
self._test_node(SequentialReceiverTestNode(), [
|
||||
(1, PayloadMessage(0), 1),
|
||||
(1, PayloadMessage(1), 4),
|
||||
(1, PayloadMessage(2), 7),
|
||||
])
|
||||
|
|
|
@ -2,9 +2,15 @@
|
|||
Utilities.
|
||||
"""
|
||||
|
||||
|
||||
def skip():
|
||||
"""
|
||||
(process) Does nothing.
|
||||
"""
|
||||
# Make this a generator.
|
||||
yield from []
|
||||
|
||||
|
||||
class Unique:
|
||||
"""Represents a unique value."""
|
||||
pass
|
||||
|
|
Loading…
Reference in New Issue