wunderspec 0.136.1__tar.gz → 0.136.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. {wunderspec-0.136.1 → wunderspec-0.136.4}/CHANGELOG.md +6 -0
  2. {wunderspec-0.136.1 → wunderspec-0.136.4}/PKG-INFO +6 -35
  3. {wunderspec-0.136.1 → wunderspec-0.136.4}/README.md +5 -34
  4. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/tendermint_single.py +100 -134
  5. wunderspec-0.136.4/examples/tendermint_single_indinv.py +807 -0
  6. {wunderspec-0.136.1 → wunderspec-0.136.4}/pyproject.toml +1 -1
  7. {wunderspec-0.136.1 → wunderspec-0.136.4}/tests/README.md +8 -8
  8. {wunderspec-0.136.1 → wunderspec-0.136.4}/uv.lock +1 -1
  9. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/__init__.py +1 -1
  10. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/apalache.py +3 -2
  11. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/api.py +72 -2
  12. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/tlc.py +3 -2
  13. wunderspec-0.136.1/examples/tendermint_single_indinv.py +0 -516
  14. {wunderspec-0.136.1 → wunderspec-0.136.4}/.devcontainer/Dockerfile +0 -0
  15. {wunderspec-0.136.1 → wunderspec-0.136.4}/.devcontainer/devcontainer.json +0 -0
  16. {wunderspec-0.136.1 → wunderspec-0.136.4}/.devcontainer/post-create.sh +0 -0
  17. {wunderspec-0.136.1 → wunderspec-0.136.4}/.flake8 +0 -0
  18. {wunderspec-0.136.1 → wunderspec-0.136.4}/.github/workflows/build.yml +0 -0
  19. {wunderspec-0.136.1 → wunderspec-0.136.4}/.github/workflows/convert-to-tla.yml +0 -0
  20. {wunderspec-0.136.1 → wunderspec-0.136.4}/.github/workflows/lint.yml +0 -0
  21. {wunderspec-0.136.1 → wunderspec-0.136.4}/.github/workflows/publish.yml +0 -0
  22. {wunderspec-0.136.1 → wunderspec-0.136.4}/.github/workflows/run-examples.yml +0 -0
  23. {wunderspec-0.136.1 → wunderspec-0.136.4}/.gitignore +0 -0
  24. {wunderspec-0.136.1 → wunderspec-0.136.4}/CONTRIBUTING.md +0 -0
  25. {wunderspec-0.136.1 → wunderspec-0.136.4}/LICENSE +0 -0
  26. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/README.md +0 -0
  27. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/png/wunderspec-avatar-github-1024.png +0 -0
  28. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/png/wunderspec-avatar-github-256.png +0 -0
  29. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/png/wunderspec-avatar-github-512.png +0 -0
  30. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/png/wunderspec-icon-circle-dark-1024.png +0 -0
  31. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/png/wunderspec-icon-circle-dark-512.png +0 -0
  32. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/png/wunderspec-icon-circle-transparent-1024.png +0 -0
  33. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/png/wunderspec-lockup-horizontal-dark-1600.png +0 -0
  34. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/png/wunderspec-lockup-horizontal-dark-800.png +0 -0
  35. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/png/wunderspec-mark-transparent-1024.png +0 -0
  36. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/png/wunderspec-readme-header-dark-1200x480.png +0 -0
  37. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/png/wunderspec-readme-header-dark-1600x640.png +0 -0
  38. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/png/wunderspec-social-preview-1280x640.png +0 -0
  39. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/png/wunderspec-wordmark-transparent-900.png +0 -0
  40. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/svg/wunderspec-avatar-github.svg +0 -0
  41. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/svg/wunderspec-icon-circle-dark.svg +0 -0
  42. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/svg/wunderspec-icon-circle-transparent.svg +0 -0
  43. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/svg/wunderspec-lockup-horizontal-dark.svg +0 -0
  44. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/svg/wunderspec-lockup-horizontal-transparent.svg +0 -0
  45. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/svg/wunderspec-mark-transparent.svg +0 -0
  46. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/svg/wunderspec-readme-header-dark.svg +0 -0
  47. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/svg/wunderspec-wordmark-transparent.svg +0 -0
  48. {wunderspec-0.136.1 → wunderspec-0.136.4}/assets/design/svg/wunderspec_flower.svg +0 -0
  49. {wunderspec-0.136.1 → wunderspec-0.136.4}/basedpyrightconfig.json +0 -0
  50. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/references/from-quint-llms.md +0 -0
  51. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/references/from-tla-llms.md +0 -0
  52. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/user-references/booleans.md +0 -0
  53. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/user-references/cheatsheet.html +0 -0
  54. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/user-references/comprehensions.md +0 -0
  55. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/user-references/decorators.md +0 -0
  56. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/user-references/enums.md +0 -0
  57. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/user-references/flow.md +0 -0
  58. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/user-references/integers.md +0 -0
  59. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/user-references/lists.md +0 -0
  60. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/user-references/maps.md +0 -0
  61. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/user-references/records.md +0 -0
  62. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/user-references/sets.md +0 -0
  63. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/user-references/state-machine.md +0 -0
  64. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/user-references/strings.md +0 -0
  65. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/user-references/temporal.md +0 -0
  66. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/user-references/tuples.md +0 -0
  67. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/user-references/unions.md +0 -0
  68. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/user-references/wunderspec-five-minutes.md +0 -0
  69. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/user-stories/bobs_log.md +0 -0
  70. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/user-stories/bobs_log.png +0 -0
  71. {wunderspec-0.136.1 → wunderspec-0.136.4}/docs/user-stories/img/bob.png +0 -0
  72. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/bags.py +0 -0
  73. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/bakery.py +0 -0
  74. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/bakery_walk.py +0 -0
  75. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/ben_or.py +0 -0
  76. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/channel.py +0 -0
  77. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/dekker.py +0 -0
  78. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/epfd.py +0 -0
  79. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/etcdraft.py +0 -0
  80. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/examples.yaml +0 -0
  81. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/fifo.py +0 -0
  82. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/fpaxos.py +0 -0
  83. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/kv_store.py +0 -0
  84. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/lamport_mutex.py +0 -0
  85. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/ledger.py +0 -0
  86. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/minimmit.py +0 -0
  87. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/payment.py +0 -0
  88. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/producer_consumer.py +0 -0
  89. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/readers_writers.py +0 -0
  90. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/readers_writers_walk.py +0 -0
  91. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/simple_ponzi.py +0 -0
  92. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/simple_ponzi_machine.py +0 -0
  93. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/simple_ponzi_machine_walk.py +0 -0
  94. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/simple_wal1.py +0 -0
  95. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/simple_wal2.py +0 -0
  96. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/sliding_puzzles.py +0 -0
  97. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/sloppy_counter.py +0 -0
  98. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/test_simple_ponzi_machine.py +0 -0
  99. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/tiny_workers.py +0 -0
  100. {wunderspec-0.136.1 → wunderspec-0.136.4}/examples/two_phase.py +0 -0
  101. {wunderspec-0.136.1 → wunderspec-0.136.4}/pyrightconfig.json +0 -0
  102. {wunderspec-0.136.1 → wunderspec-0.136.4}/scripts/__init__.py +0 -0
  103. {wunderspec-0.136.1 → wunderspec-0.136.4}/scripts/_spec_utils.py +0 -0
  104. {wunderspec-0.136.1 → wunderspec-0.136.4}/scripts/convert_examples_to_tla.py +0 -0
  105. {wunderspec-0.136.1 → wunderspec-0.136.4}/scripts/run_examples.py +0 -0
  106. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/__main__.py +0 -0
  107. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/_edition.py +0 -0
  108. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/ast/__init__.py +0 -0
  109. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/ast/action_ast.py +0 -0
  110. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/ast/ast.py +0 -0
  111. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/ast/list_ast.py +0 -0
  112. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/ast/map_ast.py +0 -0
  113. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/ast/record_ast.py +0 -0
  114. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/ast/serialization.py +0 -0
  115. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/ast/set_ast.py +0 -0
  116. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/ast/sorts.py +0 -0
  117. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/ast/temporal_ast.py +0 -0
  118. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/ast/terms.py +0 -0
  119. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/ast/tuple_ast.py +0 -0
  120. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/ast/union_ast.py +0 -0
  121. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/cli.py +0 -0
  122. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/dev_debug.py +0 -0
  123. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/doc_format.py +0 -0
  124. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/errors.py +0 -0
  125. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/exec/__init__.py +0 -0
  126. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/exec/action_exec.py +0 -0
  127. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/exec/action_profile.py +0 -0
  128. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/exec/context.py +0 -0
  129. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/exec/scheduler.py +0 -0
  130. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/explain.py +0 -0
  131. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/expr.py +0 -0
  132. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/flow.py +0 -0
  133. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/fuzzer.py +0 -0
  134. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/interpreter.py +0 -0
  135. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/interpreter_sampling.py +0 -0
  136. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/interpreter_value.py +0 -0
  137. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/itf_trace.py +0 -0
  138. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/lang.py +0 -0
  139. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/linter.py +0 -0
  140. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/machine.py +0 -0
  141. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/model_checker.py +0 -0
  142. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/permutation.py +0 -0
  143. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/petnames.py +0 -0
  144. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/pretty.py +0 -0
  145. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/py.typed +0 -0
  146. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/quint_convert.py +0 -0
  147. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/random_walk.py +0 -0
  148. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/source_tracking.py +0 -0
  149. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/submachine.py +0 -0
  150. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/sym_context.py +0 -0
  151. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/tla.py +0 -0
  152. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/tlc_trace.py +0 -0
  153. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/trace_output.py +0 -0
  154. {wunderspec-0.136.1 → wunderspec-0.136.4}/wunderspec/uniq_names.py +0 -0
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.136.4] -- 2026-07-06
4
+
5
+ Changes since public release v0.136.1.
6
+
7
+ - Add an inductive invariant in `examples/tendermint_single_indinv.py`.
8
+
3
9
  ## [0.136.1] -- 2026-07-04
4
10
 
5
11
  Changes since public release v0.134.1.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wunderspec
3
- Version: 0.136.1
3
+ Version: 0.136.4
4
4
  Summary: Protocol specifications as Python code
5
5
  Author: Igor Konnov, Thomas Pani
6
6
  License-File: LICENSE
@@ -103,45 +103,16 @@ commands are not included in this package.
103
103
 
104
104
  ## 4. Release Provenance
105
105
 
106
- - Release tag: `v0.136.1`
107
- - Source commit: `6ef37ade3f08676708e0127d20ba14e313abbbb2`
106
+ - Release tag: `v0.136.4`
107
+ - Source commit: `7beedd2736b068f1c371f59fd8ed619a1d9e75a5`
108
108
 
109
109
  See [tests/README.md][] for the development test log captured at release time.
110
110
 
111
111
  ## 5. Latest Release Notes
112
112
 
113
- Changes since public release v0.134.1.
114
-
115
- - Add `--coverage NAME` to `wunderspec with-apalache`, passing the generated
116
- TLA+ operator for the selected `@coverage` function as `--view=<Oper>` for
117
- `check` and `simulate`.
118
- - Allow `wunderspec with-apalache --max-steps 0` to check initial states.
119
- - Write Apalache output to an `apalache.log` file while it runs, report the log
120
- path, and add `--verbose` to stream the log to the terminal immediately.
121
- - Require `default=...` for collection-form `Max` and `Min`, e.g.
122
- `Max(s, default=0)`. The default is the reduce seed, so empty collections no
123
- longer need a separate size guard or `CHOOSE`-based seed.
124
- - Render collection-form `Max` and `Min` reducers as TLAPS-friendly `CHOOSE`
125
- expressions in generated TLA+ while keeping reducer-based interpreter
126
- evaluation.
127
- - Add TLA+ labels to `wunderspec convert` output for `@invariant` and
128
- `@example` operators, including nested predicate calls by default. Use
129
- `@invariant(inline=True)` or `@example(inline=True)` to inline nested
130
- predicate calls instead.
131
- - Preserve definition docstrings when converting Wunderspec to TLA+ by emitting
132
- them as `\*` comments immediately before the generated operator definitions.
133
- - Allow generator-form `Forall`, `Exists`, `SetIf`, `Set`, and `Map` calls to
134
- pass `name=` for readable TLA+ binder names while preserving internal unique
135
- binder identities.
136
- - Short-circuit interpreter evaluation of `Implies(False, rhs)`, so the right
137
- side is not evaluated when the antecedent is false.
138
- - Add `Max` and `Min` builtins. They take either several integers
139
- (`Max(a, b, c)`) or a single set or list of integers (`Max(s)`). Both are
140
- syntactic sugar — the multi-argument form expands to nested `Ite` and the
141
- collection form folds with `reduce` — so they work in the interpreter, TLC,
142
- and Apalache. `Max(s)`/`Min(s)` over an empty collection are undefined and
143
- raise, mirroring `CHOOSE` over an empty set; guard with
144
- `Ite(s.size == 0, default, Max(s))` when you need a fallback.
113
+ Changes since public release v0.136.1.
114
+
115
+ - Add an inductive invariant in `examples/tendermint_single_indinv.py`.
145
116
 
146
117
  ## 6. License
147
118
 
@@ -87,45 +87,16 @@ commands are not included in this package.
87
87
 
88
88
  ## 4. Release Provenance
89
89
 
90
- - Release tag: `v0.136.1`
91
- - Source commit: `6ef37ade3f08676708e0127d20ba14e313abbbb2`
90
+ - Release tag: `v0.136.4`
91
+ - Source commit: `7beedd2736b068f1c371f59fd8ed619a1d9e75a5`
92
92
 
93
93
  See [tests/README.md][] for the development test log captured at release time.
94
94
 
95
95
  ## 5. Latest Release Notes
96
96
 
97
- Changes since public release v0.134.1.
98
-
99
- - Add `--coverage NAME` to `wunderspec with-apalache`, passing the generated
100
- TLA+ operator for the selected `@coverage` function as `--view=<Oper>` for
101
- `check` and `simulate`.
102
- - Allow `wunderspec with-apalache --max-steps 0` to check initial states.
103
- - Write Apalache output to an `apalache.log` file while it runs, report the log
104
- path, and add `--verbose` to stream the log to the terminal immediately.
105
- - Require `default=...` for collection-form `Max` and `Min`, e.g.
106
- `Max(s, default=0)`. The default is the reduce seed, so empty collections no
107
- longer need a separate size guard or `CHOOSE`-based seed.
108
- - Render collection-form `Max` and `Min` reducers as TLAPS-friendly `CHOOSE`
109
- expressions in generated TLA+ while keeping reducer-based interpreter
110
- evaluation.
111
- - Add TLA+ labels to `wunderspec convert` output for `@invariant` and
112
- `@example` operators, including nested predicate calls by default. Use
113
- `@invariant(inline=True)` or `@example(inline=True)` to inline nested
114
- predicate calls instead.
115
- - Preserve definition docstrings when converting Wunderspec to TLA+ by emitting
116
- them as `\*` comments immediately before the generated operator definitions.
117
- - Allow generator-form `Forall`, `Exists`, `SetIf`, `Set`, and `Map` calls to
118
- pass `name=` for readable TLA+ binder names while preserving internal unique
119
- binder identities.
120
- - Short-circuit interpreter evaluation of `Implies(False, rhs)`, so the right
121
- side is not evaluated when the antecedent is false.
122
- - Add `Max` and `Min` builtins. They take either several integers
123
- (`Max(a, b, c)`) or a single set or list of integers (`Max(s)`). Both are
124
- syntactic sugar — the multi-argument form expands to nested `Ite` and the
125
- collection form folds with `reduce` — so they work in the interpreter, TLC,
126
- and Apalache. `Max(s)`/`Min(s)` over an empty collection are undefined and
127
- raise, mirroring `CHOOSE` over an empty set; guard with
128
- `Ite(s.size == 0, default, Max(s))` when you need a fallback.
97
+ Changes since public release v0.136.1.
98
+
99
+ - Add an inductive invariant in `examples/tendermint_single_indinv.py`.
129
100
 
130
101
  ## 6. License
131
102
 
@@ -1,7 +1,10 @@
1
1
  """
2
2
  Single-height Tendermint consensus protocol in Wunderspec.
3
3
 
4
+ The original Tendermint consensus appears in: https://arxiv.org/abs/1807.04938.
5
+
4
6
  The first version was translated by Codex GPT 5.5 from TLA+ specification:
7
+
5
8
  https://github.com/cometbft/cometbft/blob/main/spec/light-client/accountability/TendermintAcc_004_draft.tla
6
9
 
7
10
  We made additional specification extensions:
@@ -11,6 +14,9 @@ We made additional specification extensions:
11
14
  - Add `faulty_step` to model the messages by the faulty replicas
12
15
  - Add `correct_step` to model the behavior by the correct replicas only
13
16
 
17
+ As in the original specification, timeouts are in general omitted, except when
18
+ they might affect safety.
19
+
14
20
  Igor Konnov, 2026
15
21
  """
16
22
 
@@ -29,7 +35,6 @@ from wunderspec import (
29
35
  Implies,
30
36
  Interval,
31
37
  Map,
32
- Or,
33
38
  Param,
34
39
  Set,
35
40
  SetIf,
@@ -153,9 +158,6 @@ class TendermintAccState(MachineStateBase):
153
158
  msgs_propose: StateVar[dict[Round, set[ProposalMsg]]]
154
159
  msgs_prevote: StateVar[dict[Round, set[VoteMsg]]]
155
160
  msgs_precommit: StateVar[dict[Round, set[VoteMsg]]]
156
- evidence_propose: StateVar[set[ProposalMsg]]
157
- evidence_prevote: StateVar[set[VoteMsg]]
158
- evidence_precommit: StateVar[set[VoteMsg]]
159
161
  last_action: StateVar[str]
160
162
 
161
163
 
@@ -251,7 +253,10 @@ def all_faulty_precommits(s: TendermintAccState) -> Expr:
251
253
 
252
254
  @action(init=True)
253
255
  def init(c: Context[TendermintAccState]):
254
- """Initial state with no messages from faulty processes."""
256
+ """
257
+ Algorithm 1, lines 1-9, adapted to one height.
258
+ This initializer starts with empty message logs for faulty processes.
259
+ """
255
260
  s = c.state
256
261
  s.round = Map(Val(0) for _ in s.Corr)
257
262
  s.step = Map(Val(Step.PROPOSE) for _ in s.Corr)
@@ -260,18 +265,18 @@ def init(c: Context[TendermintAccState]):
260
265
  s.locked_round = Map(Val(NIL_ROUND) for _ in s.Corr)
261
266
  s.valid_value = Map(Val(NIL_VALUE) for _ in s.Corr)
262
267
  s.valid_round = Map(Val(NIL_ROUND) for _ in s.Corr)
263
- s.msgs_propose = Map(Set(ProposalMsg) for rnd in rounds(s))
264
- s.msgs_prevote = Map(Set(VoteMsg) for rnd in rounds(s))
265
- s.msgs_precommit = Map(Set(VoteMsg) for rnd in rounds(s))
266
- s.evidence_propose = Set(ProposalMsg)
267
- s.evidence_prevote = Set(VoteMsg)
268
- s.evidence_precommit = Set(VoteMsg)
268
+ s.msgs_propose = Map(Set(ProposalMsg) for _ in rounds(s))
269
+ s.msgs_prevote = Map(Set(VoteMsg) for _ in rounds(s))
270
+ s.msgs_precommit = Map(Set(VoteMsg) for _ in rounds(s))
269
271
  s.last_action = Val(ACTION_INIT)
270
272
 
271
273
 
272
274
  @action(init=True)
273
275
  def init_with_faults(c: Context[TendermintAccState]):
274
- """Initial state that may already contain messages from faulty processes."""
276
+ """
277
+ Algorithm 1, lines 1-9, adapted to one height.
278
+ Extension: the initial message logs may contain arbitrary faulty messages.
279
+ """
275
280
  s = c.state
276
281
  with (
277
282
  c.one_of(AllSubsets(all_faulty_proposals(s)), "faulty_proposals") as fprop,
@@ -294,9 +299,6 @@ def init_with_faults(c: Context[TendermintAccState]):
294
299
  s.msgs_precommit = Map(
295
300
  fcommit.filter(lambda m: m.round == rnd) for rnd in rounds(s)
296
301
  )
297
- s.evidence_propose = Set(ProposalMsg)
298
- s.evidence_prevote = Set(VoteMsg)
299
- s.evidence_precommit = Set(VoteMsg)
300
302
  s.last_action = Val(ACTION_INIT)
301
303
 
302
304
 
@@ -333,7 +335,12 @@ def broadcast_precommit(
333
335
 
334
336
  @action(inline=True)
335
337
  def start_round(c: Context[TendermintAccState], p: Expr, rnd: Expr):
336
- """Move a non-decided process to the propose step of a new round."""
338
+ """
339
+ 10: upon start, enter StartRound(0)
340
+ 11: StartRound(round)
341
+ 12: round_p <- round
342
+ 13: step_p <- propose
343
+ """
337
344
  s = c.state
338
345
  c.assume(s.step[p] != Step.DECIDED)
339
346
  s.round[p] = rnd
@@ -342,7 +349,16 @@ def start_round(c: Context[TendermintAccState], p: Expr, rnd: Expr):
342
349
 
343
350
  @action(inline=False)
344
351
  def insert_proposal(c: Context[TendermintAccState], p: Expr):
345
- """Proposer broadcasts a valid proposal for its current round."""
352
+ """
353
+ 14: if proposer(h_p, round_p) = p then
354
+ 15: if validValue_p != nil then
355
+ 16: proposal <- validValue_p
356
+ 17: else:
357
+ 18: proposal <- getValue()
358
+ 19: broadcast <PROPOSAL, h_p, round_p, proposal, validRound_p>
359
+ 20: else:
360
+ 21: schedule OnTimeoutPropose(h_p, round_p) ...
361
+ """
346
362
  s = c.state
347
363
  rnd = c.cache(s.round[p])
348
364
  c.assume(p == s.Proposer[rnd])
@@ -356,14 +372,20 @@ def insert_proposal(c: Context[TendermintAccState], p: Expr):
356
372
 
357
373
  @action(inline=False)
358
374
  def upon_proposal_in_propose(c: Context[TendermintAccState], p: Expr):
359
- """Handle a proposal with nil valid round while in the propose step."""
375
+ """
376
+ 22: upon proposal (v, -1) from proposer while step_p = propose
377
+ 23: if valid(v) and (lockedRound_p = -1 or lockedValue_p = v) then
378
+ 24: broadcast PREVOTE for v
379
+ 25: else
380
+ 26: broadcast PREVOTE for nil
381
+ 27: step_p <- prevote
382
+ """
360
383
  s = c.state
361
384
  rnd = c.cache(s.round[p])
362
385
  c.assume(s.step[p] == Step.PROPOSE)
363
386
  with c.one_of(values(s), "v") as v:
364
387
  msg = mk_proposal(s.Proposer[rnd], rnd, v, Val(NIL_ROUND))
365
388
  c.assume(s.msgs_propose[rnd].contains(msg))
366
- s.evidence_propose |= Set(msg)
367
389
  vote_id = v.if_(
368
390
  is_valid(s, v)
369
391
  & ((s.locked_round[p] == NIL_ROUND) | (s.locked_value[p] == v))
@@ -375,7 +397,15 @@ def upon_proposal_in_propose(c: Context[TendermintAccState], p: Expr):
375
397
 
376
398
  @action(inline=False)
377
399
  def upon_proposal_in_propose_and_prevote(c: Context[TendermintAccState], p: Expr):
378
- """Handle a proposal justified by a previous-round prevote quorum."""
400
+ """
401
+ 28: upon proposal (v, vr) from proposer and 2f+1 prevotes for v in vr
402
+ while step_p = propose and 0 <= vr < round_p
403
+ 29: if valid(v) and (lockedRound_p <= vr or lockedValue_p = v) then
404
+ 30: broadcast PREVOTE for v
405
+ 31: else
406
+ 32: broadcast PREVOTE for nil
407
+ 33: step_p <- prevote
408
+ """
379
409
  s = c.state
380
410
  rnd = c.cache(s.round[p])
381
411
  c.assume(s.step[p] == Step.PROPOSE)
@@ -386,8 +416,6 @@ def upon_proposal_in_propose_and_prevote(c: Context[TendermintAccState], p: Expr
386
416
  prevotes = c.cache(s.msgs_prevote[vr].filter(lambda m: m.id == v))
387
417
  c.assume(s.msgs_propose[rnd].contains(msg))
388
418
  c.assume(prevotes.size >= threshold2(s))
389
- s.evidence_propose |= Set(msg)
390
- s.evidence_prevote |= prevotes
391
419
  vote_id = v.if_(
392
420
  is_valid(s, v) & ((s.locked_round[p] <= vr) | (s.locked_value[p] == v))
393
421
  ).else_(NIL_VALUE)
@@ -398,13 +426,18 @@ def upon_proposal_in_propose_and_prevote(c: Context[TendermintAccState], p: Expr
398
426
 
399
427
  @action(inline=False)
400
428
  def upon_quorum_of_prevotes_any(c: Context[TendermintAccState], p: Expr):
401
- """Precommit nil after observing a current-round prevote quorum."""
429
+ """
430
+ 34: upon 2f+1 current-round prevotes while step_p = prevote
431
+ 35: schedule OnTimeoutPrevote(h_p, round_p)
432
+
433
+ This safety model does not store timers. The transition represents the
434
+ timeout firing and immediately takes the nil-precommit path.
435
+ """
402
436
  s = c.state
403
437
  rnd = c.cache(s.round[p])
404
438
  c.assume(s.step[p] == Step.PREVOTE)
405
439
  with c.one_of(AllSubsets(s.msgs_prevote[rnd]), "my_evidence") as ev:
406
440
  c.assume(senders(s, ev).size >= threshold2(s))
407
- s.evidence_prevote |= ev
408
441
  broadcast_precommit(c, p, rnd, Val(NIL_VALUE))
409
442
  s.step[p] = Val(Step.PRECOMMIT)
410
443
  s.last_action = Val(ACTION_UPON_QUORUM_OF_PREVOTES_ANY)
@@ -414,7 +447,17 @@ def upon_quorum_of_prevotes_any(c: Context[TendermintAccState], p: Expr):
414
447
  def upon_proposal_in_prevote_or_commit_and_prevote(
415
448
  c: Context[TendermintAccState], p: Expr
416
449
  ):
417
- """Lock and precommit a valid proposal supported by a prevote quorum."""
450
+ """
451
+ 36: upon proposal (v, *) and 2f+1 current-round prevotes for v
452
+ while valid(v) and step_p >= prevote
453
+ 37: if step_p = prevote then
454
+ 38: lockedValue_p <- v
455
+ 39: lockedRound_p <- round_p
456
+ 40: broadcast PRECOMMIT for v
457
+ 41: step_p <- precommit
458
+ 42: validValue_p <- v
459
+ 43: validRound_p <- round_p
460
+ """
418
461
  s = c.state
419
462
  rnd = c.cache(s.round[p])
420
463
  c.assume((s.step[p] == Step.PREVOTE) | (s.step[p] == Step.PRECOMMIT))
@@ -423,8 +466,6 @@ def upon_proposal_in_prevote_or_commit_and_prevote(
423
466
  prevotes = c.cache(s.msgs_prevote[rnd].filter(lambda m: m.id == v))
424
467
  c.assume(s.msgs_propose[rnd].contains(msg))
425
468
  c.assume(prevotes.size >= threshold2(s))
426
- s.evidence_propose |= Set(msg)
427
- s.evidence_prevote |= prevotes
428
469
  prevote_step, precommit_step = c.split(s.step[p] == Step.PREVOTE)
429
470
  with prevote_step:
430
471
  s.locked_value[p] = v
@@ -440,20 +481,34 @@ def upon_proposal_in_prevote_or_commit_and_prevote(
440
481
 
441
482
  @action(inline=False)
442
483
  def upon_quorum_of_precommits_any(c: Context[TendermintAccState], p: Expr):
443
- """Advance to the next round after observing a precommit quorum."""
484
+ """
485
+ 47: upon 2f+1 current-round precommits
486
+ 48: schedule OnTimeoutPrecommit(h_p, round_p)
487
+
488
+ This safety model does not store timers. The transition represents the
489
+ timeout firing and immediately advances to the next modeled round.
490
+ """
444
491
  s = c.state
445
492
  rnd = c.cache(s.round[p])
446
493
  with c.one_of(AllSubsets(s.msgs_precommit[rnd]), "my_evidence") as ev:
447
494
  c.assume(senders(s, ev).size >= threshold2(s))
448
495
  c.assume(rounds(s).contains(rnd + 1))
449
- s.evidence_precommit |= ev
450
496
  start_round(c, p, rnd + 1)
451
497
  s.last_action = Val(ACTION_UPON_QUORUM_OF_PRECOMMITS_ANY)
452
498
 
453
499
 
454
500
  @action(inline=False)
455
501
  def upon_proposal_in_precommit_no_decision(c: Context[TendermintAccState], p: Expr):
456
- """Decide a value after observing a valid proposal and precommit quorum."""
502
+ """
503
+ 49: upon proposal (v, *) and 2f+1 precommits for v while undecided
504
+ 50: if valid(v) then
505
+ 51: decision_p[h_p] <- v
506
+ 52: h_p <- h_p + 1
507
+ 53: reset locks, valid value, valid round, and message log
508
+ 54: StartRound(0)
509
+
510
+ This one-height model records the decision and moves the process to DECIDED.
511
+ """
457
512
  s = c.state
458
513
  c.assume(s.decision[p] == NIL_VALUE)
459
514
  with (
@@ -465,8 +520,6 @@ def upon_proposal_in_precommit_no_decision(c: Context[TendermintAccState], p: Ex
465
520
  precommits = c.cache(s.msgs_precommit[rnd].filter(lambda m: m.id == v))
466
521
  c.assume(s.msgs_propose[rnd].contains(msg))
467
522
  c.assume(precommits.size >= threshold2(s))
468
- s.evidence_precommit |= precommits
469
- s.evidence_propose |= Set(msg)
470
523
  s.decision[p] = v
471
524
  s.step[p] = Val(Step.DECIDED)
472
525
  s.last_action = Val(ACTION_UPON_PROPOSAL_IN_PRECOMMIT_NO_DECISION)
@@ -474,7 +527,12 @@ def upon_proposal_in_precommit_no_decision(c: Context[TendermintAccState], p: Ex
474
527
 
475
528
  @action(inline=False)
476
529
  def on_timeout_propose(c: Context[TendermintAccState], p: Expr):
477
- """Model a propose timeout by prevoting nil for a non-proposer."""
530
+ """
531
+ 57: OnTimeoutPropose(height, round)
532
+ 58: if height = h_p and round = round_p and step_p = propose then
533
+ 59: broadcast PREVOTE for nil
534
+ 60: step_p <- prevote
535
+ """
478
536
  s = c.state
479
537
  rnd = c.cache(s.round[p])
480
538
  c.assume(s.step[p] == Step.PROPOSE)
@@ -486,13 +544,16 @@ def on_timeout_propose(c: Context[TendermintAccState], p: Expr):
486
544
 
487
545
  @action(inline=False)
488
546
  def on_quorum_of_nil_prevotes(c: Context[TendermintAccState], p: Expr):
489
- """Precommit nil after observing a quorum of nil prevotes."""
547
+ """
548
+ 44: upon 2f+1 current-round PREVOTE nil messages while step_p = prevote
549
+ 45: broadcast PRECOMMIT for nil
550
+ 46: step_p <- precommit
551
+ """
490
552
  s = c.state
491
553
  rnd = c.cache(s.round[p])
492
554
  c.assume(s.step[p] == Step.PREVOTE)
493
555
  prevotes = c.cache(s.msgs_prevote[rnd].filter(lambda m: m.id == NIL_VALUE))
494
556
  c.assume(prevotes.size >= threshold2(s))
495
- s.evidence_prevote |= prevotes
496
557
  broadcast_precommit(c, p, rnd, Val(NIL_VALUE))
497
558
  s.step[p] = Val(Step.PRECOMMIT)
498
559
  s.last_action = Val(ACTION_ON_QUORUM_OF_NIL_PREVOTES)
@@ -500,7 +561,10 @@ def on_quorum_of_nil_prevotes(c: Context[TendermintAccState], p: Expr):
500
561
 
501
562
  @action(inline=False)
502
563
  def on_round_catchup(c: Context[TendermintAccState], p: Expr):
503
- """Catch up to a higher round after seeing enough faster-process evidence."""
564
+ """
565
+ 55: upon f+1 messages from a higher round
566
+ 56: StartRound(round)
567
+ """
504
568
  s = c.state
505
569
  with (
506
570
  c.one_of(rounds(s), "rnd") as rnd,
@@ -513,9 +577,6 @@ def on_round_catchup(c: Context[TendermintAccState], p: Expr):
513
577
  senders(s, ev_propose) | senders(s, ev_prevote) | senders(s, ev_precommit)
514
578
  )
515
579
  c.assume(faster.size >= threshold1(s))
516
- s.evidence_propose |= ev_propose
517
- s.evidence_prevote |= ev_prevote
518
- s.evidence_precommit |= ev_precommit
519
580
  start_round(c, p, rnd)
520
581
  s.last_action = Val(ACTION_ON_ROUND_CATCHUP)
521
582
 
@@ -642,38 +703,6 @@ def equivocation_in(msgs: Expr, p: Expr) -> Expr:
642
703
  )
643
704
 
644
705
 
645
- def equivocation_by(s: TendermintAccState, p: Expr) -> Expr:
646
- """Whether process p equivocates in any collected evidence set."""
647
- return Or(
648
- equivocation_in(s.evidence_propose, p),
649
- equivocation_in(s.evidence_prevote, p),
650
- equivocation_in(s.evidence_precommit, p),
651
- )
652
-
653
-
654
- def amnesia_by(s: TendermintAccState, p: Expr) -> Expr:
655
- """Whether process p shows amnesia across conflicting locks and votes."""
656
- return Exists(
657
- And(
658
- r1 < r2,
659
- v1 != v2,
660
- s.evidence_precommit.contains(mk_precommit(p, r1, v1)),
661
- s.evidence_prevote.contains(mk_prevote(p, r2, v2)),
662
- Forall(
663
- s.evidence_prevote.filter(
664
- lambda m: (m.round == rnd) & (m.id == v2)
665
- ).size
666
- < threshold2(s)
667
- for rnd in rounds(s).filter(lambda r: (r1 <= r) & (r < r2))
668
- ),
669
- )
670
- for r1 in rounds(s)
671
- for r2 in rounds(s)
672
- for v1 in s.ValidValues
673
- for v2 in s.ValidValues
674
- )
675
-
676
-
677
706
  @invariant
678
707
  def assumptions_hold(s: TendermintAccState) -> BoolExpr:
679
708
  """Model assumptions needed for the bounded accountability instance."""
@@ -714,18 +743,6 @@ def type_ok(s: TendermintAccState) -> BoolExpr:
714
743
  )
715
744
  for rnd in rounds(s)
716
745
  ),
717
- Forall(
718
- proposal_ok(s, m, m.round) & rounds(s).contains(m.round)
719
- for m in s.evidence_propose
720
- ),
721
- Forall(
722
- vote_ok(s, m, m.round, VoteKind.PREVOTE) & rounds(s).contains(m.round)
723
- for m in s.evidence_prevote
724
- ),
725
- Forall(
726
- vote_ok(s, m, m.round, VoteKind.PRECOMMIT) & rounds(s).contains(m.round)
727
- for m in s.evidence_precommit
728
- ),
729
746
  action_names().contains(s.last_action),
730
747
  )
731
748
 
@@ -745,30 +762,6 @@ def validity(s: TendermintAccState) -> BoolExpr:
745
762
  )
746
763
 
747
764
 
748
- @invariant
749
- def accountability(s: TendermintAccState) -> BoolExpr:
750
- """Either agreement holds or enough faulty processes expose evidence."""
751
- return agreement_expr(s) | Exists(
752
- And(
753
- detectable.size >= threshold1(s),
754
- Forall(equivocation_by(s, p) | amnesia_by(s, p) for p in detectable),
755
- )
756
- for detectable in AllSubsets(s.Faulty)
757
- ) # type: ignore
758
-
759
-
760
- @invariant
761
- def no_amnesia(s: TendermintAccState) -> BoolExpr:
762
- """False invariant used to produce traces with amnesic faulty behavior."""
763
- return Forall(~amnesia_by(s, p) for p in s.Faulty)
764
-
765
-
766
- @invariant
767
- def no_equivocation(s: TendermintAccState) -> BoolExpr:
768
- """False invariant used to produce traces with equivocation evidence."""
769
- return Forall(~equivocation_by(s, p) for p in s.Faulty)
770
-
771
-
772
765
  @example
773
766
  def no_agreement(s: TendermintAccState) -> BoolExpr:
774
767
  """Example of violating agreement, e.g. for N=4,T=1,F=2."""
@@ -782,30 +775,6 @@ def no_agreement(s: TendermintAccState) -> BoolExpr:
782
775
  )
783
776
 
784
777
 
785
- @example
786
- def agreement_or_amnesia(s: TendermintAccState) -> BoolExpr:
787
- """Counterexample should show disagreement not explained by amnesia alone."""
788
- return agreement_expr(s) | Forall(amnesia_by(s, p) for p in s.Faulty) # type: ignore
789
-
790
-
791
- @example
792
- def show_me_amnesia_without_equivocation(s: TendermintAccState) -> BoolExpr:
793
- """Example search for amnesia by a faulty process without equivocation."""
794
- return Implies(
795
- ~agreement_expr(s) & Exists(~equivocation_by(s, p) for p in s.Faulty),
796
- Forall(~amnesia_by(s, p) for p in s.Faulty),
797
- )
798
-
799
-
800
- @invariant
801
- def amnesia_implies_equivocation(s: TendermintAccState) -> BoolExpr:
802
- """False invariant: amnesia need not imply equivocation."""
803
- return Implies(
804
- Exists(amnesia_by(s, p) for p in s.Faulty),
805
- Exists(equivocation_by(s, q) for q in s.Faulty),
806
- )
807
-
808
-
809
778
  @example
810
779
  def never_undecided_in_max_round(s: TendermintAccState) -> BoolExpr:
811
780
  """Example where all correct processes reach MaxRound but not all decide."""
@@ -841,9 +810,6 @@ def state_cov(s: TendermintAccState) -> Expr:
841
810
  s.msgs_propose,
842
811
  s.msgs_prevote,
843
812
  s.msgs_precommit,
844
- s.evidence_propose,
845
- s.evidence_prevote,
846
- s.evidence_precommit,
847
813
  )
848
814
 
849
815