<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <generator uri="https://www.11ty.dev/">Eleventy</generator>
  <link href="https://protocols-made-fun.com/feed.xml?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" rel="self" type="application/atom+xml" />
  <link href="https://protocols-made-fun.com/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" rel="alternate" type="text/html" />
  <updated>2026-06-27T09:31:46.391Z</updated>
  <id>https://protocols-made-fun.com/feed.xml</id>
  <title type="html">Protocols Made Fun</title>
  <subtitle>All things about protocol specification, testing, and verification. Creative Commons Attribution 4.0 International License.</subtitle>
  <author>
    <name>Igor Konnov</name>
    <email>igor@konnov.phd</email>
    <uri>https://konnov.phd/?utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site</uri>
  </author>
  
  
  
  <entry>
    <title type="html">Formal proofs for distributed protocols with AI may be closer than you think</title>
    <link href="https://protocols-made-fun.com/proofs-are-closer.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" rel="alternate" type="text/html" title="Formal proofs for distributed protocols with AI may be closer than you think" />
    <published>2026-06-26T00:00:00.000Z</published>
    <updated>2026-06-26T00:00:00.000Z</updated>
    <id>https://protocols-made-fun.com/proofs-are-closer.html</id>
    <content type="html" xml:base="https://protocols-made-fun.com/proofs-are-closer.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"><![CDATA[<p><em>This text is artisanally typed using Das Keyboard, with occasional suggestions
by Copilot (most of them ignored anyways). The figures are generated with
ChatGPT 5.5.</em></p>
<p>In November 2024, I wrote a <a href="https://protocols-made-fun.com/ben-or.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">blog post</a> about checking safety
of the Ben-Or consensus protocol using TLA<sup>+</sup> and <a href="https://apalache-mc.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache</a>. The
<a href="https://protocols-made-fun.com/ben-or.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#5.-checking-unbounded-executions-via-an-inductive-invariant">last section</a> of the blog post introduces an inductive
invariant <code>IndInv</code> that is used to prove protocol safety for executions of
arbitrary length. Two things are important to note about this inductive
invariant:</p>
<ol>
<li>
<p>Back then, it took me about <strong>two days</strong> to come with the lemmas
iteratively. I was fixing the lemmas after counterexamples shown by the model
checker. After that, it took the model checker about <strong>nine days</strong> to check
these lemmas. Hence, the most of the time went into <strong>computing</strong>. If this
reminds you of what we have with LLMs now, yes, this is a similar picture.
Most of the the time went into computing, not into thinking.</p>
</li>
<li>
<p>Apalache checked the inductive invariant for two <strong>fixed configurations</strong> of
<span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="6.157ex" height="1.731ex" role="img" focusable="false" viewBox="0 -683 2721.6 765"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D441" d="M234 637Q231 637 226 637Q201 637 196 638T191 649Q191 676 202 682Q204 683 299 683Q376 683 387 683T401 677Q612 181 616 168L670 381Q723 592 723 606Q723 633 659 637Q635 637 635 648Q635 650 637 660Q641 676 643 679T653 683Q656 683 684 682T767 680Q817 680 843 681T873 682Q888 682 888 672Q888 650 880 642Q878 637 858 637Q787 633 769 597L620 7Q618 0 599 0Q585 0 582 2Q579 5 453 305L326 604L261 344Q196 88 196 79Q201 46 268 46H278Q284 41 284 38T282 19Q278 6 272 0H259Q228 2 151 2Q123 2 100 2T63 2T46 1Q31 1 31 10Q31 14 34 26T39 40Q41 46 62 46Q130 49 150 85Q154 91 221 362L289 634Q287 635 234 637Z"></path></g><g data-mml-node="mo" transform="translate(1165.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mn" transform="translate(2221.6,0)"><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z"></path></g></g></g></svg></mjx-container></span>, <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="5.741ex" height="1.717ex" role="img" focusable="false" viewBox="0 -677 2537.6 759"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D447" d="M40 437Q21 437 21 445Q21 450 37 501T71 602L88 651Q93 669 101 677H569H659Q691 677 697 676T704 667Q704 661 687 553T668 444Q668 437 649 437Q640 437 637 437T631 442L629 445Q629 451 635 490T641 551Q641 586 628 604T573 629Q568 630 515 631Q469 631 457 630T439 622Q438 621 368 343T298 60Q298 48 386 46Q418 46 427 45T436 36Q436 31 433 22Q429 4 424 1L422 0Q419 0 415 0Q410 0 363 1T228 2Q99 2 64 0H49Q43 6 43 9T45 27Q49 40 55 46H83H94Q174 46 189 55Q190 56 191 56Q196 59 201 76T241 233Q258 301 269 344Q339 619 339 625Q339 630 310 630H279Q212 630 191 624Q146 614 121 583T67 467Q60 445 57 441T43 437H40Z"></path></g><g data-mml-node="mo" transform="translate(981.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mn" transform="translate(2037.6,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g></svg></mjx-container></span>, <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="9.991ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 4416.2 1000"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D439" d="M48 1Q31 1 31 11Q31 13 34 25Q38 41 42 43T65 46Q92 46 125 49Q139 52 144 61Q146 66 215 342T285 622Q285 629 281 629Q273 632 228 634H197Q191 640 191 642T193 659Q197 676 203 680H742Q749 676 749 669Q749 664 736 557T722 447Q720 440 702 440H690Q683 445 683 453Q683 454 686 477T689 530Q689 560 682 579T663 610T626 626T575 633T503 634H480Q398 633 393 631Q388 629 386 623Q385 622 352 492L320 363H375Q378 363 398 363T426 364T448 367T472 374T489 386Q502 398 511 419T524 457T529 475Q532 480 548 480H560Q567 475 567 470Q567 467 536 339T502 207Q500 200 482 200H470Q463 206 463 212Q463 215 468 234T473 274Q473 303 453 310T364 317H309L277 190Q245 66 245 60Q245 46 334 46H359Q365 40 365 39T363 19Q359 6 353 0H336Q295 2 185 2Q120 2 86 2T48 1Z"></path></g><g data-mml-node="mo" transform="translate(1026.8,0)"><path data-c="2208" d="M84 250Q84 372 166 450T360 539Q361 539 377 539T419 540T469 540H568Q583 532 583 520Q583 511 570 501L466 500Q355 499 329 494Q280 482 242 458T183 409T147 354T129 306T124 272V270H568Q583 262 583 250T568 230H124V228Q124 207 134 177T167 112T231 48T328 7Q355 1 466 0H570Q583 -10 583 -20Q583 -32 568 -40H471Q464 -40 446 -40T417 -41Q262 -41 172 45Q84 127 84 250Z"></path></g><g data-mml-node="mo" transform="translate(1971.6,0)"><path data-c="7B" d="M434 -231Q434 -244 428 -250H410Q281 -250 230 -184Q225 -177 222 -172T217 -161T213 -148T211 -133T210 -111T209 -84T209 -47T209 0Q209 21 209 53Q208 142 204 153Q203 154 203 155Q189 191 153 211T82 231Q71 231 68 234T65 250T68 266T82 269Q116 269 152 289T203 345Q208 356 208 377T209 529V579Q209 634 215 656T244 698Q270 724 324 740Q361 748 377 749Q379 749 390 749T408 750H428Q434 744 434 732Q434 719 431 716Q429 713 415 713Q362 710 332 689T296 647Q291 634 291 499V417Q291 370 288 353T271 314Q240 271 184 255L170 250L184 245Q202 239 220 230T262 196T290 137Q291 131 291 1Q291 -134 296 -147Q306 -174 339 -192T415 -213Q429 -213 431 -216Q434 -219 434 -231Z"></path></g><g data-mml-node="mn" transform="translate(2471.6,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g><g data-mml-node="mo" transform="translate(2971.6,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="mn" transform="translate(3416.2,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g><g data-mml-node="mo" transform="translate(3916.2,0)"><path data-c="7D" d="M65 731Q65 745 68 747T88 750Q171 750 216 725T279 670Q288 649 289 635T291 501Q292 362 293 357Q306 312 345 291T417 269Q428 269 431 266T434 250T431 234T417 231Q380 231 345 210T298 157Q293 143 292 121T291 -28V-79Q291 -134 285 -156T256 -198Q202 -250 89 -250Q71 -250 68 -247T65 -230Q65 -224 65 -223T66 -218T69 -214T77 -213Q91 -213 108 -210T146 -200T183 -177T207 -139Q208 -134 209 3L210 139Q223 196 280 230Q315 247 330 250Q305 257 280 270Q225 304 212 352L210 362L209 498Q208 635 207 640Q195 680 154 696T77 713Q68 713 67 716T65 731Z"></path></g></g></g></svg></mjx-container></span>, and three rounds. This is very important for the
rest of this blog post. To make the proofs complete, we have to show
inductiveness and safety for <strong>arbitrary configurations</strong> of <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.09ex;" xmlns="http://www.w3.org/2000/svg" width="9.385ex" height="1.636ex" role="img" focusable="false" viewBox="0 -683 4148 723"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D441" d="M234 637Q231 637 226 637Q201 637 196 638T191 649Q191 676 202 682Q204 683 299 683Q376 683 387 683T401 677Q612 181 616 168L670 381Q723 592 723 606Q723 633 659 637Q635 637 635 648Q635 650 637 660Q641 676 643 679T653 683Q656 683 684 682T767 680Q817 680 843 681T873 682Q888 682 888 672Q888 650 880 642Q878 637 858 637Q787 633 769 597L620 7Q618 0 599 0Q585 0 582 2Q579 5 453 305L326 604L261 344Q196 88 196 79Q201 46 268 46H278Q284 41 284 38T282 19Q278 6 272 0H259Q228 2 151 2Q123 2 100 2T63 2T46 1Q31 1 31 10Q31 14 34 26T39 40Q41 46 62 46Q130 49 150 85Q154 91 221 362L289 634Q287 635 234 637Z"></path></g><g data-mml-node="mo" transform="translate(1165.8,0)"><path data-c="3E" d="M84 520Q84 528 88 533T96 539L99 540Q106 540 253 471T544 334L687 265Q694 260 694 250T687 235Q685 233 395 96L107 -40H101Q83 -38 83 -20Q83 -19 83 -17Q82 -10 98 -1Q117 9 248 71Q326 108 378 132L626 250L378 368Q90 504 86 509Q84 513 84 520Z"></path></g><g data-mml-node="mn" transform="translate(2221.6,0)"><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z"></path></g><g data-mml-node="mo" transform="translate(2943.8,0)"><path data-c="22C5" d="M78 250Q78 274 95 292T138 310Q162 310 180 294T199 251Q199 226 182 208T139 190T96 207T78 250Z"></path></g><g data-mml-node="mi" transform="translate(3444,0)"><path data-c="1D447" d="M40 437Q21 437 21 445Q21 450 37 501T71 602L88 651Q93 669 101 677H569H659Q691 677 697 676T704 667Q704 661 687 553T668 444Q668 437 649 437Q640 437 637 437T631 442L629 445Q629 451 635 490T641 551Q641 586 628 604T573 629Q568 630 515 631Q469 631 457 630T439 622Q438 621 368 343T298 60Q298 48 386 46Q418 46 427 45T436 36Q436 31 433 22Q429 4 424 1L422 0Q419 0 415 0Q410 0 363 1T228 2Q99 2 64 0H49Q43 6 43 9T45 27Q49 40 55 46H83H94Q174 46 189 55Q190 56 191 56Q196 59 201 76T241 233Q258 301 269 344Q339 619 339 625Q339 630 310 630H279Q212 630 191 624Q146 614 121 583T67 467Q60 445 57 441T43 437H40Z"></path></g></g></g></svg></mjx-container></span> and
<span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.312ex;" xmlns="http://www.w3.org/2000/svg" width="10.453ex" height="1.851ex" role="img" focusable="false" viewBox="0 -680 4620.1 818"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D447" d="M40 437Q21 437 21 445Q21 450 37 501T71 602L88 651Q93 669 101 677H569H659Q691 677 697 676T704 667Q704 661 687 553T668 444Q668 437 649 437Q640 437 637 437T631 442L629 445Q629 451 635 490T641 551Q641 586 628 604T573 629Q568 630 515 631Q469 631 457 630T439 622Q438 621 368 343T298 60Q298 48 386 46Q418 46 427 45T436 36Q436 31 433 22Q429 4 424 1L422 0Q419 0 415 0Q410 0 363 1T228 2Q99 2 64 0H49Q43 6 43 9T45 27Q49 40 55 46H83H94Q174 46 189 55Q190 56 191 56Q196 59 201 76T241 233Q258 301 269 344Q339 619 339 625Q339 630 310 630H279Q212 630 191 624Q146 614 121 583T67 467Q60 445 57 441T43 437H40Z"></path></g><g data-mml-node="mo" transform="translate(981.8,0)"><path data-c="2265" d="M83 616Q83 624 89 630T99 636Q107 636 253 568T543 431T687 361Q694 356 694 346T687 331Q685 329 395 192L107 56H101Q83 58 83 76Q83 77 83 79Q82 86 98 95Q117 105 248 167Q326 204 378 228L626 346L360 472Q291 505 200 548Q112 589 98 597T83 616ZM84 -118Q84 -108 99 -98H678Q694 -104 694 -118Q694 -130 679 -138H98Q84 -131 84 -118Z"></path></g><g data-mml-node="mi" transform="translate(2037.6,0)"><path data-c="1D439" d="M48 1Q31 1 31 11Q31 13 34 25Q38 41 42 43T65 46Q92 46 125 49Q139 52 144 61Q146 66 215 342T285 622Q285 629 281 629Q273 632 228 634H197Q191 640 191 642T193 659Q197 676 203 680H742Q749 676 749 669Q749 664 736 557T722 447Q720 440 702 440H690Q683 445 683 453Q683 454 686 477T689 530Q689 560 682 579T663 610T626 626T575 633T503 634H480Q398 633 393 631Q388 629 386 623Q385 622 352 492L320 363H375Q378 363 398 363T426 364T448 367T472 374T489 386Q502 398 511 419T524 457T529 475Q532 480 548 480H560Q567 475 567 470Q567 467 536 339T502 207Q500 200 482 200H470Q463 206 463 212Q463 215 468 234T473 274Q473 303 453 310T364 317H309L277 190Q245 66 245 60Q245 46 334 46H359Q365 40 365 39T363 19Q359 6 353 0H336Q295 2 185 2Q120 2 86 2T48 1Z"></path></g><g data-mml-node="mo" transform="translate(3064.3,0)"><path data-c="2265" d="M83 616Q83 624 89 630T99 636Q107 636 253 568T543 431T687 361Q694 356 694 346T687 331Q685 329 395 192L107 56H101Q83 58 83 76Q83 77 83 79Q82 86 98 95Q117 105 248 167Q326 204 378 228L626 346L360 472Q291 505 200 548Q112 589 98 597T83 616ZM84 -118Q84 -108 99 -98H678Q694 -104 694 -118Q694 -130 679 -138H98Q84 -131 84 -118Z"></path></g><g data-mml-node="mn" transform="translate(4120.1,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g></g></g></svg></mjx-container></span>, as well as for <strong>arbitrary number of rounds</strong>.</p>
</li>
</ol>
<p>This very example is used in the tool paper on <em>The TLA+ Model Checker Apalache</em>
that is going to be presented at <a href="https://conferences.i-cav.org/2026/accepted/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Computer-Aided Verification 2026</a>.
As soon as the paper is published, I will update this blog post with a link to
it.</p>
<p>Since then, I challenged several professors to write a complete proof of safety
for the Ben-Or protocol in Lean or TLAPS. The proof structure is already there,
and the lemmas are already known. However, the proof economics did not work, as
it would take a few months to write the proof by hand, which would lock a
student or an intern for a long time. I've heard similar objections from the
customers. Nobody wanted to commit to a long-term project, to get formal proofs
(for more complex protocols than Ben-Or).</p>
<p>Now, as of June 2026, <strong>the economics of formal proofs has changed</strong>. I ran
<em>Codex GPT 5.5 xhigh/high</em> and <em>Claude 4.8 Opus xhigh/high</em> to write the
complete proofs of inductiveness and safety of the Ben-Or protocol, both in Lean
4 and TLAPS. In both cases, the proofs took <strong>about 4-5 days to generate</strong>. At
some point, both tools were stuck, so I had to help them. Also, one of the tools
cheated in the proofs. Both in cases of Lean and TLAPS, the tools burned most of
my weekly subscriptions. It is important to note that in both cases, <strong>the tools
were given the inductive invariant</strong> in TLA<sup>+</sup> as a starting point. So
<strong>they had the core proof argument and did not have to invent it</strong>.</p>
<p>Just to be clear about the time figure, Ben-Or's consensus is the core
algorithm. Practical implementations contain 5-10 more protocols on top of the
core consensus such as p2p, write-ahead log, etc. Hence, a <strong>practical consensus
would take more time to prove</strong>.</p>
<p>At some point, it became clear that the tools had a trouble proving the
inductiveness. There was a good reason for that! Lemma 8 worked for <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="5.741ex" height="1.717ex" role="img" focusable="false" viewBox="0 -677 2537.6 759"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D447" d="M40 437Q21 437 21 445Q21 450 37 501T71 602L88 651Q93 669 101 677H569H659Q691 677 697 676T704 667Q704 661 687 553T668 444Q668 437 649 437Q640 437 637 437T631 442L629 445Q629 451 635 490T641 551Q641 586 628 604T573 629Q568 630 515 631Q469 631 457 630T439 622Q438 621 368 343T298 60Q298 48 386 46Q418 46 427 45T436 36Q436 31 433 22Q429 4 424 1L422 0Q419 0 415 0Q410 0 363 1T228 2Q99 2 64 0H49Q43 6 43 9T45 27Q49 40 55 46H83H94Q174 46 189 55Q190 56 191 56Q196 59 201 76T241 233Q258 301 269 344Q339 619 339 625Q339 630 310 630H279Q212 630 191 624Q146 614 121 583T67 467Q60 445 57 441T43 437H40Z"></path></g><g data-mml-node="mo" transform="translate(981.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mn" transform="translate(2037.6,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g></svg></mjx-container></span>, but
it did not hold true for <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.09ex;" xmlns="http://www.w3.org/2000/svg" width="5.741ex" height="1.622ex" role="img" focusable="false" viewBox="0 -677 2537.6 717"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D447" d="M40 437Q21 437 21 445Q21 450 37 501T71 602L88 651Q93 669 101 677H569H659Q691 677 697 676T704 667Q704 661 687 553T668 444Q668 437 649 437Q640 437 637 437T631 442L629 445Q629 451 635 490T641 551Q641 586 628 604T573 629Q568 630 515 631Q469 631 457 630T439 622Q438 621 368 343T298 60Q298 48 386 46Q418 46 427 45T436 36Q436 31 433 22Q429 4 424 1L422 0Q419 0 415 0Q410 0 363 1T228 2Q99 2 64 0H49Q43 6 43 9T45 27Q49 40 55 46H83H94Q174 46 189 55Q190 56 191 56Q196 59 201 76T241 233Q258 301 269 344Q339 619 339 625Q339 630 310 630H279Q212 630 191 624Q146 614 121 583T67 467Q60 445 57 441T43 437H40Z"></path></g><g data-mml-node="mo" transform="translate(981.8,0)"><path data-c="3E" d="M84 520Q84 528 88 533T96 539L99 540Q106 540 253 471T544 334L687 265Q694 260 694 250T687 235Q685 233 395 96L107 -40H101Q83 -38 83 -20Q83 -19 83 -17Q82 -10 98 -1Q117 9 248 71Q326 108 378 132L626 250L378 368Q90 504 86 509Q84 513 84 520Z"></path></g><g data-mml-node="mn" transform="translate(2037.6,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g></svg></mjx-container></span>. Since we checked inductiveness for <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="5.741ex" height="1.717ex" role="img" focusable="false" viewBox="0 -677 2537.6 759"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D447" d="M40 437Q21 437 21 445Q21 450 37 501T71 602L88 651Q93 669 101 677H569H659Q691 677 697 676T704 667Q704 661 687 553T668 444Q668 437 649 437Q640 437 637 437T631 442L629 445Q629 451 635 490T641 551Q641 586 628 604T573 629Q568 630 515 631Q469 631 457 630T439 622Q438 621 368 343T298 60Q298 48 386 46Q418 46 427 45T436 36Q436 31 433 22Q429 4 424 1L422 0Q419 0 415 0Q410 0 363 1T228 2Q99 2 64 0H49Q43 6 43 9T45 27Q49 40 55 46H83H94Q174 46 189 55Q190 56 191 56Q196 59 201 76T241 233Q258 301 269 344Q339 619 339 625Q339 630 310 630H279Q212 630 191 624Q146 614 121 583T67 467Q60 445 57 441T43 437H40Z"></path></g><g data-mml-node="mo" transform="translate(981.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mn" transform="translate(2037.6,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g></svg></mjx-container></span>
with Apalache, it was not obvious that it does not hold true in the general
case. Keep reading, to see how the AI tools figured this out.</p>
<p>In the rest of this blog post, I will refer to both AI tools as &quot;C1&quot; and &quot;C2&quot;,
without disclosing which is which. It is one benchmark, so I don't want you to
make wrong conclusions about which one is better. They are getting updated every
few months anyways. Interestingly, the tools did not require a lot of
hand-holding, though C1 was definitely diverging at some point, producing more
and more theorems.</p>
<p><em>The big picture of our approach looks as follows</em>:</p>
<figure>
  <a href="https://protocols-made-fun.com/img/invariants-and-proofs.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
    target="_blank" title="Click to open full-size"><picture>
    <img class="responsive-img"
    src="https://protocols-made-fun.com/img/invariants-and-proofs.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
    alt="Invariants and proofs">
  </picture></a>
  <figcaption>Figure 1: Relation between specifications, invariants, and proofs.</figcaption>
</figure>
<h2 id="1.-proving-inductiveness-and-safety-with-lean-4" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/proofs-are-closer.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#1.-proving-inductiveness-and-safety-with-lean-4">1. Proving inductiveness and safety with Lean 4</a></h2>
<p>To write the Lean proofs, we start with the specification of Ben-Or protocol in
Python DSL. It is the same DSL that I mentioned in the blog post about
<a href="https://protocols-made-fun.com/zookeeper-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Zookeeper testing</a>. If you are interested, check
<a href="https://github.com/wunderspec/wunderspec/blob/main/examples/ben_or.py?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">ben_or.py</a>. This specification is automatically translated into Lean 4. See
<a href="https://github.com/konnov/leanda/blob/main/Ben-Or/BenOr/Defs.lean?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Defs.lean</a> for the generated Lean code. If you want to try this
translator, <a href="https://konnov.phd/?pmf=20260427&amp;utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site">contact me</a>.</p>
<p>Just to get the feeling of it, here is a fragment of <code>step1</code> in the DSL:</p>
<pre><code class="hljs language-python"><span class="hljs-meta">@action(<span class="hljs-params">inline=<span class="hljs-literal">False</span></span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">step1</span>(<span class="hljs-params">c: Context[BenOrState], rid: Expr</span>):
    s = c.state
    rnd = c.cache(s.<span class="hljs-built_in">round</span>[rid])
    c.assume(s.step[rid] == Step.S1)
    <span class="hljs-comment"># &quot;send the message (1, r, x_P) to all the processes&quot;</span>
    s.msgs1[rnd] |= <span class="hljs-type">Set</span>(mk_m1(rid, rnd, s.value[rid]))
    s.step[rid] = Step.S2
    s.ghost_trigger = Val(<span class="hljs-literal">False</span>)
</code></pre>
<p>Below is the generated Lean code for the same fragment:</p>
<pre><code class="hljs language-lean"><span class="hljs-keyword">def</span> step1 (rid : Int) (s s&#x27; : State) : Prop :=
  (<span class="hljs-keyword">let</span> _cache21 := Finmap.lookupD rid s.round;
    Finmap.lookupD rid s.step = Step.S1 ∧
      s&#x27;.msgs1 =
          Finmap.insert _cache21
            (Finmap.lookupD _cache21 s.msgs1 ∪
              insert (Msg1.mk _cache21 rid (Finmap.lookupD rid s.value)) (∅ : Finset Msg1))
            s.msgs1 ∧
        s&#x27;.step = Finmap.insert rid Step.S2 s.step ∧ s&#x27;.ghost_trigger = <span class="hljs-keyword">false</span>) ∧
    s&#x27;.N = s.N ∧
      s&#x27;.T = s.T ∧
        s&#x27;.F = s.F ∧
          s&#x27;.CORRECT = s.CORRECT ∧
            s&#x27;.FAULTY = s.FAULTY ∧
              s&#x27;.ROUNDS = s.ROUNDS ∧
                s&#x27;.value = s.value ∧ s&#x27;.decision = s.decision ∧ s&#x27;.round = s.round ∧ s&#x27;.msgs2 = s.msgs2
</code></pre>
<p><strong>Bootstrapping the proof.</strong> So we had all the prerequites ready. I pointed C1 to the
generated Lean code and the inductive invariant in <a href="https://github.com/konnov/apalache-examples/blob/main/ben-or83/Ben_or83_inductive.tla?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Ben-or-inductive.tla</a>. The
goal was to prove three standard lemmas: (1) the inductive invariant holds in
the initial state, (2) it is preserved by the protocol steps, and (3) it implies
agreement. I gave this goal and left it doing its job.</p>
<p>In the bootstrapping phase, C1 was asking questions about the assumptions. For
example, it was not clear that every round <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.09ex;" xmlns="http://www.w3.org/2000/svg" width="5.169ex" height="1.597ex" role="img" focusable="false" viewBox="0 -666 2284.6 706"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D45F" d="M21 287Q22 290 23 295T28 317T38 348T53 381T73 411T99 433T132 442Q161 442 183 430T214 408T225 388Q227 382 228 382T236 389Q284 441 347 441H350Q398 441 422 400Q430 381 430 363Q430 333 417 315T391 292T366 288Q346 288 334 299T322 328Q322 376 378 392Q356 405 342 405Q286 405 239 331Q229 315 224 298T190 165Q156 25 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 114 189T154 366Q154 405 128 405Q107 405 92 377T68 316T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(728.8,0)"><path data-c="3E" d="M84 520Q84 528 88 533T96 539L99 540Q106 540 253 471T544 334L687 265Q694 260 694 250T687 235Q685 233 395 96L107 -40H101Q83 -38 83 -20Q83 -19 83 -17Q82 -10 98 -1Q117 9 248 71Q326 108 378 132L626 250L378 368Q90 504 86 509Q84 513 84 520Z"></path></g><g data-mml-node="mn" transform="translate(1784.6,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g></svg></mjx-container></span> has a predecessor round
<span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="4.917ex" height="1.692ex" role="img" focusable="false" viewBox="0 -666 2173.4 748"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D45F" d="M21 287Q22 290 23 295T28 317T38 348T53 381T73 411T99 433T132 442Q161 442 183 430T214 408T225 388Q227 382 228 382T236 389Q284 441 347 441H350Q398 441 422 400Q430 381 430 363Q430 333 417 315T391 292T366 288Q346 288 334 299T322 328Q322 376 378 392Q356 405 342 405Q286 405 239 331Q229 315 224 298T190 165Q156 25 151 16Q138 -11 108 -11Q95 -11 87 -5T76 7T74 17Q74 30 114 189T154 366Q154 405 128 405Q107 405 92 377T68 316T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(673.2,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g><g data-mml-node="mn" transform="translate(1673.4,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g></svg></mjx-container></span>. C1 has to add such assumptions. At some point, it sneaked in the
assumption of <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="6.157ex" height="1.731ex" role="img" focusable="false" viewBox="0 -683 2721.6 765"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D441" d="M234 637Q231 637 226 637Q201 637 196 638T191 649Q191 676 202 682Q204 683 299 683Q376 683 387 683T401 677Q612 181 616 168L670 381Q723 592 723 606Q723 633 659 637Q635 637 635 648Q635 650 637 660Q641 676 643 679T653 683Q656 683 684 682T767 680Q817 680 843 681T873 682Q888 682 888 672Q888 650 880 642Q878 637 858 637Q787 633 769 597L620 7Q618 0 599 0Q585 0 582 2Q579 5 453 305L326 604L261 344Q196 88 196 79Q201 46 268 46H278Q284 41 284 38T282 19Q278 6 272 0H259Q228 2 151 2Q123 2 100 2T63 2T46 1Q31 1 31 10Q31 14 34 26T39 40Q41 46 62 46Q130 49 150 85Q154 91 221 362L289 634Q287 635 234 637Z"></path></g><g data-mml-node="mo" transform="translate(1165.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mn" transform="translate(2221.6,0)"><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z"></path></g></g></g></svg></mjx-container></span> and <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="5.741ex" height="1.717ex" role="img" focusable="false" viewBox="0 -677 2537.6 759"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D447" d="M40 437Q21 437 21 445Q21 450 37 501T71 602L88 651Q93 669 101 677H569H659Q691 677 697 676T704 667Q704 661 687 553T668 444Q668 437 649 437Q640 437 637 437T631 442L629 445Q629 451 635 490T641 551Q641 586 628 604T573 629Q568 630 515 631Q469 631 457 630T439 622Q438 621 368 343T298 60Q298 48 386 46Q418 46 427 45T436 36Q436 31 433 22Q429 4 424 1L422 0Q419 0 415 0Q410 0 363 1T228 2Q99 2 64 0H49Q43 6 43 9T45 27Q49 40 55 46H83H94Q174 46 189 55Q190 56 191 56Q196 59 201 76T241 233Q258 301 269 344Q339 619 339 625Q339 630 310 630H279Q212 630 191 624Q146 614 121 583T67 467Q60 445 57 441T43 437H40Z"></path></g><g data-mml-node="mo" transform="translate(981.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mn" transform="translate(2037.6,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g></svg></mjx-container></span>. It was a good starting point, though,
obviously, my goal was to have the proofs for <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.09ex;" xmlns="http://www.w3.org/2000/svg" width="9.385ex" height="1.636ex" role="img" focusable="false" viewBox="0 -683 4148 723"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D441" d="M234 637Q231 637 226 637Q201 637 196 638T191 649Q191 676 202 682Q204 683 299 683Q376 683 387 683T401 677Q612 181 616 168L670 381Q723 592 723 606Q723 633 659 637Q635 637 635 648Q635 650 637 660Q641 676 643 679T653 683Q656 683 684 682T767 680Q817 680 843 681T873 682Q888 682 888 672Q888 650 880 642Q878 637 858 637Q787 633 769 597L620 7Q618 0 599 0Q585 0 582 2Q579 5 453 305L326 604L261 344Q196 88 196 79Q201 46 268 46H278Q284 41 284 38T282 19Q278 6 272 0H259Q228 2 151 2Q123 2 100 2T63 2T46 1Q31 1 31 10Q31 14 34 26T39 40Q41 46 62 46Q130 49 150 85Q154 91 221 362L289 634Q287 635 234 637Z"></path></g><g data-mml-node="mo" transform="translate(1165.8,0)"><path data-c="3E" d="M84 520Q84 528 88 533T96 539L99 540Q106 540 253 471T544 334L687 265Q694 260 694 250T687 235Q685 233 395 96L107 -40H101Q83 -38 83 -20Q83 -19 83 -17Q82 -10 98 -1Q117 9 248 71Q326 108 378 132L626 250L378 368Q90 504 86 509Q84 513 84 520Z"></path></g><g data-mml-node="mn" transform="translate(2221.6,0)"><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z"></path></g><g data-mml-node="mo" transform="translate(2943.8,0)"><path data-c="22C5" d="M78 250Q78 274 95 292T138 310Q162 310 180 294T199 251Q199 226 182 208T139 190T96 207T78 250Z"></path></g><g data-mml-node="mi" transform="translate(3444,0)"><path data-c="1D447" d="M40 437Q21 437 21 445Q21 450 37 501T71 602L88 651Q93 669 101 677H569H659Q691 677 697 676T704 667Q704 661 687 553T668 444Q668 437 649 437Q640 437 637 437T631 442L629 445Q629 451 635 490T641 551Q641 586 628 604T573 629Q568 630 515 631Q469 631 457 630T439 622Q438 621 368 343T298 60Q298 48 386 46Q418 46 427 45T436 36Q436 31 433 22Q429 4 424 1L422 0Q419 0 415 0Q410 0 363 1T228 2Q99 2 64 0H49Q43 6 43 9T45 27Q49 40 55 46H83H94Q174 46 189 55Q190 56 191 56Q196 59 201 76T241 233Q258 301 269 344Q339 619 339 625Q339 630 310 630H279Q212 630 191 624Q146 614 121 583T67 467Q60 445 57 441T43 437H40Z"></path></g></g></g></svg></mjx-container></span> and <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.312ex;" xmlns="http://www.w3.org/2000/svg" width="10.453ex" height="1.851ex" role="img" focusable="false" viewBox="0 -680 4620.1 818"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D447" d="M40 437Q21 437 21 445Q21 450 37 501T71 602L88 651Q93 669 101 677H569H659Q691 677 697 676T704 667Q704 661 687 553T668 444Q668 437 649 437Q640 437 637 437T631 442L629 445Q629 451 635 490T641 551Q641 586 628 604T573 629Q568 630 515 631Q469 631 457 630T439 622Q438 621 368 343T298 60Q298 48 386 46Q418 46 427 45T436 36Q436 31 433 22Q429 4 424 1L422 0Q419 0 415 0Q410 0 363 1T228 2Q99 2 64 0H49Q43 6 43 9T45 27Q49 40 55 46H83H94Q174 46 189 55Q190 56 191 56Q196 59 201 76T241 233Q258 301 269 344Q339 619 339 625Q339 630 310 630H279Q212 630 191 624Q146 614 121 583T67 467Q60 445 57 441T43 437H40Z"></path></g><g data-mml-node="mo" transform="translate(981.8,0)"><path data-c="2265" d="M83 616Q83 624 89 630T99 636Q107 636 253 568T543 431T687 361Q694 356 694 346T687 331Q685 329 395 192L107 56H101Q83 58 83 76Q83 77 83 79Q82 86 98 95Q117 105 248 167Q326 204 378 228L626 346L360 472Q291 505 200 548Q112 589 98 597T83 616ZM84 -118Q84 -108 99 -98H678Q694 -104 694 -118Q694 -130 679 -138H98Q84 -131 84 -118Z"></path></g><g data-mml-node="mi" transform="translate(2037.6,0)"><path data-c="1D439" d="M48 1Q31 1 31 11Q31 13 34 25Q38 41 42 43T65 46Q92 46 125 49Q139 52 144 61Q146 66 215 342T285 622Q285 629 281 629Q273 632 228 634H197Q191 640 191 642T193 659Q197 676 203 680H742Q749 676 749 669Q749 664 736 557T722 447Q720 440 702 440H690Q683 445 683 453Q683 454 686 477T689 530Q689 560 682 579T663 610T626 626T575 633T503 634H480Q398 633 393 631Q388 629 386 623Q385 622 352 492L320 363H375Q378 363 398 363T426 364T448 367T472 374T489 386Q502 398 511 419T524 457T529 475Q532 480 548 480H560Q567 475 567 470Q567 467 536 339T502 207Q500 200 482 200H470Q463 206 463 212Q463 215 468 234T473 274Q473 303 453 310T364 317H309L277 190Q245 66 245 60Q245 46 334 46H359Q365 40 365 39T363 19Q359 6 353 0H336Q295 2 185 2Q120 2 86 2T48 1Z"></path></g><g data-mml-node="mo" transform="translate(3064.3,0)"><path data-c="2265" d="M83 616Q83 624 89 630T99 636Q107 636 253 568T543 431T687 361Q694 356 694 346T687 331Q685 329 395 192L107 56H101Q83 58 83 76Q83 77 83 79Q82 86 98 95Q117 105 248 167Q326 204 378 228L626 346L360 472Q291 505 200 548Q112 589 98 597T83 616ZM84 -118Q84 -108 99 -98H678Q694 -104 694 -118Q694 -130 679 -138H98Q84 -131 84 -118Z"></path></g><g data-mml-node="mn" transform="translate(4120.1,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g></g></g></svg></mjx-container></span>.</p>
<p>In addition to that, C1 has absolutely cheated to speed up the proof. Just look
at the assumptions below:</p>
<pre><code class="hljs language-lean"><span class="hljs-keyword">def</span> model_assumptions (s : State) : Prop :=
  assumptions_hold s ∧
    s.CORRECT ∩ s.FAULTY = ∅ ∧
      (∀ r ∈ s.ROUNDS, <span class="hljs-number">1</span> ≤ r) ∧
        (∀ r ∈ s.ROUNDS, r ≠ <span class="hljs-number">1</span> → r - <span class="hljs-number">1</span> ∈ s.ROUNDS) ∧
          s.F ≤ s.T ∧ s.N = <span class="hljs-number">6</span> ∧ s.T = <span class="hljs-number">1</span> ∧
            step2_preserves_ind_inv_assumption s ∧
              step3_preserves_ind_inv_assumption s ∧
                faulty_step_preserves_ind_inv_assumption s
</code></pre>
<p>In short, it was proving inductiveness of <code>step1</code> only. The rest was proved
magically, by assuming inductiveness of the other actions. This is why I am
always saying that <strong>we have to review the proof obligations!</strong> The proofs are
checked by Lean, but it only checks that the proofs are correct with respect to
the theorem statements.</p>
<p>After catching C1 cheating, I gave it instructions to avoid moving the goal
posts. Then, I was checking with C1 from time to time. When it looked
alternating between the same kind of things, I was giving it a few hints.</p>
<p><strong>Finishing the proof.</strong> After 4 days I started to worry. The proof file was
approaching 30 KLOC. Not only C1 was close to the weekly limit of my
subscription, it was adding more and more theorems. Look at this git statistics:</p>
<figure>
  <a href="https://protocols-made-fun.com/img/lean-proofs-git-stats.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
    target="_blank" title="Click to open full-size"><picture>
    <img class="responsive-img"
    src="https://protocols-made-fun.com/img/lean-proofs-git-stats.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
    alt="Lean proofs git statistics">
  </picture></a>
  <figcaption>Figure 3: Git statistics for Lean proofs.</figcaption>
</figure>
<p>At this point, I switched to C2. First, I asked it to identify cheating points
in the Lean proofs. It found out that C1 was running into a circular argument.
This is why it could not properly converge. As you can see from the git
statistics, C2 removed a lot after C1. It still took it about a day to finish
the proof.</p>
<p>After finishing the proofs for <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="6.157ex" height="1.731ex" role="img" focusable="false" viewBox="0 -683 2721.6 765"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D441" d="M234 637Q231 637 226 637Q201 637 196 638T191 649Q191 676 202 682Q204 683 299 683Q376 683 387 683T401 677Q612 181 616 168L670 381Q723 592 723 606Q723 633 659 637Q635 637 635 648Q635 650 637 660Q641 676 643 679T653 683Q656 683 684 682T767 680Q817 680 843 681T873 682Q888 682 888 672Q888 650 880 642Q878 637 858 637Q787 633 769 597L620 7Q618 0 599 0Q585 0 582 2Q579 5 453 305L326 604L261 344Q196 88 196 79Q201 46 268 46H278Q284 41 284 38T282 19Q278 6 272 0H259Q228 2 151 2Q123 2 100 2T63 2T46 1Q31 1 31 10Q31 14 34 26T39 40Q41 46 62 46Q130 49 150 85Q154 91 221 362L289 634Q287 635 234 637Z"></path></g><g data-mml-node="mo" transform="translate(1165.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mn" transform="translate(2221.6,0)"><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z"></path></g></g></g></svg></mjx-container></span>, <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="5.741ex" height="1.717ex" role="img" focusable="false" viewBox="0 -677 2537.6 759"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D447" d="M40 437Q21 437 21 445Q21 450 37 501T71 602L88 651Q93 669 101 677H569H659Q691 677 697 676T704 667Q704 661 687 553T668 444Q668 437 649 437Q640 437 637 437T631 442L629 445Q629 451 635 490T641 551Q641 586 628 604T573 629Q568 630 515 631Q469 631 457 630T439 622Q438 621 368 343T298 60Q298 48 386 46Q418 46 427 45T436 36Q436 31 433 22Q429 4 424 1L422 0Q419 0 415 0Q410 0 363 1T228 2Q99 2 64 0H49Q43 6 43 9T45 27Q49 40 55 46H83H94Q174 46 189 55Q190 56 191 56Q196 59 201 76T241 233Q258 301 269 344Q339 619 339 625Q339 630 310 630H279Q212 630 191 624Q146 614 121 583T67 467Q60 445 57 441T43 437H40Z"></path></g><g data-mml-node="mo" transform="translate(981.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mn" transform="translate(2037.6,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g></svg></mjx-container></span>, and <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="5.843ex" height="1.724ex" role="img" focusable="false" viewBox="0 -680 2582.6 762"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D439" d="M48 1Q31 1 31 11Q31 13 34 25Q38 41 42 43T65 46Q92 46 125 49Q139 52 144 61Q146 66 215 342T285 622Q285 629 281 629Q273 632 228 634H197Q191 640 191 642T193 659Q197 676 203 680H742Q749 676 749 669Q749 664 736 557T722 447Q720 440 702 440H690Q683 445 683 453Q683 454 686 477T689 530Q689 560 682 579T663 610T626 626T575 633T503 634H480Q398 633 393 631Q388 629 386 623Q385 622 352 492L320 363H375Q378 363 398 363T426 364T448 367T472 374T489 386Q502 398 511 419T524 457T529 475Q532 480 548 480H560Q567 475 567 470Q567 467 536 339T502 207Q500 200 482 200H470Q463 206 463 212Q463 215 468 234T473 274Q473 303 453 310T364 317H309L277 190Q245 66 245 60Q245 46 334 46H359Q365 40 365 39T363 19Q359 6 353 0H336Q295 2 185 2Q120 2 86 2T48 1Z"></path></g><g data-mml-node="mo" transform="translate(1026.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mn" transform="translate(2082.6,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g></svg></mjx-container></span>, I asked C2 to
generalize the proofs to arbitrary <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.09ex;" xmlns="http://www.w3.org/2000/svg" width="9.385ex" height="1.636ex" role="img" focusable="false" viewBox="0 -683 4148 723"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D441" d="M234 637Q231 637 226 637Q201 637 196 638T191 649Q191 676 202 682Q204 683 299 683Q376 683 387 683T401 677Q612 181 616 168L670 381Q723 592 723 606Q723 633 659 637Q635 637 635 648Q635 650 637 660Q641 676 643 679T653 683Q656 683 684 682T767 680Q817 680 843 681T873 682Q888 682 888 672Q888 650 880 642Q878 637 858 637Q787 633 769 597L620 7Q618 0 599 0Q585 0 582 2Q579 5 453 305L326 604L261 344Q196 88 196 79Q201 46 268 46H278Q284 41 284 38T282 19Q278 6 272 0H259Q228 2 151 2Q123 2 100 2T63 2T46 1Q31 1 31 10Q31 14 34 26T39 40Q41 46 62 46Q130 49 150 85Q154 91 221 362L289 634Q287 635 234 637Z"></path></g><g data-mml-node="mo" transform="translate(1165.8,0)"><path data-c="3E" d="M84 520Q84 528 88 533T96 539L99 540Q106 540 253 471T544 334L687 265Q694 260 694 250T687 235Q685 233 395 96L107 -40H101Q83 -38 83 -20Q83 -19 83 -17Q82 -10 98 -1Q117 9 248 71Q326 108 378 132L626 250L378 368Q90 504 86 509Q84 513 84 520Z"></path></g><g data-mml-node="mn" transform="translate(2221.6,0)"><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z"></path></g><g data-mml-node="mo" transform="translate(2943.8,0)"><path data-c="22C5" d="M78 250Q78 274 95 292T138 310Q162 310 180 294T199 251Q199 226 182 208T139 190T96 207T78 250Z"></path></g><g data-mml-node="mi" transform="translate(3444,0)"><path data-c="1D447" d="M40 437Q21 437 21 445Q21 450 37 501T71 602L88 651Q93 669 101 677H569H659Q691 677 697 676T704 667Q704 661 687 553T668 444Q668 437 649 437Q640 437 637 437T631 442L629 445Q629 451 635 490T641 551Q641 586 628 604T573 629Q568 630 515 631Q469 631 457 630T439 622Q438 621 368 343T298 60Q298 48 386 46Q418 46 427 45T436 36Q436 31 433 22Q429 4 424 1L422 0Q419 0 415 0Q410 0 363 1T228 2Q99 2 64 0H49Q43 6 43 9T45 27Q49 40 55 46H83H94Q174 46 189 55Q190 56 191 56Q196 59 201 76T241 233Q258 301 269 344Q339 619 339 625Q339 630 310 630H279Q212 630 191 624Q146 614 121 583T67 467Q60 445 57 441T43 437H40Z"></path></g></g></g></svg></mjx-container></span> and <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.312ex;" xmlns="http://www.w3.org/2000/svg" width="10.453ex" height="1.851ex" role="img" focusable="false" viewBox="0 -680 4620.1 818"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D447" d="M40 437Q21 437 21 445Q21 450 37 501T71 602L88 651Q93 669 101 677H569H659Q691 677 697 676T704 667Q704 661 687 553T668 444Q668 437 649 437Q640 437 637 437T631 442L629 445Q629 451 635 490T641 551Q641 586 628 604T573 629Q568 630 515 631Q469 631 457 630T439 622Q438 621 368 343T298 60Q298 48 386 46Q418 46 427 45T436 36Q436 31 433 22Q429 4 424 1L422 0Q419 0 415 0Q410 0 363 1T228 2Q99 2 64 0H49Q43 6 43 9T45 27Q49 40 55 46H83H94Q174 46 189 55Q190 56 191 56Q196 59 201 76T241 233Q258 301 269 344Q339 619 339 625Q339 630 310 630H279Q212 630 191 624Q146 614 121 583T67 467Q60 445 57 441T43 437H40Z"></path></g><g data-mml-node="mo" transform="translate(981.8,0)"><path data-c="2265" d="M83 616Q83 624 89 630T99 636Q107 636 253 568T543 431T687 361Q694 356 694 346T687 331Q685 329 395 192L107 56H101Q83 58 83 76Q83 77 83 79Q82 86 98 95Q117 105 248 167Q326 204 378 228L626 346L360 472Q291 505 200 548Q112 589 98 597T83 616ZM84 -118Q84 -108 99 -98H678Q694 -104 694 -118Q694 -130 679 -138H98Q84 -131 84 -118Z"></path></g><g data-mml-node="mi" transform="translate(2037.6,0)"><path data-c="1D439" d="M48 1Q31 1 31 11Q31 13 34 25Q38 41 42 43T65 46Q92 46 125 49Q139 52 144 61Q146 66 215 342T285 622Q285 629 281 629Q273 632 228 634H197Q191 640 191 642T193 659Q197 676 203 680H742Q749 676 749 669Q749 664 736 557T722 447Q720 440 702 440H690Q683 445 683 453Q683 454 686 477T689 530Q689 560 682 579T663 610T626 626T575 633T503 634H480Q398 633 393 631Q388 629 386 623Q385 622 352 492L320 363H375Q378 363 398 363T426 364T448 367T472 374T489 386Q502 398 511 419T524 457T529 475Q532 480 548 480H560Q567 475 567 470Q567 467 536 339T502 207Q500 200 482 200H470Q463 206 463 212Q463 215 468 234T473 274Q473 303 453 310T364 317H309L277 190Q245 66 245 60Q245 46 334 46H359Q365 40 365 39T363 19Q359 6 353 0H336Q295 2 185 2Q120 2 86 2T48 1Z"></path></g><g data-mml-node="mo" transform="translate(3064.3,0)"><path data-c="2265" d="M83 616Q83 624 89 630T99 636Q107 636 253 568T543 431T687 361Q694 356 694 346T687 331Q685 329 395 192L107 56H101Q83 58 83 76Q83 77 83 79Q82 86 98 95Q117 105 248 167Q326 204 378 228L626 346L360 472Q291 505 200 548Q112 589 98 597T83 616ZM84 -118Q84 -108 99 -98H678Q694 -104 694 -118Q694 -130 679 -138H98Q84 -131 84 -118Z"></path></g><g data-mml-node="mn" transform="translate(4120.1,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g></g></g></svg></mjx-container></span>. This is
where it is getting interesting. It was impossible to prove Lemma 8 for <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.09ex;" xmlns="http://www.w3.org/2000/svg" width="5.741ex" height="1.622ex" role="img" focusable="false" viewBox="0 -677 2537.6 717"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D447" d="M40 437Q21 437 21 445Q21 450 37 501T71 602L88 651Q93 669 101 677H569H659Q691 677 697 676T704 667Q704 661 687 553T668 444Q668 437 649 437Q640 437 637 437T631 442L629 445Q629 451 635 490T641 551Q641 586 628 604T573 629Q568 630 515 631Q469 631 457 630T439 622Q438 621 368 343T298 60Q298 48 386 46Q418 46 427 45T436 36Q436 31 433 22Q429 4 424 1L422 0Q419 0 415 0Q410 0 363 1T228 2Q99 2 64 0H49Q43 6 43 9T45 27Q49 40 55 46H83H94Q174 46 189 55Q190 56 191 56Q196 59 201 76T241 233Q258 301 269 344Q339 619 339 625Q339 630 310 630H279Q212 630 191 624Q146 614 121 583T67 467Q60 445 57 441T43 437H40Z"></path></g><g data-mml-node="mo" transform="translate(981.8,0)"><path data-c="3E" d="M84 520Q84 528 88 533T96 539L99 540Q106 540 253 471T544 334L687 265Q694 260 694 250T687 235Q685 233 395 96L107 -40H101Q83 -38 83 -20Q83 -19 83 -17Q82 -10 98 -1Q117 9 248 71Q326 108 378 132L626 250L378 368Q90 504 86 509Q84 513 84 520Z"></path></g><g data-mml-node="mn" transform="translate(2037.6,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g></svg></mjx-container></span>,
as it was not inductive. C2 suspected this, constructed a counterexample with
<code>omega</code> and suggested a simple fix in one of the conditions.</p>
<p><strong>Bottom line.</strong> This worked! I did not have to hold the hand of C1 and C2.
However, I had to review the theorem statements and help the tools from time to
time. I did not read the detailed proofs, only ran Lean on them, so there is
still a chance that these tools cheated in the proofs, by using known soundness
issues. I only asked C2 to double-check for cheating.</p>
<p>This also brings me to the thought that we should use two LLMs to write and
double check the proofs. This is not a fresh idea. In my case, have not I not
switched to C2, I could end up having a tremendous unfinished proof file.</p>
<p>You can find the full Lean proofs in <a href="https://github.com/konnov/leanda/tree/main/Ben-Or?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Ben-or-lean-proofs</a>. The final version
is 6.6 KLOC and 259K.</p>
<h2 id="2.-proving-inductiveness-and-safety-with-tlaps" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/proofs-are-closer.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#2.-proving-inductiveness-and-safety-with-tlaps">2. Proving inductiveness and safety with TLAPS</a></h2>
<p>Since it worked with Lean 4, why not try the same with TLAPS? This is especially
interesting, as TLA<sup>+</sup> proofs are more structured towards reasoning
about state machines. This should give the AI tools less room for divergence and
cheating.</p>
<p>I let C1 do the bootstrapping. It installed the TLA proof manager (TLAPM).  As I
am running the AI tools inside a virtual machine on a MacBook Pro, the standard
distributions of TLAPS did not work. The combination of Linux and Arm64 is in
general not well supported. C1 managed to compile TLAPM from scratch. It had to
patch Z3 and the build process on the way. This is definitely something I would
not like doing by hand.</p>
<p>C1 started well. At some point, I saw it struggling with set cardinalities, and
things did not look improving. This is a well-known pain point with proofs
about quorums. So I gave it a hint how to decompose cardinalities into lemmas
about quorums. C1 figured this out very quickly and moved past this pain point.</p>
<p>In case of TLA<sup>+</sup>, the proof structure was clear from the beginning.
The proof file had a lot of <code>OMITTED</code> statements, and the job of the AI tools
was to turn them into real proofs one by one.</p>
<p>Every time, I was giving C1 the goal of closing five <code>OMITTED</code> statements.  This
worked well until three to four <code>OMITTED</code> statements were left. C1 was really
stuck there.  It actually hinted at potential problems with Lemma 8. But it was
not sure.</p>
<p>So I switched to C2. Then, something interesting happened. C2 simply ran
<a href="https://apalache-mc.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache</a> to check the inductiveness of Lemma 8 for <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="5.741ex" height="1.717ex" role="img" focusable="false" viewBox="0 -677 2537.6 759"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D447" d="M40 437Q21 437 21 445Q21 450 37 501T71 602L88 651Q93 669 101 677H569H659Q691 677 697 676T704 667Q704 661 687 553T668 444Q668 437 649 437Q640 437 637 437T631 442L629 445Q629 451 635 490T641 551Q641 586 628 604T573 629Q568 630 515 631Q469 631 457 630T439 622Q438 621 368 343T298 60Q298 48 386 46Q418 46 427 45T436 36Q436 31 433 22Q429 4 424 1L422 0Q419 0 415 0Q410 0 363 1T228 2Q99 2 64 0H49Q43 6 43 9T45 27Q49 40 55 46H83H94Q174 46 189 55Q190 56 191 56Q196 59 201 76T241 233Q258 301 269 344Q339 619 339 625Q339 630 310 630H279Q212 630 191 624Q146 614 121 583T67 467Q60 445 57 441T43 437H40Z"></path></g><g data-mml-node="mo" transform="translate(981.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mn" transform="translate(2037.6,0)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g></g></g></svg></mjx-container></span>. This is
brilliant. After some time, it got a counterexamples and proposed a fix.  You
can see the correction in <a href="https://github.com/konnov/apalache-examples/commit/d624842d697fe9e1c539eda1d3636326b28962ad?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">this git
commit</a>.
It simply had to fix <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.375ex;" xmlns="http://www.w3.org/2000/svg" width="10.074ex" height="1.92ex" role="img" focusable="false" viewBox="0 -683 4452.6 848.6"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g><g data-mml-node="mo" transform="translate(722.2,0)"><path data-c="22C5" d="M78 250Q78 274 95 292T138 310Q162 310 180 294T199 251Q199 226 182 208T139 190T96 207T78 250Z"></path></g><g data-mml-node="msub" transform="translate(1222.4,0)"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mn" transform="translate(605,-150) scale(0.707)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g></g><g data-mml-node="mo" transform="translate(2508.8,0)"><path data-c="2264" d="M674 636Q682 636 688 630T694 615T687 601Q686 600 417 472L151 346L399 228Q687 92 691 87Q694 81 694 76Q694 58 676 56H670L382 192Q92 329 90 331Q83 336 83 348Q84 359 96 365Q104 369 382 500T665 634Q669 636 674 636ZM84 -118Q84 -108 99 -98H678Q694 -104 694 -118Q694 -130 679 -138H98Q84 -131 84 -118Z"></path></g><g data-mml-node="mi" transform="translate(3564.6,0)"><path data-c="1D441" d="M234 637Q231 637 226 637Q201 637 196 638T191 649Q191 676 202 682Q204 683 299 683Q376 683 387 683T401 677Q612 181 616 168L670 381Q723 592 723 606Q723 633 659 637Q635 637 635 648Q635 650 637 660Q641 676 643 679T653 683Q656 683 684 682T767 680Q817 680 843 681T873 682Q888 682 888 672Q888 650 880 642Q878 637 858 637Q787 633 769 597L620 7Q618 0 599 0Q585 0 582 2Q579 5 453 305L326 604L261 344Q196 88 196 79Q201 46 268 46H278Q284 41 284 38T282 19Q278 6 272 0H259Q228 2 151 2Q123 2 100 2T63 2T46 1Q31 1 31 10Q31 14 34 26T39 40Q41 46 62 46Q130 49 150 85Q154 91 221 362L289 634Q287 635 234 637Z"></path></g></g></g></svg></mjx-container></span> to <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.375ex;" xmlns="http://www.w3.org/2000/svg" width="14.432ex" height="1.92ex" role="img" focusable="false" viewBox="0 -683 6379 848.6"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g><g data-mml-node="mo" transform="translate(722.2,0)"><path data-c="22C5" d="M78 250Q78 274 95 292T138 310Q162 310 180 294T199 251Q199 226 182 208T139 190T96 207T78 250Z"></path></g><g data-mml-node="msub" transform="translate(1222.4,0)"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mn" transform="translate(605,-150) scale(0.707)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g></g><g data-mml-node="mo" transform="translate(2508.8,0)"><path data-c="3C" d="M694 -11T694 -19T688 -33T678 -40Q671 -40 524 29T234 166L90 235Q83 240 83 250Q83 261 91 266Q664 540 678 540Q681 540 687 534T694 519T687 505Q686 504 417 376L151 250L417 124Q686 -4 687 -5Q694 -11 694 -19Z"></path></g><g data-mml-node="mi" transform="translate(3564.6,0)"><path data-c="1D441" d="M234 637Q231 637 226 637Q201 637 196 638T191 649Q191 676 202 682Q204 683 299 683Q376 683 387 683T401 677Q612 181 616 168L670 381Q723 592 723 606Q723 633 659 637Q635 637 635 648Q635 650 637 660Q641 676 643 679T653 683Q656 683 684 682T767 680Q817 680 843 681T873 682Q888 682 888 672Q888 650 880 642Q878 637 858 637Q787 633 769 597L620 7Q618 0 599 0Q585 0 582 2Q579 5 453 305L326 604L261 344Q196 88 196 79Q201 46 268 46H278Q284 41 284 38T282 19Q278 6 272 0H259Q228 2 151 2Q123 2 100 2T63 2T46 1Q31 1 31 10Q31 14 34 26T39 40Q41 46 62 46Q130 49 150 85Q154 91 221 362L289 634Q287 635 234 637Z"></path></g><g data-mml-node="mo" transform="translate(4674.8,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mi" transform="translate(5675,0)"><path data-c="1D447" d="M40 437Q21 437 21 445Q21 450 37 501T71 602L88 651Q93 669 101 677H569H659Q691 677 697 676T704 667Q704 661 687 553T668 444Q668 437 649 437Q640 437 637 437T631 442L629 445Q629 451 635 490T641 551Q641 586 628 604T573 629Q568 630 515 631Q469 631 457 630T439 622Q438 621 368 343T298 60Q298 48 386 46Q418 46 427 45T436 36Q436 31 433 22Q429 4 424 1L422 0Q419 0 415 0Q410 0 363 1T228 2Q99 2 64 0H49Q43 6 43 9T45 27Q49 40 55 46H83H94Q174 46 189 55Q190 56 191 56Q196 59 201 76T241 233Q258 301 269 344Q339 619 339 625Q339 630 310 630H279Q212 630 191 624Q146 614 121 583T67 467Q60 445 57 441T43 437H40Z"></path></g></g></g></svg></mjx-container></span> (and the same
for <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.339ex;" xmlns="http://www.w3.org/2000/svg" width="2.282ex" height="1.339ex" role="img" focusable="false" viewBox="0 -442 1008.6 592"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mn" transform="translate(605,-150) scale(0.707)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g></g></svg></mjx-container></span>). This condition worked without the fix for <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="5.741ex" height="1.717ex" role="img" focusable="false" viewBox="0 -677 2537.6 759"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D447" d="M40 437Q21 437 21 445Q21 450 37 501T71 602L88 651Q93 669 101 677H569H659Q691 677 697 676T704 667Q704 661 687 553T668 444Q668 437 649 437Q640 437 637 437T631 442L629 445Q629 451 635 490T641 551Q641 586 628 604T573 629Q568 630 515 631Q469 631 457 630T439 622Q438 621 368 343T298 60Q298 48 386 46Q418 46 427 45T436 36Q436 31 433 22Q429 4 424 1L422 0Q419 0 415 0Q410 0 363 1T228 2Q99 2 64 0H49Q43 6 43 9T45 27Q49 40 55 46H83H94Q174 46 189 55Q190 56 191 56Q196 59 201 76T241 233Q258 301 269 344Q339 619 339 625Q339 630 310 630H279Q212 630 191 624Q146 614 121 583T67 467Q60 445 57 441T43 437H40Z"></path></g><g data-mml-node="mo" transform="translate(981.8,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mn" transform="translate(2037.6,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g></svg></mjx-container></span>, but not for <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.09ex;" xmlns="http://www.w3.org/2000/svg" width="5.741ex" height="1.622ex" role="img" focusable="false" viewBox="0 -677 2537.6 717"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D447" d="M40 437Q21 437 21 445Q21 450 37 501T71 602L88 651Q93 669 101 677H569H659Q691 677 697 676T704 667Q704 661 687 553T668 444Q668 437 649 437Q640 437 637 437T631 442L629 445Q629 451 635 490T641 551Q641 586 628 604T573 629Q568 630 515 631Q469 631 457 630T439 622Q438 621 368 343T298 60Q298 48 386 46Q418 46 427 45T436 36Q436 31 433 22Q429 4 424 1L422 0Q419 0 415 0Q410 0 363 1T228 2Q99 2 64 0H49Q43 6 43 9T45 27Q49 40 55 46H83H94Q174 46 189 55Q190 56 191 56Q196 59 201 76T241 233Q258 301 269 344Q339 619 339 625Q339 630 310 630H279Q212 630 191 624Q146 614 121 583T67 467Q60 445 57 441T43 437H40Z"></path></g><g data-mml-node="mo" transform="translate(981.8,0)"><path data-c="3E" d="M84 520Q84 528 88 533T96 539L99 540Q106 540 253 471T544 334L687 265Q694 260 694 250T687 235Q685 233 395 96L107 -40H101Q83 -38 83 -20Q83 -19 83 -17Q82 -10 98 -1Q117 9 248 71Q326 108 378 132L626 250L378 368Q90 504 86 509Q84 513 84 520Z"></path></g><g data-mml-node="mn" transform="translate(2037.6,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g></svg></mjx-container></span>. This is how Lemma 8 looks like after the fix:</p>
<pre><code class="hljs language-tlaplus"><span class="hljs-title function_">Lemma8_Q2RequiresNoQuorumFaster</span> <span class="hljs-operator">==</span>
  Lemma8a ::
  <span class="hljs-keyword">LET</span> RoundsWithQ2 <span class="hljs-operator">==</span>
    { r <span class="hljs-keyword">\in</span> ROUNDS:
      <span class="hljs-keyword">\E</span> m <span class="hljs-keyword">\in</span> msgs2[r]: IsQ2(m) <span class="hljs-operator">/\</span> AsQ2(m).src <span class="hljs-keyword">\in</span> CORRECT }
  <span class="hljs-keyword">IN</span>
  <span class="hljs-keyword">\A</span> r <span class="hljs-keyword">\in</span> RoundsWithQ2:
    <span class="hljs-comment">\* follows from Step2</span>
    <span class="hljs-keyword">LET</span> n0 <span class="hljs-operator">==</span> <span class="hljs-keyword">Cardinality</span>({ id <span class="hljs-keyword">\in</span> CORRECT: [ src |<span class="hljs-operator">-</span>&gt; id, r |<span class="hljs-operator">-</span>&gt; r, v |<span class="hljs-operator">-</span>&gt; <span class="hljs-number">0</span> ] <span class="hljs-keyword">\in</span> msgs1[r] })
        n1 <span class="hljs-operator">==</span> <span class="hljs-keyword">Cardinality</span>({ id <span class="hljs-keyword">\in</span> CORRECT: [ src |<span class="hljs-operator">-</span>&gt; id, r |<span class="hljs-operator">-</span>&gt; r, v |<span class="hljs-operator">-</span>&gt; <span class="hljs-number">1</span> ] <span class="hljs-keyword">\in</span> msgs1[r] })
        <span class="hljs-comment">\* we wrap the map in a filter to constrain the set bound</span>
        nf <span class="hljs-operator">==</span> <span class="hljs-keyword">Cardinality</span>({ id <span class="hljs-keyword">\in</span> FAULTY: id <span class="hljs-keyword">\in</span> { m.src: m <span class="hljs-keyword">\in</span> msgs1[r] } })
    <span class="hljs-keyword">IN</span>
    <span class="hljs-keyword">\E</span> x0, x1 <span class="hljs-keyword">\in</span> <span class="hljs-number">0</span><span class="hljs-operator">..</span>N:
      <span class="hljs-operator">/\</span> x0 <span class="hljs-operator">&lt;=</span> n0 <span class="hljs-operator">/\</span> x1 <span class="hljs-operator">&lt;=</span> n1
      <span class="hljs-operator">/\</span> x0 <span class="hljs-operator">+</span> x1 <span class="hljs-operator">+</span> nf <span class="hljs-operator">&gt;=</span> N <span class="hljs-operator">-</span> T
      <span class="hljs-operator">/\</span> <span class="hljs-number">2</span> <span class="hljs-operator">*</span> x0 <span class="hljs-operator">&lt;=</span> N <span class="hljs-operator">+</span> T
      <span class="hljs-operator">/\</span> <span class="hljs-number">2</span> <span class="hljs-operator">*</span> x1 <span class="hljs-operator">&lt;=</span> N <span class="hljs-operator">+</span> T
</code></pre>
<p>Again, C1 and C2 managed to construct the proofs. Closer to the end, it was
taking TLAPM a lot of time to check the proofs. So it was becoming a bottleneck.</p>
<p>Below, you can see the dynamics of the proof file. In contrast to the Lean
proofs, only small portions of the TLA<sup>+</sup> proofs were removed. It looks
like the TLAPS proofs give the AI tools more structure to succeed.</p>
<figure>
  <a href="https://protocols-made-fun.com/img/tlaps-proofs-git-stats.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
    target="_blank" title="Click to open full-size"><picture>
    <img class="responsive-img"
    src="https://protocols-made-fun.com/img/tlaps-proofs-git-stats.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
    alt="TLA+ proofs git statistics">
  </picture></a>
  <figcaption>Figure 4: Git statistics for TLA+ proofs.</figcaption>
</figure>
<p><strong>Bottom line.</strong> This also worked! Interestingly, with TLA<sup>+</sup>,
I did not see the AI tools cheating. I am impressed by that C2 just picked up
Apalache to construct a counterexample for Lemma 8. This is really using
the strong sides of different tools.</p>
<p>You can find the full proofs in <a href="https://github.com/konnov/apalache-examples/blob/main/ben-or83/Ben_or83_proofs.tla?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Ben-or-tla-proofs</a>. They are 6.2 KLOC and
306K. Surprisingly, these figures are very close to the Lean proofs.</p>
<h2 id="conclusions" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/proofs-are-closer.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#conclusions">Conclusions</a></h2>
<p>The economics of formal proofs has definitely changed. It is still not fully
automated. AI tools require supervision and good proof structure. Given that,
they can write proofs that would take me much longer to write by hand.</p>
<p>Note that the <strong>AI tools were given the core proof arguments and the proof
structure</strong> in the form of an inductive invariant in TLA<sup>+</sup>. I suspect
that it would be much harder for them to come up with a good inductive
invariant. The search space is much larger than proving the lemmas for a given
invariant.</p>
<p>This is where coordination between humans, model checkers (like Apalache), and
AI tools plays well. Now, we can delegate the tedious work of checking the
inductiveness to Apalache, and the tedious work of writing the proofs to AI
tools. Importantly, the human (me) was still in the loop, but did not have to do
the tedious work.</p>
<p>If you thought of having formal proofs for your protocols in the past, but
considered it to be too expensive, now it is the time to reconsider. You can do
it yourself, or you can save your time by hiring me to do it for you.</p>
<h2 id="want-to-talk%3F" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/proofs-are-closer.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#want-to-talk%3F">Want to talk?</a></h2>
<!-- References -->
]]></content>
    <author>
      <name>Igor Konnov</name>
      <email>igor@konnov.phd</email>
      <uri>https://konnov.phd/?utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site</uri>
    </author>
    
    
    <category term="distributed" />
    
    <category term="proving" />
    
    
    
  </entry>
  
  
  <entry>
    <title type="html">Extracting formal specifications from Apache ZooKeeper with AI tools and Apalache</title>
    <link href="https://protocols-made-fun.com/zookeeper-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" rel="alternate" type="text/html" title="Extracting formal specifications from Apache ZooKeeper with AI tools and Apalache" />
    <published>2026-05-26T00:00:00.000Z</published>
    <updated>2026-05-26T00:00:00.000Z</updated>
    <id>https://protocols-made-fun.com/zookeeper-testing.html</id>
    <content type="html" xml:base="https://protocols-made-fun.com/zookeeper-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"><![CDATA[<p><strong>Author:</strong> <a href="https://konnov.phd/?utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site">Igor Konnov</a></p>
<p><strong>Date:</strong> May 26, 2026</p>
<p><em>This text is artisanally typed using a keyboard, with occasional suggestions by
Copilot. The figures are generated with ChatGPT 5.5. The plots are produced by
AI-generated scripts from the experimental data. By AI tools, I refer to Codex
GPT 5.4/5.5 and Claude Code Sonnet/Opus 4.6/4.7.</em></p>
<img class="zm-logo" src="https://protocols-made-fun.com/img/zk-testing/zm-logo.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" alt="ZooKeeper logo">
<p>Recently, I gave a talk on &quot;<em>Interactive symbolic testing with TLA<sup>+</sup>,
Apalache, and LLMs</em>&quot; at the <a href="https://conf.tlapl.us/2026-etaps/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">TLA+ Community Meeting 2026</a>. If you
prefer watching talks, see <a href="https://www.youtube.com/watch?v=CQPhAfi-6Uk&amp;utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">the talk recording</a>. I talked
about the new <a href="https://github.com/apalache-mc/apalache/tree/main/json-rpc?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache JSON-RPC</a> and how it can be used to test real
distributed protocols. As the first example, I presented the case study on
<a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">symbolic testing of TFTP protocol</a>, which was published in
December 2025. As the second example, I presented a case study on symbolic
testing of <a href="https://zookeeper.apache.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apache ZooKeeper</a>, which is the subject of this
blog post. I also talked about this as ongoing work at <a href="https://www.tu.berlin/en/mtv/research/events/d-con-2026?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">D-CON 2026</a> (thanks to
<a href="https://www.tu.berlin/en/mtv/team/head/uwe-nestmann?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Uwe Nestmann</a> for inviting me!).</p>
<p>In case of TFTP, <strong>the main hypothesis was that AI tools can accelerate the
process of writing test harnesses for protocol testing</strong>. In October 2025, I
used Copilot and Sonnet 4.5. The answer was &quot;yes&quot;, though the AI tools in 2025
required plenty of manual intervention and literally drained my energy. Back
then, I wrote the TLA<sup>+</sup> specification of TFTP by hand. I also had to
refine it manually, in about 20-25 iterations. As a reward, the test harness
helped me to find a few bugs in the real implementations. I still had to triage
the bugs manually though.</p>
<p><em>Footnote</em>: Actually, the real question for me was not whether AI tools could
help the engineers to write a test harness. In my experience, engineers avoid
writing test harnesses as much as they can. So the real question was whether the
AI tools could do the job that engineers avoid doing.</p>
<p>The next step was to ask the following question:</p>
<div style="font-size: 1.3em; text-align: center;">
<p style="font-size: 1.3em;"><strong>Can AI tools extract formal specifications
from the source code and write test harnesses?</strong></p>
</div>
<p>In March-April 2026, I ran Claude Code Sonnet/Opus 4.6 and Codex GPT 5.4 to
<strong>check this hypothesis on the example of Apache ZooKeeper</strong>.
This case study is the subject of this blog post. I already hinted at this work
in <a href="https://protocols-made-fun.com/debug-as-code-generation.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Debug as Code Generation</a>.  To this end, I have been running
this loop:</p>
  <figure>
    <a href="https://protocols-made-fun.com/img/zk-testing/extraction-loop.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/zk-testing/extraction-loop.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Extraction loop">
    </picture></a>
    <figcaption>Figure 1: Formal specification extraction loop with AI tools and Apalache.</figcaption>
  </figure>
<p>In general, it looks like <strong>the answer is &quot;yes, but&quot;</strong>. The extraction-checking
loop stopped finding new mismatches between the behavior of a running ZooKeeper
replica and the extracted formal specification and harness. So I do not have
new logs to feed into Codex and Claude Code, and it is good time to reflect on
this.</p>
<p>Look carefully at Figure 1. Even though my AI agents had a lot of freedom in
coming up with their plans and implementing them, <strong>I did not let the agents run
wild</strong>.  I keep reading claims about &quot;autonomous agents&quot; and &quot;agentic loops&quot;,
where agents simulate unhealthy human management loops. I still had to read the
triage reports and implementation plans. Several times, had not I caught an
agent in planning to introduce really bad workarounds in the specification, we
would have gone down the rabbit hole of slop. Every iteration had a separate
commit, so we could keep track of regressions in the specification and harness.
Having said that, I admit that my reviews were high-level and intuitive, not
Github-level reviews.</p>
<p>What I believe is the killer feature of this approach is that <strong>it does not need
any pre-existing test suites</strong>. We do not mutate the existing tests. The model
checker <strong>finds new tests</strong>, including timeouts, crashes, TPC disconnects, etc.
Moreover, this approach requires <strong>zero code instrumentation</strong>. We do not have
to add any hooks or logging to the implementation. <strong>The test harness operates at
the TCP boundary</strong>.</p>
<p><strong>Did I burn thousands of dollars on this?</strong> Not at all. I did this case study
with two lowest-tier subscriptions to Codex and Claude Code, which cost me
<strong>about $80 for two months</strong> in total. (Given the news about <a href="https://www.theregister.com/2026/04/22/anthropic_removes_claude_code_pro/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Claude price
changes</a> and <a href="https://github.blog/news-insights/company-news/changes-to-github-copilot-individual-plans/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Copilot price changes</a>,
this becomes more expensive). Most of the time went into running the testing
experiments on my workstation: AMD Ryzen 9 5950X processor (16 physical, 32
logical cores), 128 GB RAM. The cool thing about my testing architecture is that
the machine was running 10-15 episodes of 300 steps in parallel on 10-20 cores,
totalling in 30000-90000 steps in a single campaign.  Hence, the AI tools had to
triage 1-30 counterexamples at once, before starting a new campaign.</p>
<p>Since we now live in a hype-driven world, I want to stress that <strong>this is still
an experiment</strong>. I am pretty sure that <a href="https://dl.acm.org/doi/abs/10.1145/3689031.3696069?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Ouyang et. al.</a> had much
more time to write their TLA<sup>+</sup> specifications of ZooKeeper and to
conduct their experiments.</p>
<p>If you read the blog post carefully, you will probably find some points that
could be investigated further. I have decided to time box this experiment and
report about it where it is.</p>
<div class="tip-box">
  <div class="tip-header"><strong>Tip:</strong></div>
  <div class="tip"><p>As I mentioned earlier, I stopped accompaying my
blog posts with complete artifacts. AI slop forks are real. It takes me time to
design and conduct the experiments on a beefy machine, as well as to find the
right format to interpret and explain the data. It only takes 10-15 minutes to
repackage the benchmarks and results with an AI tool, having the experimental
data. Hence, I am sharing my lab book with the customers and researchers, upon
request.</p>
</div>
</div>
<p><strong>Want to skip the long text?</strong> <a href="https://protocols-made-fun.com/zookeeper-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#conclusions">Jump to the
conclusions</a>.</p>
<h2 id="1.-the-effort" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/zookeeper-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#1.-the-effort">1. The effort</a></h2>
<p>This experiment took about two months, from March 2026 to April 2026. The git
repository has 336 commits in total. Except for several initial commits, each
new commit corresponds to a new iteration of the extraction-checking loop.</p>
<p>You can see the statistics in the figures below:</p>
<ul>
<li>Figure 2 shows the number of commits per day.</li>
<li>Figure 3 shows the number of lines added and deleted in the whole repository.</li>
<li>Figure 4 shows the number of lines added and deleted in the specification files.</li>
<li>Figure 5 shows the number of lines added and deleted in the test harness (zoomonkey).</li>
</ul>
<p>You can see that the commit volume decays with time. This is a sign of
convergence. The first week has the most commits and the most code added and
deleted. This was the bootstrapping phase. It's also interesting to observe a
big splash around the first-second week of April. This is where we start to
reach a new class of behaviors that did not match the implementation.</p>
  <figure>
    <a href="https://protocols-made-fun.com/img/zk-testing/git_stats_commits.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/zk-testing/git_stats_commits.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Git stats">
    </picture></a>
    <figcaption>Figure 2: Commit statistics in this experiment.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/zk-testing/git_stats_lines.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/zk-testing/git_stats_lines.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Git stats">
    </picture></a>
    <figcaption>Figure 3: Addition/deletion statistics in this experiment.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/zk-testing/git_stats_spec.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/zk-testing/git_stats_spec.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Git stats">
    </picture></a>
    <figcaption>Figure 4: Addition/deletion statistics in the specification files.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/zk-testing/git_stats_zoomonkey.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/zk-testing/git_stats_zoomonkey.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Git stats">
    </picture></a>
    <figcaption>Figure 5: Addition/deletion statistics in the test harness.</figcaption>
  </figure>
<h2 id="2.-extracting-formal-specifications" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/zookeeper-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#2.-extracting-formal-specifications">2. Extracting formal specifications</a></h2>
<p>As I learned with <a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">TFTP testing</a>, AI tools need a good predefined
architecture. Hence, I spent some time capturing this architecture in
<code>AGENTS.md</code> and <code>CLAUDE.md</code>. The formal specification is composed of several
modules, each corresponding to a subprotocol of ZooKeeper:</p>
<ol>
<li><strong>Module process</strong> captures the normal lifecycle of a ZooKeeper replica: <code>start</code>,
<code>on_started</code>, <code>on_stopped</code>. Crashes and restarts are handled by
the <strong>system</strong> module.</li>
<li><strong>Module tcp</strong> captures the standard TCP lifecycle: <code>connect</code>, <code>accept</code>, <code>disconnect</code>,
<code>half_close</code>, <code>reset</code>, <code>refused</code>.</li>
<li><strong>Module fle</strong> captures the Fast Leader Election protocol, which is used by ZooKeeper to
elect a leader among the replicas: <code>send_notification</code>, <code>rcv_notification</code>,
<code>become_leader</code>, <code>become_follower</code>, <code>restart_election</code>.</li>
<li><strong>Module zab</strong> captures the ZooKeeper Atomic Broadcast protocol and its clients. It has 22
actions, including <code>proposal</code>, <code>ack_proposal</code>, <code>commit</code>, <code>diff</code>, <code>trunc</code>, <code>snap</code>,
<code>client_connect</code>, <code>client_ping</code>,  <code>client_create</code>, <code>client_set_data</code>, etc.</li>
<li><strong>Module system</strong> composes the above modules and adds failures.</li>
</ol>
<p>These modules were written by the AI tools, by following the high-level
architecture, hands off the keyboard. To get the flavor of the specification,
look at one action from the specification of ZAB:</p>
<pre><code class="hljs language-python"><span class="hljs-meta">@action(<span class="hljs-params">inline=<span class="hljs-literal">False</span></span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">send_diff</span>(<span class="hljs-params">c: Context[ZabState], leader: Expr, follower: Expr, next_turn: Expr</span>):
    <span class="hljs-string">&quot;&quot;&quot;Leader sends DIFF to a follower (requires quorum of epoch_acked).&quot;&quot;&quot;</span>
    s = c.state
    c.assume(follower != leader)
    c.assume(_proc_up(s, leader))
    <span class="hljs-comment"># The leader must have completed its election before registering in</span>
    <span class="hljs-comment"># epoch_leader.  Without this guard a FOLLOWING replica whose</span>
    <span class="hljs-comment"># fle_current_vote happens to be targeted by another follower can</span>
    <span class="hljs-comment"># act as a second leader for the same epoch, violating leadership2.</span>
    c.assume(s.fle_role[leader] == LEADING)
    c.assume(_follower_targets_leader(s, follower, leader))
    c.assume(s.zab_sync[follower] == SYNC_EPOCH_ACKED)
    c.assume(_has_quorum_of_sync_state(s, leader, <span class="hljs-string">&quot;epoch_acked&quot;</span>))
    c.assume(_can_send_diff(s, leader, follower))
    <span class="hljs-comment"># Leader-initiated</span>
    c.assume(_turn_matches_iut_actor(s, leader, next_turn))
    next_s = s.edit()
    next_s.zab_sync[follower] = SYNC_DIFF_SENT
    next_s.zab_state[leader] = SYNCHRONIZATION
    next_s.zab_accepted_epoch[leader] = s.zab_current_epoch[leader]
    next_s.zab_persisted_accepted_epoch[leader] = s.zab_current_epoch[leader]
    <span class="hljs-comment"># By this point ACKEPOCH quorum has formed (see epoch_acked guard above),</span>
    <span class="hljs-comment"># which means ZK&#x27;s Leader.lead() has already called setCurrentEpoch on</span>
    <span class="hljs-comment"># disk. Bump the currentEpoch shadow here to match that disk write.</span>
    next_s.zab_persisted_current_epoch[leader] = s.zab_current_epoch[leader]
    next_s.epoch_leader[s.zab_current_epoch[leader]] = (
        s.epoch_leader[s.zab_current_epoch[leader]].union(<span class="hljs-type">Set</span>(leader))  <span class="hljs-comment"># type: ignore</span>
    )
    s.zab_action = ZabAction.SendDiff(  <span class="hljs-comment"># type: ignore</span>
        ZabDiff(leader=leader, follower=follower)
    )
</code></pre>
<p>As you can see, this is Python code, not TLA<sup>+</sup>. I noticed that the AI
tools are quite good at writing Python. Hence, they write the specification in a
Python DSL, which is automatically translated to TLA<sup>+</sup>. The test
harness is also written in Python, and it uses the <a href="https://github.com/apalache-mc/apalache/tree/main/json-rpc?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache JSON-RPC</a> to
interact with the model checker. <strong>If you are interested in the details of this
Python DSL, <a href="https://konnov.phd/?pmf=20260427&amp;utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site">contact me</a></strong>.</p>
<p>The fragment of the above action in TLA<sup>+</sup> looks like <a href="https://gist.github.com/konnov/38af0cbd45b68da819cd76f70859ed94?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#file-system-tla-L2272-L2311">this</a>.
The complete generated specification looks much more hairy. If you are still
curious, check its snapshot in <a href="https://gist.github.com/konnov/38af0cbd45b68da819cd76f70859ed94?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">this gist</a>.</p>
<p>In the table below, you can see the statistics on the formal specification.
Since the TLA<sup>+</sup> specification is generated from the Python code,
this specification is monolithic and has no submodules.</p>
<table>
<thead>
<tr>
<th>Module</th>
<th style="text-align:right">Actions</th>
<th style="text-align:right">Python LOC</th>
<th style="text-align:right">TLA<sup>+</sup> LOC</th>
</tr>
</thead>
<tbody>
<tr>
<td>process</td>
<td style="text-align:right">5</td>
<td style="text-align:right">90</td>
<td style="text-align:right">-</td>
</tr>
<tr>
<td>tcp</td>
<td style="text-align:right">6</td>
<td style="text-align:right">220</td>
<td style="text-align:right">-</td>
</tr>
<tr>
<td>fle</td>
<td style="text-align:right">5</td>
<td style="text-align:right">1605</td>
<td style="text-align:right">-</td>
</tr>
<tr>
<td>zab</td>
<td style="text-align:right">22</td>
<td style="text-align:right">2256</td>
<td style="text-align:right">-</td>
</tr>
<tr>
<td>system</td>
<td style="text-align:right">9</td>
<td style="text-align:right">1603</td>
<td style="text-align:right">-</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td style="text-align:right"></td>
<td style="text-align:right"><strong>5774</strong></td>
<td style="text-align:right"><strong>3065</strong></td>
</tr>
</tbody>
</table>
<h2 id="3.-generating-the-test-harness" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/zookeeper-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#3.-generating-the-test-harness">3. Generating the test harness</a></h2>
<p>The test harness is also written in Python. It is composed of several modules,
which are listed in the table below.</p>
<table>
<thead>
<tr>
<th>Subsystem</th>
<th>Modules</th>
<th style="text-align:right">Lines</th>
</tr>
</thead>
<tbody>
<tr>
<td>Orchestration</td>
<td>main.py, scheduler.py, runner.py</td>
<td style="text-align:right">3462</td>
</tr>
<tr>
<td>Validation</td>
<td>oracle.py, serde.py</td>
<td style="text-align:right">2493</td>
</tr>
<tr>
<td>Networking / wire</td>
<td>comms.py, client_wire.py, quorum_wire.py, election_wire.py</td>
<td style="text-align:right">3063</td>
</tr>
<tr>
<td>Data model / support</td>
<td>events.py, queues.py, config.py, fixed_tree.py, log.py</td>
<td style="text-align:right">867</td>
</tr>
<tr>
<td>Tooling</td>
<td>log_to_mermaid.py</td>
<td style="text-align:right">414</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td></td>
<td style="text-align:right"><strong>10299</strong></td>
</tr>
</tbody>
</table>
<p>The interesting design choice here is that the test harness runs a <strong>single
replica of ZooKeeper</strong>. We call this replica <strong>implementation under test</strong>
(IUT). The whole distributed system exists only in the formal specification and
its behavior. This is conceptually similar to a <a href="https://en.wikipedia.org/wiki/Digital_twin?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Digital Twin</a> of the real
distributed system.</p>
<p>Since most of the behavior exists only in the specification, this approach is
sensitive to <strong>quick and accurate</strong> choice of events. I believe that a random
simulator would not help us much here, as it would keep crunching through a very
large set of unproductive events.</p>
<p>This is where the new <a href="https://github.com/apalache-mc/apalache/tree/main/json-rpc?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache JSON-RPC</a> comes into play. The test harness
chooses the next action to execute and asks the symbolic model checker to find
the action parameters that would enable it. It also calls Apalache to check the
state invariants and find out whether the implementation's output matches the
specification. Since the complexity of SMT solving grows with the number of
steps very quickly, we use the new method <a href="https://github.com/apalache-mc/apalache/tree/main/json-rpc?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#311-method-compact">compact</a> to prune the symbolic
context and keep it manageable.</p>
<h2 id="4.-running-the-test-harness" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/zookeeper-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#4.-running-the-test-harness">4. Running the test harness</a></h2>
<p>Running the test harness looks quite boring:</p>
<pre><code class="hljs language-sh">$ ./scripts/run-parallel.sh 8 -- --replicas 3 --episodes 20 --steps 500 \
  --failure-rate 0.2 --fle-rate 0.05 --fallback-rate 0.1 --decay 0.8 --crashes 0
</code></pre>
<p>It basically runs 20 episodes of 500 steps in parallel, with 3 replicas and a
number of parameters to control the test scenario generation. The script is
using <a href="https://www.gnu.org/software/parallel/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">GNU Parallel</a> to run the episodes in parallel. Since each test runs <strong>a
single actual replica</strong> and simulates the rest of the distributed system with
the specification, running multiple experiments in parallel is easy. We only
have to make sure that different experiments get assigned different ports.</p>
<p>Every episode produces a detailed log of events. If it finds an invariant
violation or a mismatch between the behavior of the real replica and the
specification, it produces a trace in the <a href="https://apalache-mc.org/docs/adr/015adr-trace.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">ITF format</a>. These logs and traces
are read by the AI tools to triage the mismatches and to improve the
specification and the test harness.</p>
<p>Similar to <a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">tftp-testing</a>, I have a script to convert the logs into a sequence
chart in Mermaid. However, for a system of this complexity, these diagrams are
hard to digest. Instead, I produce a high-level figure of the test campaign that
shows the events in all episodes in one big picture. See Figure 6. Click on it
to see the full-size version and examine it in detail.</p>
  <figure>
    <a href="https://protocols-made-fun.com/img/zk-testing/episodes-summary.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/zk-testing/episodes-summary.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Episodes summary">
    </picture></a>
    <figcaption>Figure 6: Episodes summary of the test campaign (view the full-size image by clicking).</figcaption>
  </figure>
<p>If you look at the events in the figure, you will see that the most episodes
have productive events such as ZAB proposals, commits, diffs, snapshots, client
operations, etc. However, a few episodes degrade into permanent leader election,
where the implementation-under-test keeps sending FLE notifications. Basically,
the two simulated replicas keep working together and exclude the IUT from the
quorum.</p>
<h2 id="5.-triaging-conformance-mismatches" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/zookeeper-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#5.-triaging-conformance-mismatches">5. Triaging conformance mismatches</a></h2>
<p>Back in October 2025, Copilot + Sonnet 4.5 were quite bad at triaging
specification mismatches. Now, the frontier models are quite good at it. This is
definitely an improvement in the frontier models.  I also believe that my effort
of definining a good architecture for the test harness paid off this time. Below
are fragments of a triage report by Claude Code Opus 4.7:</p>
<div markdown="1" class="ai-output-frame">
<p>A single oracle-reported spec violation landed in the 2026-04-24 parallel
sweep. The dump files live at:</p>
<ul>
<li>logs/20260424_071307/episode_009_step_160_spec_violation.itf.json</li>
<li>logs/20260424_071307/episode_009_step_160_spec_violation_trace.itf.json</li>
</ul>
<p>Configuration: inst03 (PERSIST_IUT_STATE=True), 3 replicas,
permutation {1: 2, 2: 3, 3: 1}, IUT is spec replica 1 / dynamic id 2.
Violation reason: output_queue_violation — the oracle could not validate
the IUT's zab_follower_info output after 10 drain passes.</p>
<p>Episode timeline (relevant subset)</p>
<ol>
<li>Replica 1 starts at 10:32; long FLE churn; becomes follower; emits
zab_follower_info at 10:41 with accepted_epoch=0 — spec accepts
(transition 31). No zab_leader_info ever validated in this episode.</li>
<li>At 10:58 replica 1 is stopped (tester action, not crash). On-disk
acceptedEpoch at that moment: still 0 (follower never received
LEADERINFO, never persisted).</li>
<li>At 11:06:52 replica 1 restarts. inst03 preserves the data directory,
so on-disk acceptedEpoch is read back as 0.</li>
<li>At 11:12:34 IUT emits fle_become_leader(r1) (transition 27). Spec
speculatively bumps zab_accepted_epoch[1] to 1 via
candidate_epoch_capped in spec/system.py:665.</li>
<li>At 11:12:42 scheduler picks simulated→IUT zab_follower_info
(transition 31 on the simulated follower side). Spec's
send_followerinfo (spec/zab.py:900) atomically observes leader-side
quorum (1 IUT + 1 simulated ≥ majority of 3) and bumps
zab_persisted_accepted_epoch[1] at spec/zab.py:972 — the comment
at line 1068 acknowledges this is the earliest-disk-write model of
Leader.getEpochToPropose().</li>
<li>At 11:12:48 tcp_disconnect r1 ↔ r3 (quorum).</li>
<li>At 11:12:50 IUT emits fle_restart_election(r1) (transition 30). Spec
rolls back zab_accepted_epoch[1] to
zab_persisted_accepted_epoch[1] per commit aee611c
(spec/system.py:648-693) — which is already 1 due to step 5, so the
rollback is a no-op on the live field and the shadow stays at 1.</li>
<li>At 11:14:16 replica 1 (now FOLLOWING r2 in both spec and IUT) emits
zab_follower_info(accepted_epoch=0). Spec expects 1. Blocked.</li>
</ol>
<p>Root cause</p>
<p>The spec assumes Leader.getEpochToPropose()'s disk write has completed
atomically the moment any FOLLOWERINFO arrival would establish quorum —
that's the design of the zab_persisted_accepted_epoch[leader] bump in
send_followerinfo (spec/zab.py:972). In this trace the IUT's leader
thread either never processed the simulated FOLLOWERINFO at all (the
incoming quorum connection to r1 was disrupted around that time — the
TCP disconnect at 11:12:48 is between r1 and r3, but similar disruption
to the r2–r1 quorum path around 11:12:42–48 would leave
getEpochToPropose() blocked), or processed it but was interrupted
before the disk write completed. Either way, the IUT's on-disk
acceptedEpoch stayed at 0 while the spec's shadow went to 1.</p>
<p>The aee611c rollback is correctly applied but operates on the live
epoch field only; by design it restores live from shadow, so when the
shadow itself is stale-high, restart_election can't recover. This is
the new Family-A sub-variant noted in the earlier audit: speculative
advance of the persisted shadow, not just the live epoch.</p>
</div>
<p>Several things are impressive here:</p>
<ol>
<li>
<p><strong>The test harness stopped a replica and dropped a TCP connection at the
right moments</strong>, so the replica did not have a chance to persist the new
accepted epoch.  It did not happen often, but the parallel campaign was diverse
enough to trigger this scenario. To be fair, the initial version of the test
harness would not be able to trigger this scenario. <strong>I had to teach the AI
tools to properly diversify the test scenarios</strong>.</p>
</li>
<li>
<p><strong>Claude figured this out in a matter of minutes</strong>. It would be hard for me to
figure this out.</p>
</li>
<li>
<p><strong>It also proposed a fix.</strong></p>
</li>
</ol>
<h2 id="6.-checking-invariants-and-producing-examples" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/zookeeper-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#6.-checking-invariants-and-producing-examples">6. Checking invariants and producing examples</a></h2>
<p>Since the AI tools write the specification and the test harness, we have to
evaluate the quality of the specification and the harness together. To this end,
we do two things:</p>
<ol>
<li>Add state invariants to evaluate safety.</li>
<li>Add state examples to illustrate reachability of interesting states.</li>
</ol>
<h3 id="6.1.-state-invariants" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/zookeeper-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#6.1.-state-invariants">6.1. State invariants</a></h3>
<p>To our luck, ZooKeeper already has several <a href="https://github.com/Disalg-ICS-NJU/zookeeper-tla-spec?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">TLA<sup>+</sup>
specifications</a> for earlier versions. I let the AI tools
harvest these specifications for invariants.</p>
<p>For example, these are the shortest invariants these tools wrote:</p>
<pre><code class="hljs language-python"><span class="hljs-meta">@invariant</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">leadership1</span>(<span class="hljs-params">s: SystemState</span>):
    <span class="hljs-keyword">return</span> s.REPLICA.forall(
        <span class="hljs-keyword">lambda</span> i: s.REPLICA.forall(
            <span class="hljs-keyword">lambda</span> j: (
                _is_established_leader(s, i)
                &amp; _is_established_leader(s, j)
                &amp; (s.zab_accepted_epoch[i] == s.zab_accepted_epoch[j])
            ).implies(i == j)
        )
    )

<span class="hljs-meta">@invariant</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">leadership2</span>(<span class="hljs-params">s: SystemState</span>):
    <span class="hljs-keyword">return</span> <span class="hljs-type">Set</span>(Val(<span class="hljs-number">1</span>), ..., Val(s.MAX_EPOCH)).forall(
        <span class="hljs-keyword">lambda</span> epoch: s.epoch_leader[epoch].size &lt;= Val(<span class="hljs-number">1</span>)
    )

<span class="hljs-meta">@invariant</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">fle_wait_finalize_sound</span>(<span class="hljs-params">s: SystemState</span>):
    <span class="hljs-keyword">return</span> s.REPLICA.forall(
        <span class="hljs-keyword">lambda</span> replica: (
            _fle_invariant_replica_live(s, replica) &amp; s.fle_wait_finalize[replica]
        ).implies(
            _fle_has_proposed_recv_quorum(s, replica)
            | _fle_has_local_ooe_quorum(s, replica)
        )
    )
</code></pre>
<p>Their TLA<sup>+</sup> translations look like this:</p>
<pre><code class="hljs language-tla"><span class="hljs-title function_">Leadership1</span> <span class="hljs-operator">==</span>
    <span class="hljs-keyword">\A</span> i142 <span class="hljs-keyword">\in</span> REPLICA: <span class="hljs-keyword">\A</span> j143 <span class="hljs-keyword">\in</span> REPLICA:
        (<span class="hljs-operator">/\</span> <span class="hljs-operator">/\</span> <span class="hljs-operator">/\</span> (fle_role[i142] <span class="hljs-operator">=</span> <span class="hljs-string">&quot;LEADING&quot;</span>)
               <span class="hljs-operator">/\</span> <span class="hljs-operator">\/</span> (zab_state[i142] <span class="hljs-operator">=</span> <span class="hljs-string">&quot;synchronization&quot;</span>)
                  <span class="hljs-operator">\/</span> (zab_state[i142] <span class="hljs-operator">=</span> <span class="hljs-string">&quot;broadcast&quot;</span>)
            <span class="hljs-operator">/\</span> <span class="hljs-operator">/\</span> (fle_role[j143] <span class="hljs-operator">=</span> <span class="hljs-string">&quot;LEADING&quot;</span>)
               <span class="hljs-operator">/\</span> <span class="hljs-operator">\/</span> (zab_state[j143] <span class="hljs-operator">=</span> <span class="hljs-string">&quot;synchronization&quot;</span>)
                  <span class="hljs-operator">\/</span> (zab_state[j143] <span class="hljs-operator">=</span> <span class="hljs-string">&quot;broadcast&quot;</span>)
         <span class="hljs-operator">/\</span> (zab_accepted_epoch[i142] <span class="hljs-operator">=</span> zab_accepted_epoch[j143])) <span class="hljs-operator">=&gt;</span> ((i142 <span class="hljs-operator">=</span> j143))

<span class="hljs-title function_">Leadership2</span> <span class="hljs-operator">==</span>
    <span class="hljs-keyword">\A</span> epoch144 <span class="hljs-keyword">\in</span> (<span class="hljs-number">1</span>)<span class="hljs-operator">..</span>(MAX_EPOCH): (<span class="hljs-keyword">Cardinality</span>(epoch_leader[epoch144]) <span class="hljs-operator">&lt;=</span> <span class="hljs-number">1</span>)
</code></pre>
<p>The translation of <code>fle_wait_finalize_sound</code> is a bit longer, you can check it
in the
<a href="https://gist.github.com/konnov/38af0cbd45b68da819cd76f70859ed94?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#file-system-tla-L535-L559">FleWaitFinalizeSound</a>.</p>
<p>We have 11 invariants in total. The other 8 invariants are more complex. These
invariants are checked by the test harness with Apalache. We can also check them
against the generated TLA<sup>+</sup> specification.</p>
<h3 id="6.2.-reachability-examples" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/zookeeper-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#6.2.-reachability-examples">6.2. Reachability examples</a></h3>
<p>I usually write &quot;falsy invariants&quot; to check reachability of interesting states.
Again, the AI tools are quite good at writing such &quot;examples&quot;. For instance:</p>
<pre><code class="hljs language-python"><span class="hljs-meta">@example</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">at_least_one_committed</span>(<span class="hljs-params">s: SystemState</span>):
    <span class="hljs-keyword">return</span> s.REPLICA.exists(
        <span class="hljs-keyword">lambda</span> replica: s.zab_last_committed[replica].index &gt;= Val(<span class="hljs-number">1</span>)
    )
</code></pre>
<p>This example is translated to the following TLA<sup>+</sup> invariant:</p>
<pre><code class="hljs language-tla"><span class="hljs-title function_">AtLeastOneCommitted</span> <span class="hljs-operator">==</span>
    <span class="hljs-operator">~</span>(<span class="hljs-keyword">\E</span> replica63 <span class="hljs-keyword">\in</span> REPLICA: (zab_last_committed[replica63].index <span class="hljs-operator">&gt;=</span> <span class="hljs-number">1</span>))
</code></pre>
<p>To see an instance of this example, I ran a test campaign with 10 episodes of
100 steps each. One of the episodes reached a state that satisfies
<code>at_least_one_committed</code>. Below is the summary of this episode by Claude Code
Opus 4.7:</p>
<div markdown="1" class="ai-output-frame">
<p>Trace summary — example of at_least_one_committed</p>
<p>Episode 5, step 86. Replica mapping: spec 1 ↔ dynamic 2 (the IUT), spec 2 ↔ dynamic 1, spec 3 ↔ dynamic 3. So the IUT
is spec replica 1 in the analysis below.</p>
<p>What happened, in order:</p>
<ol>
<li>Initial election attempt fails. Spec 1 (IUT) briefly thinks it's leader (fle_become_leader replica=1 at t≈+25s) but
then has to fle_restart_election (t≈+45s). Spec 3 simultaneously becomes leader (fle_become_leader replica=3 at
t≈+19s); spec 2 becomes follower early on.</li>
<li>Stable leadership formed under spec 3. After the restart, IUT (spec 1) joins as follower of spec 3
(fle_become_follower replica=1, then zab_follower_info {follower_id:1, leader_id:3}).</li>
<li>ZAB recovery completes. The leader (spec 3) walks IUT through LEADERINFO → DIFF → NEWLEADER → UPTODATE (all with
follower_id:1, leader_id:3). A second LEADERINFO/NEWLEADER/UPTODATE round (no ids logged) syncs the other simulated
follower spec 2. All three replicas reach zab_state = &quot;broadcast&quot;.</li>
<li>Write submitted to the leader. Step 85: a simulated client connects to spec 3 (the leader) and issues
zab_client_create(&quot;/p1&quot;).</li>
<li>Proposal phase (step 86):
<ul>
<li>Leader (spec 3) emits zab_proposal at zxid=1, epoch=1.</li>
<li>A follower acks: zab_ack_proposal zxid=1 validates cleanly (transition 52).</li>
<li>With its own self-ack the leader reaches quorum and commits locally.</li>
<li>Oracle then detects the violated invariant — i.e. the dual @example fired.</li>
</ul>
</li>
</ol>
<p>Violation state (spec replica 3 only):</p>
<ul>
<li>zab_history[3] = [{create, /p1, zxid=1, epoch=1}]</li>
<li index:1,="" zxid:1="">zab_last_committed[3] =</li>
<li>zab_committed_zxid = 1</li>
<li>Replicas 1 (IUT) and 2 still have zab_last_committed.index = 0 and empty histories — the commit has not yet been
broadcast to them.</li>
</ul>
</div>
<p>Below is the sequence diagram of this full episode. Click on it to see the full-size
version and examine it in detail. It is quite long, so feel free to scroll
through it.</p>
<figure>
    <a href="https://protocols-made-fun.com/img/zk-testing/at_least_one_committed.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/zk-testing/at_least_one_committed.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="At least one committed">
    </picture></a>
    <figcaption>Figure 7: A trace where at least one replica has committed (view the full-size image by clicking).</figcaption>
</figure>
<p>The table below shows the example coverage in two test campaigns (20 episodes of
100 steps and 20 episodes of 200 steps).</p>
<table>
<thead>
<tr>
<th>Example</th>
<th style="text-align:right">Times found</th>
<th style="text-align:right">Min step</th>
<th style="text-align:right">Max step</th>
</tr>
</thead>
<tbody>
<tr>
<td>at_least_one_committed</td>
<td style="text-align:right">1</td>
<td style="text-align:right">48</td>
<td style="text-align:right">48</td>
</tr>
<tr>
<td>at_least_two_committed</td>
<td style="text-align:right">1</td>
<td style="text-align:right">74</td>
<td style="text-align:right">74</td>
</tr>
<tr>
<td>some_follower_synced</td>
<td style="text-align:right">4</td>
<td style="text-align:right">26</td>
<td style="text-align:right">43</td>
</tr>
<tr>
<td>quorum_recovery_completed</td>
<td style="text-align:right">7</td>
<td style="text-align:right">21</td>
<td style="text-align:right">185</td>
</tr>
<tr>
<td>newleader_sent</td>
<td style="text-align:right">3</td>
<td style="text-align:right">23</td>
<td style="text-align:right">42</td>
</tr>
<tr>
<td>forwarded_request_sent</td>
<td style="text-align:right">1</td>
<td style="text-align:right">46</td>
<td style="text-align:right">46</td>
</tr>
<tr>
<td>forwarded_request_received</td>
<td style="text-align:right">1</td>
<td style="text-align:right">68</td>
<td style="text-align:right">68</td>
</tr>
<tr>
<td>forwarded_write_committed</td>
<td style="text-align:right">1</td>
<td style="text-align:right">74</td>
<td style="text-align:right">74</td>
</tr>
<tr>
<td>proposal_in_flight</td>
<td style="text-align:right">2</td>
<td style="text-align:right">46</td>
<td style="text-align:right">47</td>
</tr>
<tr>
<td>proposal_has_quorum_acks</td>
<td style="text-align:right">1</td>
<td style="text-align:right">74</td>
<td style="text-align:right">74</td>
</tr>
<tr>
<td>two_proposals_in_flight</td>
<td style="text-align:right">1</td>
<td style="text-align:right">72</td>
<td style="text-align:right">72</td>
</tr>
<tr>
<td>first_write_committed_on_quorum</td>
<td style="text-align:right">1</td>
<td style="text-align:right">48</td>
<td style="text-align:right">48</td>
</tr>
<tr>
<td>two_distinct_znodes_committed</td>
<td style="text-align:right">1</td>
<td style="text-align:right">74</td>
<td style="text-align:right">74</td>
</tr>
</tbody>
</table>
<p><span id="conclusions"></span></p>
<h2 id="7.-conclusions" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/zookeeper-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#7.-conclusions">7. Conclusions</a></h2>
<p>Obviously, the AI tools change the way we should test distributed systems.
Interestingly, my conversations with these tools show that they have very little
understanding of distributed computations. Funny enough, even though they have
all the knowledge about TCP/IP at their fingertips, if you ask them right, they
cannot efficiently operate with this knowledge. However, when they have plenty
of counterexamples to learn from, they improve the quality of the testing
harness very quickly. This is where the interplay of formal verification and AI
tools becomes really powerful. The model checker produces negative and positive
examples, and the AI tools learn from them and improve the specification and the
test harness.</p>
<p><strong>The good</strong>:</p>
<ul>
<li>
<p>It is actually <strong>possible to extract formal specifications from the source
code</strong> of a real distributed system and to write test harnesses with AI tools.
We have to keep in mind that this requires a verification loop, which uses a
tool such as <a href="https://apalache-mc.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache</a>.</p>
</li>
<li>
<p>In this experiment, we extracted a <strong>modular specification that captures five
protocols</strong>.</p>
</li>
<li>
<p>If we do not try to one-shot the testing process and <strong>follow an iterative
process with a clear pre-defined architecture</strong>, the AI tools actually help us.
<strong>&quot;Test it and make no mistakes&quot; obviously does not work</strong>.</p>
</li>
<li>
<p>Comparing to <a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">tftp-testing</a>, the <strong>AI tools in 2026 are much better</strong> at
triaging specification mismatches and producing fixes. The whole process is
<strong>much less energy-draining now</strong>.</p>
</li>
</ul>
<p><strong>The bad</strong>:</p>
<ul>
<li>
<p>I have <strong>no idea about the extracted specification</strong>. When I write a
specification by hand, I internalize the protocol behavior.  Even after I
forget the details, I can still come back and recover them from the spec.
Here, it is much harder.</p>
</li>
<li>
<p>If we focus on bug finding, it is fine to have a hard-to-understand
specification. However, <strong>from the maintainability perspective, it is a big
problem</strong>. This is probably why we see such a spike in security bugs, but no so
much in real software products.</p>
</li>
</ul>
<p>Even though the whole development is quite exciting, my main takeaway is that
<strong>writing formal specifications is still a human job</strong>. AI tools
can assist us in producing test harness and finding issues.</p>
<p>If you need help with writing formal specifications and producing test
harnesses, contact me. I can help you with that. It still takes time, expertise,
and effort to do it right. Also, coming up with the right architecture is not as
easy as it may seem. Of course, you can hire an intern and spend several months
learning from your own mistakes together. Or you can fast-forward it and hire
me.</p>
<h2 id="want-to-talk%3F" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/zookeeper-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#want-to-talk%3F">Want to talk?</a></h2>
<!-- References -->
]]></content>
    <author>
      <name>Igor Konnov</name>
      <email>igor@konnov.phd</email>
      <uri>https://konnov.phd/?utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site</uri>
    </author>
    
    
    <category term="testing" />
    
    <category term="model-checking" />
    
    
    
  </entry>
  
  
  <entry>
    <title type="html">TLC breadth-first search vs random simulation</title>
    <link href="https://protocols-made-fun.com/tlc-vs-simulation.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" rel="alternate" type="text/html" title="TLC breadth-first search vs random simulation" />
    <published>2026-04-30T00:00:00.000Z</published>
    <updated>2026-04-30T00:00:00.000Z</updated>
    <id>https://protocols-made-fun.com/tlc-vs-simulation.html</id>
    <content type="html" xml:base="https://protocols-made-fun.com/tlc-vs-simulation.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"><![CDATA[<p><strong>Author:</strong> <a href="https://konnov.phd/?utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site">Igor Konnov</a></p>
<p><strong>Date:</strong> April 30, 2026</p>
<p>Recently, I wrote a blog post on <a href="https://protocols-made-fun.com/random-walks.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">random walks</a> that compared the state
coverage of random walks for increasingly larger sets of experiments: 100
thousands, 1 million, 10 million, and even 100 million episodes. There, I used
custom-built simulators in Rust to randomly walk through the state spaces of
several TLA<sup>+</sup> benchmarks: two-phase commit, readers-writers, and
FPaxos.</p>
<p>A. Jesse Jiryu Davis noticed my blog post and <a href="https://groups.google.com/g/tlaplus/c/iFUAhlsIuQQ/m/t044etF6AwAJ?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">wrote a
message</a> on the TLA<sup>+</sup> Google group. Since the
random walks in the blog post are not exactly the same as the random simulation
in TLC, we both wondered how the TLC simulation mode compares to the model
checker in terms of coverage and running times. Markus Kuppe shared the options
to collect distinct state coverage in the TLC simulation mode, see the
<a href="https://groups.google.com/g/tlaplus/c/iFUAhlsIuQQ/m/t044etF6AwAJ?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">discussion</a>.</p>
<p>The <strong>important distinction</strong> between the <a href="https://protocols-made-fun.com/random-walks.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">random walks</a> and the TLC
simulator is that <strong>a random walk chooses one successor state at each step</strong>,
whereas <strong>TLC enumerates all successor states and then chooses one of them
uniformly at random.</strong> Markus explains the rationale behind this design choice
in TLC:</p>
<blockquote>
<p>...enumerating all successors is useful for more than just choosing
the next step: TLC can also check invariants on all generated successor states,
not only on the one that ends up being sampled. That is a meaningful benefit
when the goal is to catch bugs, not just drive a walk.</p>
</blockquote>
<p>This new blog post explores this direction. To get more details about the
bennchmarks, read the original blog post on <a href="https://protocols-made-fun.com/random-walks.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">random walks</a>.</p>
<p>Look at two groups of figures below. They summarize the results of running
random walks on specifications of three prominent distributed protocols:
two-phase commit, readers-writers, and FPaxos (see <a href="https://protocols-made-fun.com/tlc-vs-simulation.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#benchmarks">Benchmarks</a>).
The figures show the coverage achieved by the TLC simulation mode, with 100%
being the numbers of distinct states (reported by TLC). All running times are
given for an AMD Ryzen 9 5950X processor (16 physical, 32 logical cores), 128 GB
memory.</p>
<p>Importantly, the TLC simulations are run on <strong>a single worker</strong>, they are not
running in parallel. We do that, in order to compute the state coverage
precisely. When you look at running times, keep in mind that TLC can run
multiple simulation workers in parallel.</p>
<div class="tip-box">
  <div class="tip-header"><strong>Tip:</strong></div>
  <div class="tip"><p>In contrast to the previous blog posts, I do not
provide the artifacts for download. AI slop forks are real. It still takes me
several days to design and conduct the experiments on a beefy machine, as well
as to find the right format to interpret and plot the data. It only takes 10-15
minutes to repackage the benchmarks and results with an AI tool, having the
experimental data. Hence, I am sharing my lab book with the customers and
researchers, upon request.</p>
</div>
</div>
<h2 id="1.-coverage-for-minimal-instances" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tlc-vs-simulation.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#1.-coverage-for-minimal-instances">1. Coverage for minimal instances</a></h2>
<p>In this set of experiments, we run <strong>TLC simulations</strong> for the minimal instances
of the benchmarks. We start with the <strong>meaningful default</strong> of 100,000
simulation runs, with at most 100 steps per run. (Mind that the successor set is
computed at each step.) As you can see from Figure 1, the coverage is close to
100%, but it's not complete. Interestingly, 10 million runs give us 99.9%
coverage.</p>
<div class="figure-grid">
  <figure>
    <a href="https://protocols-made-fun.com/img/tlc-vs-simulation/twophase-n2-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
       target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/tlc-vs-simulation/twophase-n2-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Coverage of random walks for the two-phase commit benchmark with 2 resource managers">
    </picture></a>
    <figcaption>Figure 1.a: Two-phase commit, 2 RMs.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/tlc-vs-simulation/twophase-n3-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
       target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/tlc-vs-simulation/twophase-n3-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Coverage of random walks for the two-phase commit benchmark with 3 resource managers">
    </picture></a>
    <figcaption>Figure 1.b: Two-phase commit, 3 RMs.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/tlc-vs-simulation/rw-inst3-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
       target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/tlc-vs-simulation/rw-inst3-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Coverage of random walks for the readers-writers benchmark with 3 actors">
    </picture></a>
    <figcaption>Figure 1.c: Readers-writers, 3 actors.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/tlc-vs-simulation/fpaxos-inst2-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
       target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/tlc-vs-simulation/fpaxos-inst2-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Coverage of random walks for the FPaxos benchmark with 2 acceptors">
    </picture></a>
    <figcaption>Figure 1.d: FPaxos, 2 acceptors.</figcaption>
  </figure>
</div>
<h2 id="2.-running-times-for-tiny-instances" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tlc-vs-simulation.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#2.-running-times-for-tiny-instances">2. Running times for tiny instances</a></h2>
<p><strong>All of the above benchmarks are quite small by the model checking
standards. They have tens of thousands of states. It takes TLC only 1-3 seconds
to explore the state space and check the invariants for each of these
benchmarks.</strong></p>
<p>Figure 2 shows the running times for the above simulation benchmarks. The dashed
lines show the running times for the TLC model checker to explore the complete
state space.  Additionally, the right-hand y-axis shows the slowdown factor of
the simulations compared to the model checker. For example, for the two-phase
commit benchmark with 3 resource managers, the model checker takes about 2
seconds, while 10 million simulations take about 2 hours, which is a slowdown
factor of about 10,000. As you can see, 10 million simulations take hours, where
the model checker needs several seconds.</p>
<div class="figure-grid">
  <figure>
    <a href="https://protocols-made-fun.com/img/tlc-vs-simulation/twophase-n2-runtime.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
       target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/tlc-vs-simulation/twophase-n2-runtime.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Running times for the two-phase commit benchmark with 2 resource managers">
    </picture></a>
    <figcaption>Figure 2.a: Two-phase commit, 2 RMs.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/tlc-vs-simulation/twophase-n3-runtime.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
       target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/tlc-vs-simulation/twophase-n3-runtime.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Running times for the two-phase commit benchmark with 3 resource managers">
    </picture></a>
    <figcaption>Figure 2.b: Two-phase commit, 3 RMs.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/tlc-vs-simulation/rw-inst3-runtime.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
       target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/tlc-vs-simulation/rw-inst3-runtime.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Running times for the readers-writers benchmark with 3 actors">
    </picture></a>
    <figcaption>Figure 2.c: Readers-writers, 3 actors.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/tlc-vs-simulation/fpaxos-inst2-runtime.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
       target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/tlc-vs-simulation/fpaxos-inst2-runtime.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Running times for the FPaxos benchmark with 2 acceptors">
    </picture></a>
    <figcaption>Figure 2.d: FPaxos, 2 acceptors.</figcaption>
  </figure>
</div>
<h2 id="3.-slightly-larger-instances" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tlc-vs-simulation.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#3.-slightly-larger-instances">3. Slightly larger instances</a></h2>
<p>What happens if we take the instances that are still small, but have 1-2
participants more? Figure 3 shows the results of running TLC simulations on
these instances.</p>
<p>As you can see, with the meaningful default of 100,000 random walks, we achieve
poor coverage on readers-writers and FPaxos, though the coverage on two-phase
commit is nearly 99%. So this TLC simulations achieve much better coverage on
two-phase commit than <a href="https://protocols-made-fun.com/random-walks.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">random walks</a>, but they have comparable coverage on the
readers-writers and FPaxos benchmarks! You can also switch between this blog
post and <a href="https://protocols-made-fun.com/random-walks.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">random walks</a> to see the difference in coverage between the two
approaches.</p>
<p>To stress the message of the previous blog post, these instances are <strong>not that
large by the model checking standards</strong>.</p>
<div class="figure-grid">
  <figure>
    <a href="https://protocols-made-fun.com/img/tlc-vs-simulation/twophase-n5-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
       target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/tlc-vs-simulation/twophase-n5-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Coverage of random walks for the two-phase commit benchmark with 5 resource managers">
    </picture></a>
    <figcaption>Figure 3.a: Two-phase commit, 5 RMs.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/tlc-vs-simulation/rw-inst4-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
       target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/tlc-vs-simulation/rw-inst4-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Coverage of random walks for the readers-writers benchmark with 4 actors">
    </picture></a>
    <figcaption>Figure 3.b: Readers-writers, 4 actors.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/tlc-vs-simulation/fpaxos-inst3-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
       target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/tlc-vs-simulation/fpaxos-inst3-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Coverage of random walks for the FPaxos benchmark with 3 acceptors">
    </picture></a>
    <figcaption>Figure 3.c: FPaxos, 3 acceptors.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/tlc-vs-simulation/fpaxos-inst4-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
       target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/tlc-vs-simulation/fpaxos-inst4-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Coverage of random walks for the FPaxos benchmark with 4 acceptors">
    </picture></a>
    <figcaption>Figure 3.d: FPaxos, 4 acceptors.</figcaption>
  </figure>
</div>
<h2 id="4.-running-times-for-larger-instances" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tlc-vs-simulation.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#4.-running-times-for-larger-instances">4. Running times for larger instances</a></h2>
<p>Again, <strong>it takes the model checker TLC up to 10 minutes to enumerate all the
states and check the invariants for these instances</strong>, whereas we have been
<strong>running the simulations for hours!</strong>.</p>
<div class="figure-grid">
  <figure>
    <a href="https://protocols-made-fun.com/img/tlc-vs-simulation/twophase-n5-runtime.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
       target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/tlc-vs-simulation/twophase-n5-runtime.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Running times for the two-phase commit benchmark with 5 resource managers">
    </picture></a>
    <figcaption>Figure 4.a: Two-phase commit, 5 RMs.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/tlc-vs-simulation/rw-inst4-runtime.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
       target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/tlc-vs-simulation/rw-inst4-runtime.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Running times for the readers-writers benchmark with 4 actors">
    </picture></a>
    <figcaption>Figure 4.b: Readers-writers, 4 actors.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/tlc-vs-simulation/fpaxos-inst3-runtime.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
       target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/tlc-vs-simulation/fpaxos-inst3-runtime.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Running times for the FPaxos benchmark with 3 acceptors">
    </picture></a>
    <figcaption>Figure 4.c: FPaxos, 3 acceptors.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/tlc-vs-simulation/fpaxos-inst4-runtime.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
       target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/tlc-vs-simulation/fpaxos-inst4-runtime.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Running times for the FPaxos benchmark with 4 acceptors">
    </picture></a>
    <figcaption>Figure 4.d: FPaxos, 4 acceptors.</figcaption>
  </figure>
</div>
<h2 id="5.-conclusions" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tlc-vs-simulation.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#5.-conclusions">5. Conclusions</a></h2>
<p>I am not going to repeat the <a href="https://protocols-made-fun.com/random-walks.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#6-conclusions">conclusions from the previous blog
post</a>. They are still valid. The TLC simulation
mode achieves better coverage than the random walks on two-phase commit, but it
has comparable coverage on readers-writers and FPaxos. The running times of the
TLC simulator with a single-worker are worse than the model checker and the
random walks.</p>
<h2 id="want-to-talk%3F" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tlc-vs-simulation.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#want-to-talk%3F">Want to talk?</a></h2>
<!-- References -->
]]></content>
    <author>
      <name>Igor Konnov</name>
      <email>igor@konnov.phd</email>
      <uri>https://konnov.phd/?utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site</uri>
    </author>
    
    
    <category term="model-checking" />
    
    <category term="simulation" />
    
    
    
  </entry>
  
  
  <entry>
    <title type="html">Specification debugging as code generation</title>
    <link href="https://protocols-made-fun.com/debug-as-code-generation.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" rel="alternate" type="text/html" title="Specification debugging as code generation" />
    <published>2026-03-23T00:00:00.000Z</published>
    <updated>2026-03-23T00:00:00.000Z</updated>
    <id>https://protocols-made-fun.com/debug-as-code-generation.html</id>
    <content type="html" xml:base="https://protocols-made-fun.com/debug-as-code-generation.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"><![CDATA[<p><strong>Author:</strong> <a href="https://konnov.phd/?utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site">Igor Konnov</a></p>
<p><strong>Date:</strong> March 23, 2026</p>
<p>This is an anecdote about another useful application of Codex and Claude Code in
the middle of a testing project. It is another example of using LLMs to make
distributed systems easier to test and debug, instead of generating piles of
slop.</p>
<h2 id="context" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/debug-as-code-generation.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#context">Context</a></h2>
<p>I am currently developing a test harness for an implementation of distributed
consensus, cannot disclose the details yet. Think of the approach presented in
<a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">TFTP Symbolic Testing</a> but for a more complex distributed system.
It involves five state machines, each for a different subprotocol of the system.
The submachines are composed into a single machine. We generate the protocol
specifications and the test harness with Claude Code and Codex. This test
harness produces input events for the protocol implementation with <a href="https://apalache-mc.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache</a>
and replays the output events from the implementation, checking that they
conform to the protocol specification with Apalache. Pretty cool. This is
<strong>AI-assisted protocol extraction and testing</strong>.</p>
<p>It took me a few days to bootstrap this project, designing the proper interfaces
and the harness architecture. After sheparding the AI tools for dozens of
iterations, I got a harness that communicates with the implementation. Whenever,
it surfaces a mismatch, the LLM looks into the source code, the specification,
and the test harness, and investigates the mismatch. When it identifies the root
cause and proposes a fix, I take a careful look at the proposed fix, and if it
looks good, I apply it. Sometimes, the LLM identifies a completely wrong root
cause. However, after a few iterations, we end up with the right root cause and
the right fix. This is a very efficient way to extract the protocol
specification from the actual implementation, and it is much faster than doing
it manually.</p>
<p>This specification-testing-refinement loop worked quite well for multiple
iterations. Sometimes, I could even leave this agentic loop running for several
hours unattended, though protocol extraction often requires human supervision.
At some point, however, the harness and the implementation started to produce a
sequence of events that was rejected by the specification, or, to be more
precise, by the model checker following the specification. Codex and Claude
tried to identify the root cause and fix it. They introduced multiple fixes, but
the mismatch persisted. I basically lost a whole day looking at the LLM outputs
and prompting them. At some point, I looked at the git log and realized that
<strong>we were going in circles</strong>. What was worse, every fix was introducing a
workaround and generally the harness started to degrade. So we went down the
rabbit hole of slop. In the rest of this post, I will just call both Claude and
Codex &quot;the LLM&quot;, as I don't remember which one did what, and it doesn't matter
for the story.</p>
<p>At this point, I realized that the AI feedback loop stopped working, and the
human had to seriously intervene. The LLMs could describe what was happening, we
had a concrete state from the model checker, but a single transition that
<strong>must have been enabled</strong> in this state was not enabled. If you worked with
formal verification tools, you know that <strong>this is the situation we all dread</strong>.
It is a clear sign of <strong>the specification being overconstrained</strong>. The model
checker is doing its job, and it is correctly rejecting the transition. The
issue is that the specification requires an impossible combination somewhere,
like <strong>x = 2 and x = 3</strong>. In this case, the model checker cannot produce a
counterexample, or anything meaningful, because the constraints are
contradictory. (There is a line of research on UNSAT cores, but it's hard to
apply in practice in TLA<sup>+</sup>.)</p>
<p>If you wrote the specification yourself, you can usually stare at it and find
the combination of contradicting constraints. However, in this case, the
specification was written by the LLM! Of course, I looked at it. Things looked
fine. The LLM agreed with me that the transition should be enabled.</p>
<h2 id="debug-an-overconstrained-specification-like-a-human-would" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/debug-as-code-generation.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#debug-an-overconstrained-specification-like-a-human-would">Debug an overconstrained specification like a human would</a></h2>
<p>The LLMs were stuck. So I decided to explain them how I usually debug
overconstrained specifications. First, I asked the LLM to use <code>git bisect</code> to
find the last working commit. It crunched for 10-15 minutes and found the last
working one. Comparing the git diffs did not help though.</p>
<p>The next usual step is to comment out some parts of the specification, and see
whether the transition becomes enabled. If it does, then we know that one of the
problematic constraints is in the commented-out part. We did this exercise for
about 1 hour. The agentic loop was amazing. The LLM was doing everything
automatically.  In the end, we still could not find the root cause.</p>
<p><strong>I was surprised how well Codex and Claude were running Apalache and
transforming the TLA<sup>+</sup> specification. They neither required skills,
nor MCP.</strong> They simply ran the model checker, parsed its output and parsed the
produced counterexamples. Being a CLI tool finally paid off for Apalache!</p>
<h2 id="turn-specification-debugging-into-code-generation" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/debug-as-code-generation.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#turn-specification-debugging-into-code-generation">Turn specification debugging into code generation</a></h2>
<p>I could stare at the specification and look for a mismatch. In the hindsight,
that would not help me, as the issue was outside of the subprotocol
specification. So I thought: LLMs fail to identify the issue, but they can
generate code in minutes. <strong>Can I turn this debugging problem into a code
generation problem?</strong></p>
<p>Hence, I told the LLM to take the pieces of my specification framework, extract
the random simulator, write an ad hoc simulator that drives the system into the
exact problematic state, do random exploration from there, look for the disabled
assumptions. Since my framework is written in Python, it was quite an easy task
for the LLM. <strong>In 5 minutes, it ran the ad hoc simulator</strong>. <strong>In 2-3 more
minutes, we had the root cause!</strong> Indeed, there were two contradicting
constraints. It was hard to identify them by looking at the specification, as
one of them was in the subprotocol specification, and the other one was in the
system specification. Obviously, I extended <code>AGENTS.md</code> with an instruction to
avoid introducing constraints at both levels.</p>
<p>This approach worked, since my specification is not just a TLA<sup>+</sup>
specification. It is actually Python code. It can be executed. But it can also
generate a TLA<sup>+</sup> specification. As a result, the LLM can easily
interact with the Python code, wrap it into a large ad-hoc simulator, and run
it. At the same time, the model checker uses the generated TLA<sup>+</sup>
specification to reason about it. Moreover, the LLM also benefits from having
two different perspectives in the form of the Python code and TLA<sup>+</sup>.</p>
<p>If you find this hint useful, leave a comment below.</p>
<h2 id="want-to-talk%3F" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/debug-as-code-generation.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#want-to-talk%3F">Want to talk?</a></h2>
<!-- References -->
]]></content>
    <author>
      <name>Igor Konnov</name>
      <email>igor@konnov.phd</email>
      <uri>https://konnov.phd/?utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site</uri>
    </author>
    
    
    <category term="testing" />
    
    <category term="model-checking" />
    
    
    
  </entry>
  
  
  <entry>
    <title type="html">All you need is a simulator? Nope</title>
    <link href="https://protocols-made-fun.com/random-walks.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" rel="alternate" type="text/html" title="All you need is a simulator? Nope" />
    <published>2026-03-09T00:00:00.000Z</published>
    <updated>2026-03-09T00:00:00.000Z</updated>
    <id>https://protocols-made-fun.com/random-walks.html</id>
    <content type="html" xml:base="https://protocols-made-fun.com/random-walks.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"><![CDATA[<p><strong>Author:</strong> <a href="https://konnov.phd/?utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site">Igor Konnov</a></p>
<p><strong>Date:</strong> March 09, 2026</p>
<p><strong>Punchline: Testing distributed protocols with random simulation and stateful
property-based testing (PBT) is not enough!</strong> Yes, running a simulator for days
is better than doing manual testing or just running unit tests. But <strong>you will
miss states, which may expose bugs</strong>. <strong>Even on very small systems.</strong> I have
been saying exactly this to many software engineers. Many times. However,
whiteboard arguments do not help. As humans, we have a great deal of trust in
probabilities, and our intuitive understanding of randomness is often wrong.
Hence, I am giving you concrete figures and plots in this blog post. I must
admit that my own intuition was also wrong: I expected fewer random walks to be
needed to achieve good coverage. For a quick glance, see <a href="https://protocols-made-fun.com/random-walks.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#quick-summary">Quick
summary</a>.</p>
<p>Achieving <strong>complete coverage with random walks is hard</strong>. This is especially
important to know, <strong>if you are using them to produce test cases for your
implementation</strong>. It is also crucial to know, in case you generate an
implementation of a distributed protocol with AI tools and <strong>hope for random
walks/PBT to work as an ultimate guardrail</strong>.</p>
<p>Don't get me wrong. I like PBT and simulators (having written the <a href="https://github.com/informalsystems/quint?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Quint</a>
simulator). I believe that these tools are must-have tools for testing.  See my
recent blog post on <a href="https://protocols-made-fun.com/pbt-adversarial-llms.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Property-based testing, adversarial developers, and
LLMs</a>. However, they are not the only tools that we need to make sure
that our systems work as expected. This is especially true now, when we do not
have time to properly design and review the AI-generated code.</p>
<p><strong>Why now?</strong> It has always been difficult to compare search procedures that were
developed by different branches of computer science. Everyone wanted to promote
their technique as the ultimate winner. Want to compare property-based testing
and model checking? Bad luck. Different tools require different inputs. Some are
libraries for programming languages (like <a href="https://en.wikipedia.org/wiki/QuickCheck?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">QuickCheck</a>), some are tools for
specification languages (like <a href="https://github.com/tlaplus/tlaplus?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">TLC</a> and <a href="https://apalache-mc.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache</a>).  Now it is much faster
to design frameworks, to experiment with multiple search procedures. It is also
easier to do reproducible experiments with LLMs. Good times, if you know how to
conduct experimental research.</p>
<div class="tip-box">
  <div class="tip-header"><strong>Tip:</strong></div>
  <div class="tip"><p>In contrast to the previous blog posts, I do not
provide the artifacts for download. AI slop forks are real. It still takes me
several days to design and conduct the experiments on a beefy machine, as well
as to find the right format to interpret and plot the data. Even with the help
of the frontier models, though they are of great help. It only takes 10-15
minutes to repackage the benchmarks and results with an AI tool, having the
experimental data. Hence, I am sharing my lab book with the customers and
researchers, upon request.</p>
</div>
</div>
<p><span id="quick-summary"></span></p>
<h2 id="1.-quick-summary-for-the-impatient-readers" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/random-walks.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#1.-quick-summary-for-the-impatient-readers">1. Quick summary for the impatient readers</a></h2>
<p>Look at two groups of figures below. They summarize the results of running
random walks on specifications of three prominent distributed protocols:
two-phase commit, readers-writers, and FPaxos (see <a href="https://protocols-made-fun.com/random-walks.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#benchmarks">Benchmarks</a>).
The figures show the coverage achieved by random walks, with 100% being the
numbers of distinct states (reported by the model checker TLC). In addition, we
plot the running times of the random walks, with the values plotted against the
right y-axis. All running times are on a AMD Ryzen 9 5950X processor (16
physical, 32 logical cores), 128 GB memory.</p>
<h3 id="1.1-coverage-for-minimal-instances" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/random-walks.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#1.1-coverage-for-minimal-instances">1.1 Coverage for minimal instances</a></h3>
<p>In this set of experiments, we do random walks for the minimal instances of the
benchmarks. We start with the <strong>meaningful default</strong> of 100,000 random walks,
with at most 100 steps per walk. As you can see from Figure 1, only in the case
of two-phase commit and two resource managers, we achieve complete state
coverage. This is not surprising, since this instance has only 56 states. It's
tiny! For two-phase commit with three resource managers and readers-writers with
three actors, we achieve 85-90% coverage. This is also in the reasonable range.
On <strong>FPaxos with two acceptors, we achieve the 77.5% coverage with 100k random
walks</strong>. This is a bit worrying, since the state space is about 37k states.</p>
<p>The good news is that we can push all of the above benchmarks to achieve over
99% coverage. As you can see in the figures, it takes <strong>10 million random walks
to achieve 99% coverage</strong>. In addition to that, <strong>these runs require 1-2
hours</strong>.</p>
<p><strong>All of the above benchmarks are quite small by the model checking
standards. They have tens of thousands of states. It takes TLC only 1-3 seconds
to explore the state space and check the invariants for each of these
benchmarks.</strong></p>
<div class="figure-grid">
  <figure>
    <a href="https://protocols-made-fun.com/img/random-walks/twophase-n2-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/random-walks/twophase-n2-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Coverage of random walks for the two-phase commit benchmark with 2 resource managers">
    </picture></a>
    <figcaption>Figure 1.a: Two-phase commit, 2 RMs.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/random-walks/twophase-n3-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/random-walks/twophase-n3-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Coverage of random walks for the two-phase commit benchmark with 3 resource managers">
    </picture></a>
    <figcaption>Figure 1.b: Two-phase commit, 3 RMs.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/random-walks/rw-inst3-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/random-walks/rw-inst3-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Coverage of random walks for the readers-writers benchmark with 3 actors">
    </picture></a>
    <figcaption>Figure 1.c: Readers-writers, 3 actors.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/random-walks/fpaxos-inst2-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/random-walks/fpaxos-inst2-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Coverage of random walks for the FPaxos benchmark with 2 acceptors">
    </picture></a>
    <figcaption>Figure 1.d: FPaxos, 2 acceptors.</figcaption>
  </figure>
</div>
<h3 id="1.2-slightly-larger-instances" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/random-walks.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#1.2-slightly-larger-instances">1.2 Slightly larger instances</a></h3>
<p>What happens if we take the instances that are still small, but have 1-2
participants more? Figure 2 shows the results of doing random walks on these
instances.</p>
<p>As you can see, with the meaningful default of 100,000 random walks, we achieve
extremely poor coverage, about 25-30% on the benchmarks up to 2 million states.
<strong>On FPaxos with 4 acceptors, we achieve only 3% coverage after 100,000
random walks</strong>. Really bad!</p>
<p>To see how far we could push the coverage, we did the experiments with 10-100
million random walks. It is clear that <strong>in 1-2 hours of simulation we get to
60-80% coverage</strong>. It is good, but not great. When we push FPaxos with 3
acceptors to 100 million random walks, we get to 94.5% coverage. Nice, though it
took us 7.5 hours to get there. However, <strong>on FPaxos with 4 acceptors, we get a
poor coverage of 60.4% even with 100 million random walks, which took us 8.5
hours to run</strong>. This benchmark has about 11 million states. So it is reasonably
large, but, again, <strong>not that large by the model checking standards</strong>.</p>
<p>Again, <strong>it takes the model checker TLC up to 10 minutes to enumerate all the
states and check the invariants for these instances</strong>, whereas we have been
<strong>running the simulations for hours!</strong> This is especially
striking, given that we are <strong>running optimized simulators in Rust</strong>.</p>
<div class="figure-grid">
  <figure>
    <a href="https://protocols-made-fun.com/img/random-walks/twophase-n5-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/random-walks/twophase-n5-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Coverage of random walks for the two-phase commit benchmark with 5 resource managers">
    </picture></a>
    <figcaption>Figure 2.a: Two-phase commit, 5 RMs.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/random-walks/rw-inst4-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/random-walks/rw-inst4-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Coverage of random walks for the readers-writers benchmark with 4 actors">
    </picture></a>
    <figcaption>Figure 2.b: Readers-writers, 4 actors.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/random-walks/fpaxos-inst3-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/random-walks/fpaxos-inst3-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Coverage of random walks for the FPaxos benchmark with 3 acceptors">
    </picture></a>
    <figcaption>Figure 2.c: FPaxos, 3 acceptors.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/random-walks/fpaxos-inst4-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/random-walks/fpaxos-inst4-coverage.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Coverage of random walks for the FPaxos benchmark with 4 acceptors">
    </picture></a>
    <figcaption>Figure 2.d: FPaxos, 4 acceptors.</figcaption>
  </figure>
</div>
<p><span id="benchmarks"></span></p>
<h2 id="2.-the-benchmarks" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/random-walks.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#2.-the-benchmarks">2. The benchmarks</a></h2>
<p>As benchmarks, we use three specifications of distributed protocols. These are
prominent examples from the repository of <a href="https://github.com/tlaplus/Examples/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">TLA+ Examples</a>:</p>
<ul>
<li>
<p><strong>Two-phase commit</strong>. This is the famous two-phase commit. The specification
is explained in <a href="https://www.microsoft.com/en-us/research/publication/consensus-on-transaction-commit/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Consensus on Transaction Commit</a> by Jim Gray
and Leslie Lamport. You can check the TLA<sup>+</sup> specification in
<a href="https://github.com/tlaplus/Examples/blob/master/specifications/transaction_commit/TwoPhase.tla?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">TwoPhase.tla</a>.</p>
</li>
<li>
<p><strong>Readers-writers</strong>. This is a solution to the <a href="https://en.wikipedia.org/wiki/Readers%E2%80%93writers_problem?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Readers-Writers Problem</a>.
The TLA<sup>+</sup> specification by Stephan Merz can be found in
<a href="https://github.com/tlaplus/Examples/blob/master/specifications/ReadersWriters/ReadersWriters.tla?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">ReadersWriters.tla</a>.</p>
</li>
<li>
<p><strong>FPaxos</strong>. This is <a href="https://fpaxos.github.io/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Flexible Paxos</a> by Heidi Howard, Dahlia Malkhi, and
Alexander Spiegelman. The TLA<sup>+</sup> specification can be found in
<a href="https://github.com/fpaxos/fpaxos-tlaplus/blob/main/FPaxos.tla?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">FPaxos.tla</a>.</p>
</li>
</ul>
<p>All of the above specifications are parameterized in the number of participating
processes. We consider several instances of each benchmark. To give you an idea
of their state space size (the number of reachable states), we compute the
figures with <a href="https://github.com/tlaplus/tlaplus?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">TLC</a>. The reachable states are called <em>distinct states</em> in TLC,
whereas <em>produced states</em> are the number of states that TLC generates during the
search. Another important metric is the <em>diameter</em> of the state space, which is
the length of the longest shortest path between any two reachable states (read
it again!).</p>
<p>As you can see from Table 1, these transition systems are not tiny, but they are
actually small by the model checking standards. Surprisingly, they are
sophisticated enough to challenge random walks! <strong>Distributed protocols are
hard.</strong></p>
<figure markdown="1">
<table>
<thead>
<tr>
<th>Benchmark</th>
<th>Instance</th>
<th>Distinct states</th>
<th>Produced states</th>
<th>Diameter</th>
<th>TLC times</th>
</tr>
</thead>
<tbody>
<tr>
<td>Two-phase commit</td>
<td>2 resource managers</td>
<td>56</td>
<td>154</td>
<td>8</td>
<td>1 sec</td>
</tr>
<tr>
<td></td>
<td>3 resource managers</td>
<td>288</td>
<td>1,146</td>
<td>11</td>
<td>2 sec</td>
</tr>
<tr>
<td></td>
<td>5 resource managers</td>
<td>8,832</td>
<td>58,146</td>
<td>17</td>
<td>2 sec</td>
</tr>
<tr>
<td>Readers-writers</td>
<td>2 readers/writers</td>
<td>390</td>
<td>935</td>
<td>9</td>
<td>2 sec</td>
</tr>
<tr>
<td></td>
<td>3 readers/writers</td>
<td>21,527</td>
<td>59,674</td>
<td>13</td>
<td>2 sec</td>
</tr>
<tr>
<td></td>
<td>4 readers/writers</td>
<td>2,192,020</td>
<td>7,069,237</td>
<td>17</td>
<td>1 min</td>
</tr>
<tr>
<td>FPaxos</td>
<td>2 acceptors</td>
<td>36,953</td>
<td>245,288</td>
<td>19</td>
<td>4 sec</td>
</tr>
<tr>
<td></td>
<td>3 acceptors</td>
<td>362,361</td>
<td>2,697,682</td>
<td>25</td>
<td>21 sec</td>
</tr>
<tr>
<td></td>
<td>4 acceptors</td>
<td>11,279,393</td>
<td>96,056,172</td>
<td>31</td>
<td>9 min</td>
</tr>
</tbody>
</table>
<figcaption>Table 1: The state space size of the benchmarks</figcaption>
</figure>
<p>In the experiments, I am using a custom framework to represent the above
<strong>specifications-as-code</strong> that makes it easy to experiment with
different search procedures. To make sure that these specifications faithfully
represent the original TLA<sup>+</sup> specifications, I do the following:</p>
<ol>
<li>
<p>do a code review (obviously),</p>
</li>
<li>
<p>automatically translate the specifications to TLA<sup>+</sup> and check them
with TLC,</p>
</li>
<li>
<p>run a custom-tailored model checker to compute the number of distinct states
and check the invariants.</p>
</li>
</ol>
<p><span id="experimental-results"></span></p>
<p><span id="what-are-random-walks"></span></p>
<h2 id="3.-what-are-random-walks-and-state-enumeration%3F" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/random-walks.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#3.-what-are-random-walks-and-state-enumeration%3F">3. What are random walks and state enumeration?</a></h2>
<p>I have mentioned random walks and state enumeration multiple times so far.
Let's clarify what these terms mean. The concept of a random walk is intuitively
simple, though the details matter. Instead of looking at a large specification,
let's look at a simple example of a system that models adding and removing
workers from a pool. This example is inspired by the example in <a href="https://learntla.com/topics/tips.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#parameterize-your-actions">Parameterize
Your Actions</a> by Hillel Wayne. We add the variable
<code>count</code> to have a meaningful invariant. The specification is shown below. Even
if you do not know TLA<sup>+</sup>, it should be easy to understand.  If you
still have trouble understanding it, just ask an LLM, they are good at
explaining TLA<sup>+</sup> specifications.</p>
<figure markdown="1">
<pre><code class="hljs language-tla"><span class="hljs-keyword">EXTENDS</span> Integers, FiniteSets

<span class="hljs-keyword">CONSTANTS</span>
    <span class="hljs-comment">(* The set of workers to choose from. *)</span>
    <span class="hljs-comment">(* @type: Set(Int);                   *)</span>
    Worker

<span class="hljs-keyword">VARIABLES</span>
    <span class="hljs-comment">(* The set of active workers.         *)</span>
    <span class="hljs-comment">(* @type: Set(Int);                   *)</span>
    active,
    <span class="hljs-comment">(* The number of active workers.      *)</span>
    <span class="hljs-comment">(* @type: Int;                        *)</span>
    count

<span class="hljs-comment">(* Add a worker w to the set of active workers, if it is not already active. *)</span>
<span class="hljs-comment">(* @type: (Int) =&gt; Bool;                                                     *)</span>
Add(w) ≜ w ∉ active ∧ active&#x27; <span class="hljs-operator">=</span> active ∪ {w} ∧ count&#x27; <span class="hljs-operator">=</span> count <span class="hljs-operator">+</span> <span class="hljs-number">1</span>

<span class="hljs-comment">(* Remove a worker w from the set of active workers, if it is active.        *)</span>
<span class="hljs-comment">(* @type: (Int) =&gt; Bool;                                                     *)</span>
Remove(w) ≜ w ∈ active ∧ active&#x27; <span class="hljs-operator">=</span> active \ {w} ∧ count&#x27; <span class="hljs-operator">=</span> count <span class="hljs-operator">-</span> <span class="hljs-number">1</span>

<span class="hljs-comment">(* Initialize the system with no active workers and a count of zero.         *)</span>
Init ≜ active <span class="hljs-operator">=</span> {} ∧ count <span class="hljs-operator">=</span> <span class="hljs-number">0</span>

<span class="hljs-comment">(* In a next state, either add a worker or remove a worker.                  *)</span>
Next ≜ ∃ w ∈ Worker:
          Add(w) ∨ Remove(w)

<span class="hljs-comment">(* An invariant: `count` matches the cardinality of the active set.          *)</span>
Inv ≜ (count <span class="hljs-operator">=</span> <span class="hljs-keyword">Cardinality</span>(active))
</code></pre>
<figcaption>Figure 3: TLA<sup>+</sup> specification for the Workers example.</figcaption>
</figure>
<p>If we fix the set of workers to be <code>Worker = {1, 2}</code>, we get a nice labelled
transition system (LTS) of 4 states. The graphical representation of this LTS is
shown below.</p>
<figure markdown="1">
<div><a href="https://protocols-made-fun.com/img/random-walks-lts.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size">
<picture>
  <img class="responsive-img full-width-img"
    src="https://protocols-made-fun.com/img/random-walks-lts.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
    alt="LTS for the Workers specification of two workers">
</picture>
</a></div>
<figcaption>Figure 4: The labelled transition system for two workers.</figcaption>
</figure>
<p>TLA<sup>+</sup> does not have any built-in notion of randomness or
probabilities.  It is what is usually called a <em>qualitative</em> specification.
When evaluating <code>Next</code> in a state, we can only evaluate whether a specific
transition is possible under a specific choice of <code>w</code> and the action scheduling
decision (whether to execute <code>Add(w)</code> or <code>Remove(w)</code>). This is the standard
semantics under the definition of behaviors. We can enumerate all reachable
states for the above system by breadth-first search or depth-first search. This
is what the model checker TLC does (it uses breadth-first search). This is what
I will call <em>state enumeration</em> in this blog post.</p>
<p>We could also interpret the choice of <code>w</code> and the action scheduling decision as
a random choice. Since the above specification is small, we can visualize it as
a <a href="https://en.wikipedia.org/wiki/Markov_decision_process?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Markov decision process
(MDP)</a>. The states are
the same as in the LTS, but we also attach probabilities to the transitions.</p>
<figure markdown="1">
<div><a href="https://protocols-made-fun.com/img/random-walks-mdp.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size">
<picture>
  <img class="responsive-img full-width-img"
    src="https://protocols-made-fun.com/img/random-walks-mdp.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
    alt="MDP for the Workers specification of two workers">
</picture>
</a></div>
<figcaption>Figure 5: The MDP for two workers.</figcaption>
</figure>
<p>Notice that we assign probabilities for choosing the value of <code>w</code> and for
choosing the action to execute: <code>Add(w)</code> or <code>Remove(w)</code>. For example, in the
initial state, we choose <code>w=1</code> with probability 0.5, then the action <code>Add(1)</code>
with probability 0.5, which gives us a transition to the state where <code>active = {1}</code> and <code>count = 1</code> (with probability 0.25). However, if we choose <code>w=1</code> and
the action <code>Remove(1)</code>, we have to backtrack to the initial state, since the
precondition of <code>Remove(1)</code> is not satisfied.</p>
<p>A <em>random walk</em> is a path through the MDP. It is a sequence of states that we
get by making random choices at each step. In the above figure, you can see one
walk in blue and one walk in red. To avoid too many backward edges, we have a
retry budget, typically, 3-10 retries per step. We take this simple approach in
our custom framework. It is similar to what the randomized simulator in
<a href="https://github.com/informalsystems/quint?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Quint</a> is doing, though the Quint simulator is trying a bit more locally
before backtracking. Probabilities are basically used to produce various random
walks. There is no inherent statistical meaning to these probabilities in random
walks. This is very much how stateful property-based testing works, too, though
PBT frameworks usually use biased coins, instead of uniform ones.</p>
<div class="tip-box">
  <div class="tip-header"><strong>Tip:</strong></div>
  <div class="tip"><p>TLC also supports random simulation, but it assigns
probabilities differently. Given a state, TLC first computes all successors of
the state and then chooses one of the successors uniformly at random. This would
give us a different MDP that filters out disabled transitions. Both approaches
have their merits and drawbacks. The approach of TLC requires us to enumerate
successors, unless we use reservoir sampling. It would actually work better on
the examples in this blog post, since they have many disabled transitions.
However, in systems that inject faults, this approach has an issue, as the
faulty transitions often dominate the search.</p>
</div>
</div>
<h2 id="4.-which-states-are-missing%3F" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/random-walks.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#4.-which-states-are-missing%3F">4. Which states are missing?</a></h2>
<p>Since we can measure state coverage now, the next question is: What are these
states that we are missing? Maybe these states are not important at all. To
check that, I ran the random walks for the two-phase commit benchmark with 2
resource managers for 10,000 instead of 100,000 runs. Conveniently, exactly one
state was missing from the coverage. As our specifications are code, I just
asked Claude to instrument the search to experimentally evaluate the visit
frequencies per run for each reachable state. Figure 6 is quite detailed. Click
on it to see the full-size version.</p>
<figure markdown="1">
  <div><a href="https://protocols-made-fun.com/img/two_phase_graph.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size">
    <picture>
      <img class="responsive-img full-width-img"
        src="https://protocols-made-fun.com/img/two_phase_graph.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Experimental evaluation of the visit frequencies for each state in the two-phase commit benchmark with 2 resource managers">
    </picture>
  </a></div>
  <figcaption>
    Figure 6: Reachability frequencies for the two-phase commit benchmark with
    2 resource managers.
  </figcaption>
</figure>
<p>As we can see, the missing state (with the frequency of 0) is the state where
the transasction manager aborts the transaction, one resource manager also
aborts the transaction, and the other resource manager is in the &quot;prepared&quot;
state. This is an interesting state in this protocol, as the other resource
manager still has the potential to commit the transaction, though it should not
do that.</p>
<p><strong>Bottom line:</strong> We may miss important states with random walks.</p>
<h2 id="5.-more-coverage-plots" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/random-walks.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#5.-more-coverage-plots">5. More coverage plots</a></h2>
<p>Figure 7 shows the coverage evolution for the large instances of the benchmarks.
With this, we can see how increasing the number of random walks helps to
increase the coverage.  It also demonstrates the growing volume of covered and
missing states.</p>
<p>I wanted to share these flame plots with you. I find them cool.</p>
<div class="figure-grid">
  <figure>
    <a href="https://protocols-made-fun.com/img/random-walks/twophase-n5-overlay.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/random-walks/twophase-n5-overlay.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Overlaid coverage of random walks for the two-phase commit benchmark with 5 resource managers">
    </picture></a>
    <figcaption>Figure 7.a: Overlaid coverage for two-phase commit, 5 RMs.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/random-walks/rw-inst4-overlay.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/random-walks/rw-inst4-overlay.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Overlaid coverage of random walks for the readers-writers benchmark with 4 actors">
    </picture></a>
    <figcaption>Figure 7.b: Overlaid coverage for readers-writers, 4 actors.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/random-walks/fpaxos-inst3-overlay.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/random-walks/fpaxos-inst3-overlay.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Overlaid coverage of random walks for the FPaxos benchmark with 3 acceptors">
    </picture></a>
    <figcaption>Figure 7.c: Overlaid coverage for FPaxos, 3 acceptors.</figcaption>
  </figure>
  <figure>
    <a href="https://protocols-made-fun.com/img/random-walks/fpaxos-inst4-overlay.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" target="_blank" title="Click to open full-size"><picture>
      <img class="responsive-img"
        src="https://protocols-made-fun.com/img/random-walks/fpaxos-inst4-overlay.png?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
        alt="Overlaid coverage of random walks for the FPaxos benchmark with 4 acceptors">
    </picture></a>
    <figcaption>Figure 7.d: Overlaid coverage for FPaxos, 4 acceptors.</figcaption>
  </figure>
</div>
<h2 id="6.-conclusions" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/random-walks.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#6.-conclusions">6. Conclusions</a></h2>
<p><strong>Random walks are not sufficient to achieve complete coverage
except for very small state spaces</strong>. Moreover, <strong>random walks take
significantly longer than the model checker</strong>. This is especially striking,
given that we are <strong>running optimized simulators in Rust</strong>. Another issue with
state coverage by random walks is that <strong>you would not even know that you
achieved complete coverage</strong>. You can measure the speed of discovering new
states, but understanding that the simulator has converged basically requires a
model checker.</p>
<p>Interestingly, random walks behaved badly on FPaxos with four acceptors. This is
a relatively benign benchmark, not having a state explosion like specifications
of Byzantine consensus protocols (BFT). In BFT, the minimal configurations
contain 4-6 replicas, depending on the protocol. Hence, <strong>we should expect a
significantly worse coverage by random walks on BFT</strong>.</p>
<p>Why do engineers keep running randomized experiments? Well, it is relatively
easy to write a simulator. (It is not that easy to write one that actually
works!) I have seen people playing with action distributions in the simulator,
just to drive the search towards &quot;interesting&quot; states. Whenever I was asking,
where the distributions were coming from, they could not explain this.
Simulators are deceptive. You have to understand what you are doing, or,
better, incorporate feedback. The most basic feedback is state coverage, though
we can implement more sophisticated feedback mechanisms.</p>
<p>From our experiments it may look like <strong>state enumeration is all we need</strong>. I
would argue that it is true <strong>as long as the set of reachable states fits into
memory</strong>. We do not have to store the states directly in memory, practical model
checkers store hashes of states. We can go as far as to store 2-3 bits per
state, assuming that collisions are acceptable (still better than random
walks!). Having a machine with 128 GB of memory, we can store roughly 50
billions of states. This is way more than the number of states in our
benchmarks -- dozens of billions vs. thousands and millions.</p>
<p>There are cases where randomness may find bugs, where state enumeration gets
stuck:</p>
<ol>
<li>
<p><strong>Value domains are quite large.</strong> For example, if we choose values from the
set of all 64-bit integers, it is not feasible to enumerate all successors even
for a single state. A random walk can still do some progress without getting
stuck. One can argue that choosing a value from the set <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="6.565ex" height="2.47ex" role="img" focusable="false" viewBox="0 -841.7 2901.8 1091.7"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="5B" d="M118 -250V750H255V710H158V-210H255V-250H118Z"></path></g><g data-mml-node="mn" transform="translate(278,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g><g data-mml-node="mo" transform="translate(778,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="msup" transform="translate(1222.7,0)"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g><g data-mml-node="TeXAtom" transform="translate(533,363) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mn"><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z"></path><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z" transform="translate(500,0)"></path></g></g></g><g data-mml-node="mo" transform="translate(2512.8,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g></g></g></svg></mjx-container></span>
uniformly at random is shooting in the dark, but sometimes it helps us to find
bugs, especially if the large set has just a few large equivalence classes.
Arguably, one should be able to apply data abstraction in this case. Also,
this is usually the moment when you should consider using a model checker that
supports symbolic representation of states, like <a href="https://apalache-mc.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache</a>.</p>
</li>
<li>
<p><strong>Guided search.</strong> If we have an heuristic that guides the search towards
interesting states, we can achieve better coverage with random walks faster.
Maybe we use reinforcement learning to learn such a heuristic. Maybe we use an
LLM to predict which actions are more likely to lead to interesting states.
The main issue is that it is quite hard to find a direction for the search in
the state space of distributed protocols.</p>
</li>
</ol>
<h2 id="want-to-talk%3F" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/random-walks.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#want-to-talk%3F">Want to talk?</a></h2>
<!-- References -->
]]></content>
    <author>
      <name>Igor Konnov</name>
      <email>igor@konnov.phd</email>
      <uri>https://konnov.phd/?utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site</uri>
    </author>
    
    
    <category term="testing" />
    
    <category term="model-checking" />
    
    
    
  </entry>
  
  
  <entry>
    <title type="html">AI-generated shovels or second-order slop? </title>
    <link href="https://protocols-made-fun.com/second-order-slop.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" rel="alternate" type="text/html" title="AI-generated shovels or second-order slop? " />
    <published>2026-02-12T00:00:00.000Z</published>
    <updated>2026-02-12T00:00:00.000Z</updated>
    <id>https://protocols-made-fun.com/second-order-slop.html</id>
    <content type="html" xml:base="https://protocols-made-fun.com/second-order-slop.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"><![CDATA[<p><strong>Author:</strong> <a href="https://konnov.phd/?utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site">Igor Konnov</a></p>
<p><strong>Date:</strong> February 12, 2026</p>
<p>tl;dr:</p>
<ul>
<li>
<p><em>AI coding tools now reduce development costs, but they also accelerate the
creation of software that appears high-quality while hiding serious correctness
and reliability risks.</em></p>
</li>
<li>
<p><em>When both code and tests are autogenerated, traditional quality checks lose
their signaling value, increasing the likelihood of costly failures, outages,
and liability exposure in production systems.</em></p>
</li>
<li>
<p><em>To responsibly capture productivity gains without undermining trust,
organizations must pair AI-generated code with specification-driven development
and automated validation techniques that verify real system behavior rather
than surface-level compliance.</em></p>
</li>
</ul>
<p>In 2025, we saw plenty of enthusiastic announcements about LLMs generating (or,
more correctly, <em>replicating</em>) relatively complex projects, like web
applications or video games. Apparently, many people got so tired of all this
that you could hear the words &quot;AI slop&quot; quite often. So often that some very
important people asked all of us not to call the output of their amazing tools
&quot;slop&quot;.</p>
<p>Anyhow, by the end of 2025, the amazing AI tools became <em>visibly more amazing</em>.
In the early 2025, I was only using ChatGPT and Copilot to produce small code
snippets and scripts, as well as to search for design solutions. In the summer
of 2025, I used Copilot &amp; Sonnet to produce boilerplate code. Now, I am using
Claude Code and Copilot (both with Opus and Sonnet) to generate code and tests
as well as to fix linting errors (still the hardest task!). I still have to
define the core data structures, write non-standard code and explain in detail
what I want to achieve. It is still hit and miss (see the most notable examples
below). However, it becomes economically feasible for me to use these tools,
unless they get 10x more expensive. By the way, after finishing my experiment
with <a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Symbolic testing of TFTP</a>, I was still not sure, whether I
wanted to use agentic tools every day. The feedback loop was energy draining. It
looks like the tools became better, and I've learnt how to give them more
focused and smaller-scoped tasks.</p>
<p>It still remains to see an AI-generated product that generates revenue. Are
there any examples, except the AI coding assistants themselves? In any case,
this is not what I wanted to write about. I wanted to write about something that
looks like a new phenomenon to me. We all have heard the saying: <em>When everyone
is digging for gold, sell shovels</em>. Just over a couple of weeks, there was an
unusual number of announcements about development tools that were generated with
AI. This is what I call <em>AI-generated shovels</em>.  These announcements bring so
much joy to AI influencers that it's hard to find anything else. Do these tools
actually work though? At a closer look, some of the shovels break on a first
try, some happen to work only under very specific conditions. Most likely, you
have seen some announcements, and you know what I am talking about. It is also
very likely that you have not seen all of the announcements that I have in mind.
Since we are talking about development tools, libraries, or even languages that
do not actually work, not web apps, it is not just slop, it is a <em>second-order
slop</em>!</p>
<p>I am not going to call any names, or do any fingerpointing. This is not the
point. What makes me seriously concerned about the second-order slop is that the
software development industry was cutting corners everywhere even before the AI
boom. &quot;Move fast and break things!&quot;, <a href="https://en.wikipedia.org/wiki/Minimum_viable_product?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">minimum-viable products</a> (or are they
solutions?), <a href="https://en.wikipedia.org/wiki/Product-market_fit?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">product-market-fit</a>, etc. A couple of years ago, I was joking
that I would rather not use an MVP compiler, operating system, or database.
Well, AI tools generate compilers. Here we are.</p>
<p><strong>Shovel ad!</strong> Since I have been working on pre-LLM shovels like <a href="https://apalache-mc.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache</a>
and <a href="https://github.com/informalsystems/quint?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Quint</a> myself, I am in the shovel business, too! (Do you know that SMT
solvers were also considered AI?) Of course, I am developing new shovels, and
they are also AI-generated and AI-compatible, and they are the best in town, by
the way. So if you want to talk, <a href="https://protocols-made-fun.com/second-order-slop.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#want-to-talk">drop me a message</a>. To be fair,
my time tracker shows that I've burnt six weeks of my time on the latest shovel,
in addition to burning through and over my Copilot and Claude budgets, so it's
not entirely AI-generated. Perhaps, a bit artisanal.</p>
<p><strong>Good shovel or slop?</strong> How do we distinguish a robust AI-generated shovel from
a second-order slop? In the pre-LLM years, I could just look at the test suite
and say, whether the team was serious or not. Those amazing days when blockchain
engineers would nod their heads to the question: <em>Do you have integration
tests?</em> They were proudly demonstrating a <em>single</em> integration test that was 3-5
KLOC long. Also, by looking at the code, you could sense whether it was written
just yesterday, or someone had time to think about it.</p>
<p>In 2026, the code may look professionally written and follow all the best
practices and still be completely broken. On top of that, LLMs generate
well-looking tests, if you ask them. A lot of tests! The more tests you have,
the more tokens you have to pay for. Win-win. Moreover, the generated
tests may check that the code works, but this does not mean that the code does
what you expect. This happened to me (see below).</p>
<p>So when we evaluate an AI-generated shovel, we want to answer two questions:</p>
<ol>
<li>
<p>Does this shovel do what the authors claim it should do?</p>
</li>
<li>
<p>Does this shovel work beyond a few simple tests?</p>
</li>
</ol>
<p>These are not new questions. The testing and verification communities have been
trying to automate validation and verification for long time. Interestingly,
these questions did not get much attention over the last two decades. It was
expected that open source projects and products by respectable companies were
&quot;more or less&quot; correct and complete. In my understanding, two factors
contributed to that:</p>
<ol>
<li>
<p>The code was written and reviewed by highly-skilled engineers, for fun or
profit.</p>
</li>
<li>
<p>The projects were extensively tested with continuous integration tools.</p>
</li>
</ol>
<p>Now, if an LLM generated the code just yesterday, and all tests pass, are we
good? It is hard to tell. If we follow the brand new <em>spec-driven development</em>,
we have a bunch of markdown files. Apparently, we should ask a few other LLMs to
check whether the implemented code matches the markdown specs. Something like
that.</p>
<p><strong>Can we do better?</strong> I believe we can. For example, if you are developing
a distributed system, do not generate it directly. First, write or AI-generate a
sequential reference implementation (e.g., in Python) or, even better, a formal
specification (e.g., in TLA<sup>+</sup>). Second, use this artifact to produce
the code for the actual distributed system.</p>
<p>Why does this help? For two reasons:</p>
<ol>
<li>
<p>It is easier to compare the reference implementation or specification
against the markdown requirements than to compare the entire codebase.</p>
</li>
<li>
<p>The reference implementation/specification is an actionable artifact.  Use
it to produce tests for the distributed system. Instead of generating 10 KLOC
tests once (and paying for loading them into the LLM context), automatically
produce as many tests as you can. This is where property-based testing and
model checking start to shine. See <a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Symbolic testing of TFTP</a>
for an example.</p>
</li>
</ol>
<p><strong>Examples of LLMs hit and miss.</strong> If you <a href="https://www.linkedin.com/in/igor-konnov-at/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">follow me on LinkedIn</a>, you could
have seen some of the examples. Below are the most curious instances that I would
regret missing in a code review (by Sonnet 4.5 and Opus 4.5):</p>
<ul>
<li>
<p><strong>The set minimum.</strong> When I asked an LLM to implement the search for the
minimal element of a set by using its string representation (called <code>repr</code> in
Python), it collected all set elements in a list, sorted them by <code>repr</code> and
picked the first one. It looks like my requirement was slightly non-standard.</p>
</li>
<li>
<p><strong>Sets with duplicates.</strong> An LLM has produced a unit test that constructed
the data structure called &quot;Set&quot; from the list <code>[ V(1), V(2), V(3), V(1) ]</code> and
asserted that the set cardinality was 4. The test passed, since <code>V</code> did not
have equality defined, and two different instances of <code>V(1)</code> had different
references. So it was doing the things right, but it was not doing the right
things!</p>
</li>
<li>
<p><strong>Performance bottleneck.</strong> An LLM translated my Python function into a Rust
function. Perfectly looking code. However, instead of adding a big integer <code>x</code>
to the big integer <code>y</code>, it used an iterator that made <code>y</code> increments of <code>x</code>.
Almost like a theorem prover! A logically correct solution, but my Rust code
was slower than the Python code. I only spotted it after running the profiler.
Again, a bit non-standard setup threw it off.</p>
</li>
</ul>
<h2 id="want-to-talk%3F" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/second-order-slop.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#want-to-talk%3F">Want to talk?</a></h2>
<!-- References -->
]]></content>
    <author>
      <name>Igor Konnov</name>
      <email>igor@konnov.phd</email>
      <uri>https://konnov.phd/?utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site</uri>
    </author>
    
    
    <category term="llms" />
    
    <category term="testing" />
    
    
    
  </entry>
  
  
  <entry>
    <title type="html">Property-based testing, adversarial developers, and LLMs</title>
    <link href="https://protocols-made-fun.com/pbt-adversarial-llms.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" rel="alternate" type="text/html" title="Property-based testing, adversarial developers, and LLMs" />
    <published>2025-12-22T00:00:00.000Z</published>
    <updated>2025-12-22T00:00:00.000Z</updated>
    <id>https://protocols-made-fun.com/pbt-adversarial-llms.html</id>
    <content type="html" xml:base="https://protocols-made-fun.com/pbt-adversarial-llms.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"><![CDATA[<p><strong>Author:</strong> <a href="https://konnov.phd/?utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site">Igor Konnov</a></p>
<p><strong>Date:</strong> December 22, 2025</p>
<p>I present a simple example that illustrates how property-based testing (PBT) and
model checking can help us catch unexpected behaviors of LLMs when they are used
to generate code. The example is inspired by the <a href="https://youtu.be/IYzDFHx6QPY?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">talk on property-based
testing</a> by <a href="https://scottwlaschin.com/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Scott Wlaschin</a>. If you are looking for a light
example that stresses the importance of writing good properties and having them
checked, this post is for you.</p>
<h2 id="1.-adversarial-developer" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/pbt-adversarial-llms.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#1.-adversarial-developer">1. Adversarial Developer</a></h2>
<p>A few days ago, I watched the <a href="https://youtu.be/IYzDFHx6QPY?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">talk on property-based testing</a> by
<a href="https://scottwlaschin.com/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Scott Wlaschin</a>. He started the talk by introducing a persona that he called
the <strong>Enterprise Developer from Hell</strong>. This is basically someone who
implements a feature to satisfy the given requirements, but they do it in
creatively evil (or just stupid) and unexpected ways. I will call such a persona
an <strong>adversarial developer</strong> in the rest of this post.</p>
<p>Then, Scott<sup class="footnote-ref"><a href="https://protocols-made-fun.com/pbt-adversarial-llms.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#fn1" id="fnref1">[1]</a></sup> showed how an adversarial developer could ruin as simple task as
adding up two numbers. For example, if we give them two tests <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="9.176ex" height="1.717ex" role="img" focusable="false" viewBox="0 -677 4056 759"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g><g data-mml-node="mo" transform="translate(722.2,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(1722.4,0)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g><g data-mml-node="mo" transform="translate(2500.2,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mn" transform="translate(3556,0)"><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z"></path></g></g></g></svg></mjx-container></span> and <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="12.57ex" height="1.717ex" role="img" focusable="false" viewBox="0 -677 5556 759"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mn"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(500,0)"></path></g><g data-mml-node="mo" transform="translate(1222.2,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(2222.4,0)"><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z"></path><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z" transform="translate(500,0)"></path></g><g data-mml-node="mo" transform="translate(3500.2,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mn" transform="translate(4556,0)"><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z"></path><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z" transform="translate(500,0)"></path></g></g></g></svg></mjx-container></span>, they will implement exactly those cases by case distinction. I am not
going to repeat Scott's talk. <a href="https://youtu.be/IYzDFHx6QPY?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Watch it</a>! It's instructive and
entertaining.</p>
<p>Back in 2020, of course, Scott added that we are often aversarial developers
ourselves, and our peers are rarely that evil. It could be an enthusiastic
junior developer, who has just started and now wants to rewrite the whole code
base.  Now, we are a few weeks away from 2026, and <strong>we definitely have such a
peer</strong>!  It is called an LLM, or just AI, as the corporate marketers prefer.
LLMs are not necessarily evil, but they are definitely less predictable than
experienced human software engineers. I am not talking about <a href="https://genai.owasp.org/llmrisk/llm01-prompt-injection/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">prompt
injection</a> here, which is another real issue with LLMs.</p>
<p>To be clear, in the rest of this text, I am talking about how an <strong>LLM could
behave like an adversarial developer when it generates code</strong>. It does not mean
that I ran one of the commercial LLMs and got those results.</p>
<h2 id="2.-property-based-testing" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/pbt-adversarial-llms.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#2.-property-based-testing">2. Property-Based Testing</a></h2>
<p>The point of Scott's talk was to show that a few data points (typical unit
tests) are insufficient to demonstrate correctness of the implementation.
A totally valid point!</p>
<p>In addition to the standard unit tests, we should also write the expected
properties of our implementation. The PBT frameworks test the code by producing
input values at random. For example, have a look at <a href="https://hypothesis.readthedocs.io/en/latest/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Hypothesis</a>. While this
may seem to be a silly idea at first, property-based tests uncovers tricky
bugs. Moreover the input value distribution does not have to be uniform. Keep
reading to see how this helps us catch the adversarial developer.</p>
<p>Here are the three properties of addition that Scott used to defeat the
adversarial developer:</p>
<ul>
<li><strong>identity</strong>: for every number <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="1.294ex" height="1.025ex" role="img" focusable="false" viewBox="0 -442 572 453"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g></g></g></svg></mjx-container></span>, we have <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="9.502ex" height="1.692ex" role="img" focusable="false" viewBox="0 -666 4200 748"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mo" transform="translate(794.2,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(1794.4,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g><g data-mml-node="mo" transform="translate(2572.2,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mi" transform="translate(3628,0)"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g></g></g></svg></mjx-container></span>,</li>
<li><strong>commutativity</strong>: for every numbers <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="1.294ex" height="1.025ex" role="img" focusable="false" viewBox="0 -442 572 453"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g></g></g></svg></mjx-container></span> and <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.464ex;" xmlns="http://www.w3.org/2000/svg" width="1.109ex" height="1.464ex" role="img" focusable="false" viewBox="0 -442 490 647"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D466" d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g></g></g></svg></mjx-container></span>, we have <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.464ex;" xmlns="http://www.w3.org/2000/svg" width="13.354ex" height="1.783ex" role="img" focusable="false" viewBox="0 -583 5902.4 788"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mo" transform="translate(794.2,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mi" transform="translate(1794.4,0)"><path data-c="1D466" d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(2562.2,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mi" transform="translate(3618,0)"><path data-c="1D466" d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(4330.2,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mi" transform="translate(5330.4,0)"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g></g></g></svg></mjx-container></span>, and</li>
<li><strong>associativity</strong>: for every numbers <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="1.294ex" height="1.025ex" role="img" focusable="false" viewBox="0 -442 572 453"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g></g></g></svg></mjx-container></span>, <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.464ex;" xmlns="http://www.w3.org/2000/svg" width="1.109ex" height="1.464ex" role="img" focusable="false" viewBox="0 -442 490 647"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D466" d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g></g></g></svg></mjx-container></span>, and <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="1.052ex" height="1.025ex" role="img" focusable="false" viewBox="0 -442 465 453"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D467" d="M347 338Q337 338 294 349T231 360Q211 360 197 356T174 346T162 335T155 324L153 320Q150 317 138 317Q117 317 117 325Q117 330 120 339Q133 378 163 406T229 440Q241 442 246 442Q271 442 291 425T329 392T367 375Q389 375 411 408T434 441Q435 442 449 442H462Q468 436 468 434Q468 430 463 420T449 399T432 377T418 358L411 349Q368 298 275 214T160 106L148 94L163 93Q185 93 227 82T290 71Q328 71 360 90T402 140Q406 149 409 151T424 153Q443 153 443 143Q443 138 442 134Q425 72 376 31T278 -11Q252 -11 232 6T193 40T155 57Q111 57 76 -3Q70 -11 59 -11H54H41Q35 -5 35 -2Q35 13 93 84Q132 129 225 214T340 322Q352 338 347 338Z"></path></g></g></g></svg></mjx-container></span>, we have
<span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="24.51ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 10833.3 1000"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="mi" transform="translate(389,0)"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mo" transform="translate(1183.2,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mi" transform="translate(2183.4,0)"><path data-c="1D466" d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(2673.4,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g><g data-mml-node="mo" transform="translate(3284.7,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mi" transform="translate(4284.9,0)"><path data-c="1D467" d="M347 338Q337 338 294 349T231 360Q211 360 197 356T174 346T162 335T155 324L153 320Q150 317 138 317Q117 317 117 325Q117 330 120 339Q133 378 163 406T229 440Q241 442 246 442Q271 442 291 425T329 392T367 375Q389 375 411 408T434 441Q435 442 449 442H462Q468 436 468 434Q468 430 463 420T449 399T432 377T418 358L411 349Q368 298 275 214T160 106L148 94L163 93Q185 93 227 82T290 71Q328 71 360 90T402 140Q406 149 409 151T424 153Q443 153 443 143Q443 138 442 134Q425 72 376 31T278 -11Q252 -11 232 6T193 40T155 57Q111 57 76 -3Q70 -11 59 -11H54H41Q35 -5 35 -2Q35 13 93 84Q132 129 225 214T340 322Q352 338 347 338Z"></path></g><g data-mml-node="mo" transform="translate(5027.7,0)"><path data-c="3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path></g><g data-mml-node="mi" transform="translate(6083.4,0)"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mo" transform="translate(6877.7,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mo" transform="translate(7877.9,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="mi" transform="translate(8266.9,0)"><path data-c="1D466" d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z"></path></g><g data-mml-node="mo" transform="translate(8979.1,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mi" transform="translate(9979.3,0)"><path data-c="1D467" d="M347 338Q337 338 294 349T231 360Q211 360 197 356T174 346T162 335T155 324L153 320Q150 317 138 317Q117 317 117 325Q117 330 120 339Q133 378 163 406T229 440Q241 442 246 442Q271 442 291 425T329 392T367 375Q389 375 411 408T434 441Q435 442 449 442H462Q468 436 468 434Q468 430 463 420T449 399T432 377T418 358L411 349Q368 298 275 214T160 106L148 94L163 93Q185 93 227 82T290 71Q328 71 360 90T402 140Q406 149 409 151T424 153Q443 153 443 143Q443 138 442 134Q425 72 376 31T278 -11Q252 -11 232 6T193 40T155 57Q111 57 76 -3Q70 -11 59 -11H54H41Q35 -5 35 -2Q35 13 93 84Q132 129 225 214T340 322Q352 338 347 338Z"></path></g><g data-mml-node="mo" transform="translate(10444.3,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g></g></g></svg></mjx-container></span>.</li>
</ul>
<p>At this point of the talk, I was like: Wait a minute! <strong>I could continue this
game of the adversarial developer</strong>. Before doing this, let's look at where we
are with respect to the code and the properties. Here is the obvious
implementation of integer addition in Python, since the language has built-in
support for unbounded integers:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">add</span>(<span class="hljs-params">a: <span class="hljs-built_in">int</span>, b: <span class="hljs-built_in">int</span></span>) -&gt; <span class="hljs-built_in">int</span>:
    <span class="hljs-keyword">return</span> a + b
</code></pre>
<p>Here are the property-based tests in <a href="https://hypothesis.readthedocs.io/en/latest/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Hypothesis</a>, generated by Claude Sonnet 4.5:</p>
<pre><code class="hljs language-python"><span class="hljs-meta">@given(<span class="hljs-params">st.integers(<span class="hljs-params"></span>)</span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">test_identity</span>(<span class="hljs-params">a</span>):
    <span class="hljs-string">&quot;&quot;&quot;Test identity property: a + 0 = a.&quot;&quot;&quot;</span>
    <span class="hljs-keyword">assert</span> add(a, <span class="hljs-number">0</span>) == a
    <span class="hljs-keyword">assert</span> add(<span class="hljs-number">0</span>, a) == a


<span class="hljs-meta">@given(<span class="hljs-params">st.integers(<span class="hljs-params"></span>), st.integers(<span class="hljs-params"></span>)</span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">test_commutativity</span>(<span class="hljs-params">a, b</span>):
    <span class="hljs-string">&quot;&quot;&quot;Test commutativity property: a + b = b + a.&quot;&quot;&quot;</span>
    <span class="hljs-keyword">assert</span> add(a, b) == add(b, a)


<span class="hljs-meta">@given(<span class="hljs-params">st.integers(<span class="hljs-params"></span>), st.integers(<span class="hljs-params"></span>), st.integers(<span class="hljs-params"></span>)</span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">test_associativity</span>(<span class="hljs-params">a, b, c</span>):
    <span class="hljs-string">&quot;&quot;&quot;Test associativity property: (a + b) + c = a + (b + c).&quot;&quot;&quot;</span>
    <span class="hljs-keyword">assert</span> add(add(a, b), c) == add(a, add(b, c))
</code></pre>
<p>You can find these and further examples in the <a href="https://github.com/konnov/pbt-example-summation?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">example repository</a>.</p>
<p>We run these tests with <code>pytest</code> to make sure that they all pass:</p>
<pre><code class="hljs language-sh">$ git <span class="hljs-built_in">clone</span> https://github.com/konnov/pbt-example-summation.git
$ <span class="hljs-built_in">cd</span> pbt-example-summation/python
$ poetry run pytest tests/test_add.py \
  -k <span class="hljs-string">&quot;test_identity or test_commutativity or test_associativity&quot;</span> --verbose
...
tests/test_add.py::test_identity PASSED                                        [ 33%]
tests/test_add.py::test_commutativity PASSED                                   [ 66%]
tests/test_add.py::test_associativity PASSED                                   [100%]
</code></pre>
<h2 id="3.-symbolic-model-checking-with-apalache" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/pbt-adversarial-llms.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#3.-symbolic-model-checking-with-apalache">3. Symbolic model checking with Apalache</a></h2>
<p>I decided to go even further and write a TLA<sup>+</sup> specification, to check
the three properties with <a href="https://apalache-mc.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache</a> and <a href="https://github.com/Z3Prover/z3?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Z3</a> (only showing the relevant parts):</p>
<pre><code class="hljs language-tlaplus"><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span> <span class="hljs-keyword">MODULE</span> Add <span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span>
<span class="hljs-comment">(*
 * A simple TLA+ specification of different kinds of addition.
 *
 * Igor Konnov, 2025
 *)</span>
<span class="hljs-keyword">EXTENDS</span> Integers

<span class="hljs-keyword">VARIABLE</span>
    <span class="hljs-comment">\* @type: Int;</span>
    x,
    <span class="hljs-comment">\* @type: Int;</span>
    y,
    <span class="hljs-comment">\* @type: Int;</span>
    z

AddMath(a, b) <span class="hljs-operator">==</span> a <span class="hljs-operator">+</span> b

<span class="hljs-title function_">InitMath</span> <span class="hljs-operator">==</span>
    <span class="hljs-operator">/\</span> x <span class="hljs-keyword">\in</span> Int
    <span class="hljs-operator">/\</span> y <span class="hljs-keyword">\in</span> Int
    <span class="hljs-operator">/\</span> z <span class="hljs-keyword">\in</span> Int

<span class="hljs-title function_">Next</span> <span class="hljs-operator">==</span> <span class="hljs-keyword">UNCHANGED</span> &lt;&lt;x, y, z&gt;&gt;

Identity(F(_, _)) <span class="hljs-operator">==</span>
    F(x, <span class="hljs-number">0</span>) <span class="hljs-operator">=</span> x

Commutativity(F(_, _)) <span class="hljs-operator">==</span>
    F(x, y) <span class="hljs-operator">=</span> F(y, x)

Associativity(F(_, _)) <span class="hljs-operator">==</span>
    F(F(x, y), z) <span class="hljs-operator">=</span> F(x, F(y, z))

<span class="hljs-title function_">InvMath</span> <span class="hljs-operator">==</span>
    <span class="hljs-operator">/\</span> Identity(AddMath)
    <span class="hljs-operator">/\</span> Commutativity(AddMath)
    <span class="hljs-operator">/\</span> Associativity(AddMath)
</code></pre>
<p>With the above specification, we define a very simple state machine that
non-deterministically picks three integers <code>x</code>, <code>y</code>, and <code>z</code> with <code>InitMath</code>.
These variables do not change their values in the state machine, as you can see
from the definition of <code>Next</code>. We use <code>x</code>, <code>y</code>, and <code>z</code> to define three
properties of addition: <code>Identity</code>, <code>Commutativity</code>, and <code>Associativity</code>. As you
can see, these definitions are parameterized by the operator <code>F</code>, which is <code>Add</code>
for now.  Our invariant <code>InvMath</code> is simply the conjunction of the three
properties.</p>
<p>This is how we run Apalache to check the invariant:</p>
<pre><code class="hljs language-sh">$ <span class="hljs-built_in">cd</span> pbt-example-summation/tla-spec
$ docker pull ghcr.io/apalache-mc/apalache
$ docker run --<span class="hljs-built_in">rm</span> -v `<span class="hljs-built_in">pwd</span>`:/var/apalache ghcr.io/apalache-mc/apalache \
  check --init=InitMath --inv=InvMath --length=0 Add.tla
</code></pre>
<p>With the above command, we tell Apalache to check the invariant <code>InvMath</code>
starting from the initial state <code>InitMath</code>. The <code>--length=0</code> option tells
Apalache to unroll <code>Next</code> zero times, which is sufficient in our case, since the
state machine does not change the values of the state variables.</p>
<h2 id="4.-playing-adversarial" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/pbt-adversarial-llms.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#4.-playing-adversarial">4. Playing adversarial</a></h2>
<p>Ok, the code and the specification above seem to be correct. But what if our
friendly AI produced something unexpected?</p>
<h3 id="4.1.-hallucinating-addition-over-32-bit-unsigned-integers" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/pbt-adversarial-llms.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#4.1.-hallucinating-addition-over-32-bit-unsigned-integers">4.1. Hallucinating addition over 32-bit unsigned integers</a></h3>
<p>Since we are dealing with an adversarial developer, they could simply use a
different definition of addition. So far, we have been talking about unbounded
mathematical integers, which Python conveniently implements for us.</p>
<p>Now, the adversarial developer gives us this implementation:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">add32</span>(<span class="hljs-params">a: <span class="hljs-built_in">int</span>, b: <span class="hljs-built_in">int</span></span>) -&gt; <span class="hljs-built_in">int</span>:
    <span class="hljs-keyword">return</span> (a + b) % (<span class="hljs-number">2</span>**<span class="hljs-number">32</span>)
</code></pre>
<p>This implementation is actually not wrong. An LLM could copy it from a code base
that emulates a 32-bit CPU architecture in Python. This is a bit of a stretch,
but possible.</p>
<p>Let's add property-based tests for this implementation as well:</p>
<pre><code class="hljs language-python"><span class="hljs-comment"># Tests for add32 (32-bit natural numbers with wrapping)</span>

<span class="hljs-meta">@given(<span class="hljs-params">st.integers(<span class="hljs-params">min_value=<span class="hljs-number">0</span>, max_value=<span class="hljs-number">2</span>**<span class="hljs-number">32</span> - <span class="hljs-number">1</span></span>)</span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">test_add32_identity</span>(<span class="hljs-params">a</span>):
    <span class="hljs-string">&quot;&quot;&quot;Test identity property for add32: a + 0 = a.&quot;&quot;&quot;</span>
    <span class="hljs-keyword">assert</span> add32(a, <span class="hljs-number">0</span>) == a
    <span class="hljs-keyword">assert</span> add32(<span class="hljs-number">0</span>, a) == a


<span class="hljs-meta">@given(<span class="hljs-params">st.integers(<span class="hljs-params">min_value=<span class="hljs-number">0</span>, max_value=<span class="hljs-number">2</span>**<span class="hljs-number">32</span> - <span class="hljs-number">1</span></span>), st.integers(<span class="hljs-params">min_value=<span class="hljs-number">0</span>, max_value=<span class="hljs-number">2</span>**<span class="hljs-number">32</span> - <span class="hljs-number">1</span></span>)</span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">test_add32_commutativity</span>(<span class="hljs-params">a, b</span>):
    <span class="hljs-string">&quot;&quot;&quot;Test commutativity property for add32: a + b = b + a.&quot;&quot;&quot;</span>
    <span class="hljs-keyword">assert</span> add32(a, b) == add32(b, a)


<span class="hljs-meta">@given(<span class="hljs-params">
    st.integers(<span class="hljs-params">min_value=<span class="hljs-number">0</span>, max_value=<span class="hljs-number">2</span>**<span class="hljs-number">32</span> - <span class="hljs-number">1</span></span>),
    st.integers(<span class="hljs-params">min_value=<span class="hljs-number">0</span>, max_value=<span class="hljs-number">2</span>**<span class="hljs-number">32</span> - <span class="hljs-number">1</span></span>),
    st.integers(<span class="hljs-params">min_value=<span class="hljs-number">0</span>, max_value=<span class="hljs-number">2</span>**<span class="hljs-number">32</span> - <span class="hljs-number">1</span></span>)
</span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">test_add32_associativity</span>(<span class="hljs-params">a, b, c</span>):
    <span class="hljs-string">&quot;&quot;&quot;Test associativity property for add32: (a + b) + c = a + (b + c).&quot;&quot;&quot;</span>
    <span class="hljs-keyword">assert</span> add32(add32(a, b), c) == add32(a, add32(b, c))
</code></pre>
<p>These tests also pass:</p>
<pre><code class="hljs language-sh">$ poetry run pytest tests/test_add.py \
  -k <span class="hljs-string">&quot;test_add32_identity or test_add32_commutativity or test_add32_associativity&quot;</span> \
  --verbose
...
tests/test_add.py::test_add32_identity PASSED                                [ 33%]
tests/test_add.py::test_add32_commutativity PASSED                           [ 66%]
tests/test_add.py::test_add32_associativity PASSED                           [100%]
</code></pre>
<p>What is going on? Well, identity, commutativity, and associativity also hold for
32-bit integers with overflow semantics. <strong>If we let AI generate not only the
implementation but also the properties, we may end up with a correct
implementation, but not the one we wanted!</strong> In this case, imagine an LLM has
added the <code>@given</code> decorators for the inputs to be in the range in <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="6.565ex" height="2.452ex" role="img" focusable="false" viewBox="0 -833.9 2901.8 1083.9"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="5B" d="M118 -250V750H255V710H158V-210H255V-250H118Z"></path></g><g data-mml-node="mn" transform="translate(278,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g><g data-mml-node="mo" transform="translate(778,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="msup" transform="translate(1222.7,0)"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g><g data-mml-node="TeXAtom" transform="translate(533,363) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mn"><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z"></path><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" transform="translate(500,0)"></path></g></g></g><g data-mml-node="mo" transform="translate(2512.8,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g></g></g></svg></mjx-container></span>, whereas we wanted unbounded integers! This is an example of the
classical question in requirements engineering:
<em>do we get things right</em> vs. <em>do we get the right things</em>.</p>
<p>Just to double check that it is not the random chance, I ran Apalache on the
TLA<sup>+</sup> specification above with <code>Add32</code> instead of <code>Add</code>:</p>
<pre><code class="hljs language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">docker run --<span class="hljs-built_in">rm</span> -v `<span class="hljs-built_in">pwd</span>`:/var/apalache ghcr.io/apalache-mc/apalache \
  check --length=0 --init=Init32 --inv=Inv32 Add.tla</span>
...
Checker reports no error up to computation length 0
Total time: 2.205 sec
</code></pre>
<p>Further, the SMT solver Z3 confirms that identity, commutativity, and
associativity hold for 32-bit integers with overflow semantics. This is provided
that we pick the integers from the range <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="6.565ex" height="2.452ex" role="img" focusable="false" viewBox="0 -833.9 2901.8 1083.9"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="5B" d="M118 -250V750H255V710H158V-210H255V-250H118Z"></path></g><g data-mml-node="mn" transform="translate(278,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g><g data-mml-node="mo" transform="translate(778,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="msup" transform="translate(1222.7,0)"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g><g data-mml-node="TeXAtom" transform="translate(533,363) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mn"><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z"></path><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" transform="translate(500,0)"></path></g></g></g><g data-mml-node="mo" transform="translate(2512.8,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g></g></g></svg></mjx-container></span>, which we do with
<code>Init32</code>.</p>
<p>However, <strong>this is not what we wanted initially</strong>. Let's catch the adversarial
developer with the PBT tests that pick unbounded non-negative integers:</p>
<pre><code class="hljs language-python"><span class="hljs-meta">@given(<span class="hljs-params">st.integers(<span class="hljs-params"><span class="hljs-number">0</span></span>)</span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">test_add32_unbounded_inputs_identity</span>(<span class="hljs-params">a</span>):
    <span class="hljs-string">&quot;&quot;&quot;Test identity property for add32: a + 0 = a.&quot;&quot;&quot;</span>
    <span class="hljs-keyword">assert</span> add32(a, <span class="hljs-number">0</span>) == a
    <span class="hljs-keyword">assert</span> add32(<span class="hljs-number">0</span>, a) == a

<span class="hljs-meta">@given(<span class="hljs-params">st.integers(<span class="hljs-params"><span class="hljs-number">0</span></span>), st.integers(<span class="hljs-params"><span class="hljs-number">0</span></span>)</span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">test_add32_unbounded_inputs_commutativity</span>(<span class="hljs-params">a, b</span>):
    <span class="hljs-string">&quot;&quot;&quot;Test commutativity property for add32: a + b = b + a.&quot;&quot;&quot;</span>
    <span class="hljs-keyword">assert</span> add32(a, b) == add32(b, a)


<span class="hljs-meta">@given(<span class="hljs-params">
    st.integers(<span class="hljs-params"><span class="hljs-number">0</span></span>),
    st.integers(<span class="hljs-params"><span class="hljs-number">0</span></span>),
    st.integers(<span class="hljs-params"><span class="hljs-number">0</span></span>)
</span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">test_add32_unbounded_inputs_associativity</span>(<span class="hljs-params">a, b, c</span>):
    <span class="hljs-string">&quot;&quot;&quot;Test associativity property for add32: (a + b) + c = a + (b + c).&quot;&quot;&quot;</span>
    <span class="hljs-keyword">assert</span> add32(add32(a, b), c) == add32(a, add32(b, c))
</code></pre>
<p>This time, Hypothesis catches the issue with identity:</p>
<pre><code class="hljs language-sh">$ poetry run pytest tests/test_add.py  --verbose \
  -k <span class="hljs-string">&quot;test_add32_unbounded_inputs_identity or test_add32_unbounded_inputs_commutativity or test_add32_unbounded_inputs_associativity&quot;</span>
...
a = 4294967296

    @given(st.integers(0))
    def test_add32_unbounded_inputs_identity(a):
        <span class="hljs-string">&quot;&quot;</span><span class="hljs-string">&quot;Test identity property for add32: a + 0 = a.&quot;</span><span class="hljs-string">&quot;&quot;</span>
&gt;       assert add32(a, 0) == a
E       assert 0 == 4294967296
E        +  <span class="hljs-built_in">where</span> 0 = add32(4294967296, 0)
E       Falsifying example: test_add32_unbounded_inputs_identity(
E           a=4_294_967_296,
E       )
</code></pre>
<div class="tip-box">
  <div class="tip-header"><strong>Tip:</strong></div>
  <div class="tip"><p>We can also implement <code>add64</code> that wraps
integers modulo <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="2.919ex" height="1.904ex" role="img" focusable="false" viewBox="0 -841.7 1290.1 841.7"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g><g data-mml-node="TeXAtom" transform="translate(533,363) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mn"><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z"></path><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z" transform="translate(500,0)"></path></g></g></g></g></g></svg></mjx-container></span> and the PBT tests will catch the issue with
identity almost immediately.</p>
</div>
</div>
<h3 id="4.2.-is-property-based-testing-a-magic-tool%3F" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/pbt-adversarial-llms.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#4.2.-is-property-based-testing-a-magic-tool%3F">4.2. Is property-based testing a magic tool?</a></h3>
<p>Let's stop and think about our example. How did Hypothesis catch the issue over
an unbounded integer domain? Even it was picking the integers from the interval
<span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="6.565ex" height="2.47ex" role="img" focusable="false" viewBox="0 -841.7 2901.8 1091.7"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="5B" d="M118 -250V750H255V710H158V-210H255V-250H118Z"></path></g><g data-mml-node="mn" transform="translate(278,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g><g data-mml-node="mo" transform="translate(778,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="msup" transform="translate(1222.7,0)"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g><g data-mml-node="TeXAtom" transform="translate(533,363) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mn"><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z"></path><path data-c="34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z" transform="translate(500,0)"></path></g></g></g><g data-mml-node="mo" transform="translate(2512.8,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g></g></g></svg></mjx-container></span>, the chance of picking <code>4294967296</code> by uniform random sampling is
pretty slim. Yet, Hypothesis keeps picking this number.</p>
<p>Well, the trick is that its input generator tries
the well-known &quot;magic numbers&quot; such as <code>0</code>, <code>1</code>, <code>-1</code>, <code>2**32</code>, <code>2**64</code>, etc.
In this sense, Hypothesis does not use uniform random sampling. See the
discussion on <a href="https://hypothesis.readthedocs.io/en/latest/explanation/domain.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">domain and distribution</a> in the Hypothesis documentation for
more details.</p>
<p>What if our adversarial developer hallucinated an implementation that stays
undetected by Hypothesis? This is what our next example is about.</p>
<h3 id="4.3.-hallucinating-addition-over-256-bit-unsigned-integers" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/pbt-adversarial-llms.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#4.3.-hallucinating-addition-over-256-bit-unsigned-integers">4.3. Hallucinating addition over 256-bit unsigned integers</a></h3>
<p>This time, the adversarial developer uses 256-bit unsigned integers with overflow
semantics:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">add256</span>(<span class="hljs-params">a: <span class="hljs-built_in">int</span>, b: <span class="hljs-built_in">int</span></span>) -&gt; <span class="hljs-built_in">int</span>:
    <span class="hljs-keyword">return</span> (a + b) % (<span class="hljs-number">2</span>**<span class="hljs-number">256</span>)
</code></pre>
<p>If you think that using 256-bit integers is absurd, well, the Ethereum Virtual
Machine (EVM) does exactly that. So an LLM could have adapted the above code
from an EVM-related code base.</p>
<pre><code class="hljs language-python"><span class="hljs-meta">@given(<span class="hljs-params">st.integers(<span class="hljs-params"><span class="hljs-number">0</span></span>)</span>)</span>
<span class="hljs-meta">@settings(<span class="hljs-params">max_examples=<span class="hljs-number">100000</span></span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">test_add256_unbounded_inputs_identity</span>(<span class="hljs-params">a</span>):
    <span class="hljs-string">&quot;&quot;&quot;Test identity property for add256: a + 0 = a.&quot;&quot;&quot;</span>
    <span class="hljs-keyword">assert</span> add256(a, <span class="hljs-number">0</span>) == a
    <span class="hljs-keyword">assert</span> add256(<span class="hljs-number">0</span>, a) == a

<span class="hljs-meta">@given(<span class="hljs-params">st.integers(<span class="hljs-params"><span class="hljs-number">0</span></span>), st.integers(<span class="hljs-params"><span class="hljs-number">0</span></span>)</span>)</span>
<span class="hljs-meta">@settings(<span class="hljs-params">max_examples=<span class="hljs-number">100000</span></span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">test_add256_unbounded_inputs_commutativity</span>(<span class="hljs-params">a, b</span>):
    <span class="hljs-string">&quot;&quot;&quot;Test commutativity property for add256: a + b = b + a.&quot;&quot;&quot;</span>
    <span class="hljs-keyword">assert</span> add256(a, b) == add256(b, a)


<span class="hljs-meta">@given(<span class="hljs-params">
    st.integers(<span class="hljs-params"><span class="hljs-number">0</span></span>),
    st.integers(<span class="hljs-params"><span class="hljs-number">0</span></span>),
    st.integers(<span class="hljs-params"><span class="hljs-number">0</span></span>)
</span>)</span>
<span class="hljs-meta">@settings(<span class="hljs-params">max_examples=<span class="hljs-number">100000</span></span>)</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">test_add256_unbounded_inputs_associativity</span>(<span class="hljs-params">a, b, c</span>):
    <span class="hljs-string">&quot;&quot;&quot;Test associativity property for add256: (a + b) + c = a + (b + c).&quot;&quot;&quot;</span>
    <span class="hljs-keyword">assert</span> add256(add256(a, b), c) == add256(a, add256(b, c))
</code></pre>
<p>This time, the adversarial developer gets away, all tests pass:</p>
<pre><code class="hljs language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">poetry run pytest tests/test_add.py --verbose -k \
  <span class="hljs-string">&quot;test_add256_unbounded_inputs_identity or test_add256_unbounded_inputs_commutativity or test_add256_unbounded_inputs_associativity&quot;</span></span> 
...
tests/test_add.py::test_add256_unbounded_inputs_identity PASSED                [ 33%]
tests/test_add.py::test_add256_unbounded_inputs_commutativity PASSED           [ 66%]
tests/test_add.py::test_add256_unbounded_inputs_associativity PASSED           [100%]
</code></pre>
<p>Why? Hypothesis does not try <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="3.719ex" height="1.887ex" role="img" focusable="false" viewBox="0 -833.9 1643.7 833.9"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g><g data-mml-node="TeXAtom" transform="translate(533,363) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" transform="translate(500,0)"></path><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" transform="translate(1000,0)"></path></g></g></g></g></g></svg></mjx-container></span> as a magic number. I gave it the budget of
100,000 examples, so it had a chance to try multiple powers of two, but it did
not try anything above <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="7.616ex" height="2.072ex" role="img" focusable="false" viewBox="0 -833.9 3366.1 915.9"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g><g data-mml-node="TeXAtom" transform="translate(533,363) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" transform="translate(500,0)"></path><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" transform="translate(1000,0)"></path></g></g></g><g data-mml-node="mo" transform="translate(1865.9,0)"><path data-c="2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path></g><g data-mml-node="mn" transform="translate(2866.1,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path></g></g></g></svg></mjx-container></span>.</p>
<p>We can make sure that the identity test indeed fails when we pass <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="3.719ex" height="1.887ex" role="img" focusable="false" viewBox="0 -833.9 1643.7 833.9"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g><g data-mml-node="TeXAtom" transform="translate(533,363) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" transform="translate(500,0)"></path><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" transform="translate(1000,0)"></path></g></g></g></g></g></svg></mjx-container></span>
as an example:</p>
<pre><code class="hljs language-python"><span class="hljs-meta">@given(<span class="hljs-params">st.integers(<span class="hljs-params"><span class="hljs-number">0</span></span>)</span>)</span>
<span class="hljs-meta">@settings(<span class="hljs-params">max_examples=<span class="hljs-number">100000</span></span>)</span>
<span class="hljs-meta">@example(<span class="hljs-params"><span class="hljs-number">2</span>**<span class="hljs-number">256</span></span>)  </span><span class="hljs-comment"># This should fail!</span>
<span class="hljs-keyword">def</span> <span class="hljs-title function_">test_add256_unbounded_inputs_identity</span>(<span class="hljs-params">a</span>):
    <span class="hljs-string">&quot;&quot;&quot;Test identity property for add256: a + 0 = a.&quot;&quot;&quot;</span>
    <span class="hljs-keyword">assert</span> add256(a, <span class="hljs-number">0</span>) == a
    <span class="hljs-keyword">assert</span> add256(<span class="hljs-number">0</span>, a) == a
</code></pre>
<h3 id="4.4.-catching-the-adversarial-developer-with-apalache" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/pbt-adversarial-llms.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#4.4.-catching-the-adversarial-developer-with-apalache">4.4. Catching the adversarial developer with Apalache</a></h3>
<p>Here is how we modify the TLA<sup>+</sup> specification to use <code>Add256</code>:</p>
<pre><code class="hljs language-tlaplus">Add256(a, b) <span class="hljs-operator">==</span> (a <span class="hljs-operator">+</span> b) % (<span class="hljs-number">2</span><span class="hljs-operator">^</span><span class="hljs-number">256</span>)

<span class="hljs-title function_">InitNat</span> <span class="hljs-operator">==</span>
    <span class="hljs-operator">/\</span> x <span class="hljs-keyword">\in</span> <span class="hljs-keyword">Nat</span>
    <span class="hljs-operator">/\</span> y <span class="hljs-keyword">\in</span> <span class="hljs-keyword">Nat</span>
    <span class="hljs-operator">/\</span> z <span class="hljs-keyword">\in</span> <span class="hljs-keyword">Nat</span>

<span class="hljs-title function_">Inv256</span> <span class="hljs-operator">==</span>
    <span class="hljs-operator">/\</span> Identity(Add256)
    <span class="hljs-operator">/\</span> Commutativity(Add256)
    <span class="hljs-operator">/\</span> Associativity(Add256)
</code></pre>
<p>Apalache immediately finds the issue with identity when we run it with <code>Add256</code>:</p>
<pre><code class="hljs language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">docker run --<span class="hljs-built_in">rm</span> -v `<span class="hljs-built_in">pwd</span>`:/var/apalache ghcr.io/apalache-mc/apalache \
  check --length=0 --init=InitNat --inv=Inv256 Add.tla</span>
...
State 0: state invariant 0 violated.
Total time: 2.272 sec
</code></pre>
<p>If we check the counterexample, we see that the solver picks the value <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="3.719ex" height="1.887ex" role="img" focusable="false" viewBox="0 -833.9 1643.7 833.9"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g><g data-mml-node="TeXAtom" transform="translate(533,363) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" transform="translate(500,0)"></path><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" transform="translate(1000,0)"></path></g></g></g></g></g></svg></mjx-container></span>
for <code>x</code>:</p>
<pre><code class="hljs language-sh">$ <span class="hljs-built_in">head</span> -n 14 _apalache-out/Add.tla/2025-12-22T15-20-31_8166638658721555415/violation.tla
---------------------------- MODULE counterexample ----------------------------
EXTENDS Add

(* Constant initialization state *)
ConstInit == TRUE

(* Initial state [_transition(0)] *)
State0 ==
  x
      = 115792089237316195423570985008687907853269984665640564039457584007913129639936
    /\ y = 0
    /\ z = 0

</code></pre>
<p><strong>This is not just luck and not an heuristic!</strong> Apalache delegates solving to
the SMT solver <a href="https://github.com/Z3Prover/z3?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Z3</a>, which solves integer constraints. If you want to make
sure that it's not using magic numbers, go and change the modulo operator in
<code>Add256</code> to a large prime number, e.g., <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.186ex;" xmlns="http://www.w3.org/2000/svg" width="9.878ex" height="2.072ex" role="img" focusable="false" viewBox="0 -833.9 4366.1 915.9"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g><g data-mml-node="TeXAtom" transform="translate(533,363) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" transform="translate(500,0)"></path><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" transform="translate(1000,0)"></path></g></g></g><g data-mml-node="mo" transform="translate(1865.9,0)"><path data-c="2B" d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z"></path></g><g data-mml-node="mn" transform="translate(2866.1,0)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path><path data-c="39" d="M352 287Q304 211 232 211Q154 211 104 270T44 396Q42 412 42 436V444Q42 537 111 606Q171 666 243 666Q245 666 249 666T257 665H261Q273 665 286 663T323 651T370 619T413 560Q456 472 456 334Q456 194 396 97Q361 41 312 10T208 -22Q147 -22 108 7T68 93T121 149Q143 149 158 135T173 96Q173 78 164 65T148 49T135 44L131 43Q131 41 138 37T164 27T206 22H212Q272 22 313 86Q352 142 352 280V287ZM244 248Q292 248 321 297T351 430Q351 508 343 542Q341 552 337 562T323 588T293 615T246 625Q208 625 181 598Q160 576 154 546T147 441Q147 358 152 329T172 282Q197 248 244 248Z" transform="translate(500,0)"></path><path data-c="37" d="M55 458Q56 460 72 567L88 674Q88 676 108 676H128V672Q128 662 143 655T195 646T364 644H485V605L417 512Q408 500 387 472T360 435T339 403T319 367T305 330T292 284T284 230T278 162T275 80Q275 66 275 52T274 28V19Q270 2 255 -10T221 -22Q210 -22 200 -19T179 0T168 40Q168 198 265 368Q285 400 349 489L395 552H302Q128 552 119 546Q113 543 108 522T98 479L95 458V455H55V458Z" transform="translate(1000,0)"></path></g></g></g></svg></mjx-container></span>. Rerun the model
checker, and it will still find the issue with identity.</p>
<h3 id="4.5.-for-the-curious%3A-how-apalache-and-z3-work-together" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/pbt-adversarial-llms.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#4.5.-for-the-curious%3A-how-apalache-and-z3-work-together">4.5. For the curious: how Apalache and Z3 work together</a></h3>
<p>Our example is so simple that we can even go over the actual SMT constraints
that Apalache generates. Let's run Apalache with the option <code>--debug</code>:</p>
<pre><code class="hljs language-sh">$ docker run --<span class="hljs-built_in">rm</span> -v `<span class="hljs-built_in">pwd</span>`:/var/apalache ghcr.io/apalache-mc/apalache \
  check --debug \
  --length=0 --init=InitNat --inv=Inv256 Add.tla
</code></pre>
<p>Open the file
<code>_apalache-out/Add.tla/2025-12-23T09-31-46_16436938462355564409/log0.smt</code> (the
timestamp will be different on your machine). The log is pretty verbose. Here
are the crucial parts, which I've accompanied with explanations:</p>
<pre><code class="hljs language-lisp">-- the initial value of `x`
(<span class="hljs-name">declare-const</span> $C$<span class="hljs-number">6</span> Int)
-- `x` is a natural number
(<span class="hljs-name">assert</span> (<span class="hljs-name">&gt;=</span> $C$<span class="hljs-number">6</span> <span class="hljs-number">0</span>))
-- introduce a boolean variable for the identity property
(<span class="hljs-name">declare-const</span> $C$<span class="hljs-number">9</span> Bool)
-- Encode the identity property for `Add256` and `x`.
-- The huge number is <span class="hljs-number">2</span>^<span class="hljs-number">256</span>.
(<span class="hljs-name">assert</span> (<span class="hljs-name">=</span> $C$<span class="hljs-number">9</span>
   (<span class="hljs-name">=</span> (<span class="hljs-name">mod</span> $C$<span class="hljs-number">6</span>
           <span class="hljs-number">115792089237316195423570985008687907853269984665640564039457584007913129639936</span>)
      $C$<span class="hljs-number">6</span>)))
-- assert that the identity property is violated
(<span class="hljs-name">declare-const</span> $C$<span class="hljs-number">10</span> Bool)
(<span class="hljs-name">assert</span> (<span class="hljs-name">=</span> (<span class="hljs-name">not</span> $C$<span class="hljs-number">10</span>) $C$<span class="hljs-number">9</span>))
(<span class="hljs-name">assert</span> $C$<span class="hljs-number">10</span>)
-- check, whether the above constraints have a solution
(<span class="hljs-name">check-sat</span>)
</code></pre>
<p>If you want to understand what is going on, read the comments above. At first, I
was actually surprised that the SMT constraints did not contain addition at all.
Then I recalled that Apalache has a bunch of rewriting rules that simplify the
constraints. In this case, the symbolic model checker has applied the property
<code>a + 0 = a</code> internally to get rid of the addition (yeah, it is the identity
property!).  It was an equivalent transformation, so we are still left with the
modulo operator.</p>
<p>Essentially, we are asking Z3 to solve these inequalities over integers:</p>
<div class="math-display"><mjx-container class="MathJax" jax="SVG" display="true"><svg style="vertical-align: -2.188ex;" xmlns="http://www.w3.org/2000/svg" width="20.285ex" height="5.507ex" role="img" focusable="false" viewBox="0 -1467 8965.9 2433.9"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mrow"><g data-mml-node="mo" transform="translate(0 -0.5)"><path data-c="7B" d="M618 -943L612 -949H582L568 -943Q472 -903 411 -841T332 -703Q327 -682 327 -653T325 -350Q324 -28 323 -18Q317 24 301 61T264 124T221 171T179 205T147 225T132 234Q130 238 130 250Q130 255 130 258T131 264T132 267T134 269T139 272T144 275Q207 308 256 367Q310 436 323 519Q324 529 325 851Q326 1124 326 1154T332 1205Q369 1358 566 1443L582 1450H612L618 1444V1429Q618 1413 616 1411L608 1406Q599 1402 585 1393T552 1372T515 1343T479 1305T449 1257T429 1200Q425 1180 425 1152T423 851Q422 579 422 549T416 498Q407 459 388 424T346 364T297 318T250 284T214 264T197 254L188 251L205 242Q290 200 345 138T416 3Q421 -18 421 -48T423 -349Q423 -397 423 -472Q424 -677 428 -694Q429 -697 429 -699Q434 -722 443 -743T465 -782T491 -816T519 -845T548 -868T574 -886T595 -899T610 -908L616 -910Q618 -912 618 -928V-943Z"></path></g><g data-mml-node="mtable" transform="translate(750,0)"><g data-mml-node="mtr" transform="translate(0,717)"><g data-mml-node="mtd"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g></g><g data-mml-node="mtd" transform="translate(572,0)"><g data-mml-node="mi"></g><g data-mml-node="mo" transform="translate(277.8,0)"><path data-c="2265" d="M83 616Q83 624 89 630T99 636Q107 636 253 568T543 431T687 361Q694 356 694 346T687 331Q685 329 395 192L107 56H101Q83 58 83 76Q83 77 83 79Q82 86 98 95Q117 105 248 167Q326 204 378 228L626 346L360 472Q291 505 200 548Q112 589 98 597T83 616ZM84 -118Q84 -108 99 -98H678Q694 -104 694 -118Q694 -130 679 -138H98Q84 -131 84 -118Z"></path></g><g data-mml-node="mn" transform="translate(1333.6,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g></g></g><g data-mml-node="mtr" transform="translate(0,-717)"><g data-mml-node="mtd"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g></g><g data-mml-node="mtd" transform="translate(572,0)"><g data-mml-node="mspace"></g><g data-mml-node="mo" transform="translate(1000,0)"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="mi" transform="translate(1389,0)"><path data-c="6D" d="M41 46H55Q94 46 102 60V68Q102 77 102 91T102 122T103 161T103 203Q103 234 103 269T102 328V351Q99 370 88 376T43 385H25V408Q25 431 27 431L37 432Q47 433 65 434T102 436Q119 437 138 438T167 441T178 442H181V402Q181 364 182 364T187 369T199 384T218 402T247 421T285 437Q305 442 336 442Q351 442 364 440T387 434T406 426T421 417T432 406T441 395T448 384T452 374T455 366L457 361L460 365Q463 369 466 373T475 384T488 397T503 410T523 422T546 432T572 439T603 442Q729 442 740 329Q741 322 741 190V104Q741 66 743 59T754 49Q775 46 803 46H819V0H811L788 1Q764 2 737 2T699 3Q596 3 587 0H579V46H595Q656 46 656 62Q657 64 657 200Q656 335 655 343Q649 371 635 385T611 402T585 404Q540 404 506 370Q479 343 472 315T464 232V168V108Q464 78 465 68T468 55T477 49Q498 46 526 46H542V0H534L510 1Q487 2 460 2T422 3Q319 3 310 0H302V46H318Q379 46 379 62Q380 64 380 200Q379 335 378 343Q372 371 358 385T334 402T308 404Q263 404 229 370Q202 343 195 315T187 232V168V108Q187 78 188 68T191 55T200 49Q221 46 249 46H265V0H257L234 1Q210 2 183 2T145 3Q42 3 33 0H25V46H41Z"></path><path data-c="6F" d="M28 214Q28 309 93 378T250 448Q340 448 405 380T471 215Q471 120 407 55T250 -10Q153 -10 91 57T28 214ZM250 30Q372 30 372 193V225V250Q372 272 371 288T364 326T348 362T317 390T268 410Q263 411 252 411Q222 411 195 399Q152 377 139 338T126 246V226Q126 130 145 91Q177 30 250 30Z" transform="translate(833,0)"></path><path data-c="64" d="M376 495Q376 511 376 535T377 568Q377 613 367 624T316 637H298V660Q298 683 300 683L310 684Q320 685 339 686T376 688Q393 689 413 690T443 693T454 694H457V390Q457 84 458 81Q461 61 472 55T517 46H535V0Q533 0 459 -5T380 -11H373V44L365 37Q307 -11 235 -11Q158 -11 96 50T34 215Q34 315 97 378T244 442Q319 442 376 393V495ZM373 342Q328 405 260 405Q211 405 173 369Q146 341 139 305T131 211Q131 155 138 120T173 59Q203 26 251 26Q322 26 373 103V342Z" transform="translate(1333,0)"></path></g><g data-mml-node="mspace" transform="translate(3278,0)"></g><g data-mml-node="msup" transform="translate(3777.7,0)"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g><g data-mml-node="TeXAtom" transform="translate(533,413) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z" transform="translate(500,0)"></path><path data-c="36" d="M42 313Q42 476 123 571T303 666Q372 666 402 630T432 550Q432 525 418 510T379 495Q356 495 341 509T326 548Q326 592 373 601Q351 623 311 626Q240 626 194 566Q147 500 147 364L148 360Q153 366 156 373Q197 433 263 433H267Q313 433 348 414Q372 400 396 374T435 317Q456 268 456 210V192Q456 169 451 149Q440 90 387 34T253 -22Q225 -22 199 -14T143 16T92 75T56 172T42 313ZM257 397Q227 397 205 380T171 335T154 278T148 216Q148 133 160 97T198 39Q222 21 251 21Q302 21 329 59Q342 77 347 104T352 209Q352 289 347 316T329 361Q302 397 257 397Z" transform="translate(1000,0)"></path></g></g></g><g data-mml-node="mo" transform="translate(5421.3,0)"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g><g data-mml-node="mo" transform="translate(6088.1,0)"><path data-c="2260" d="M166 -215T159 -215T147 -212T141 -204T139 -197Q139 -190 144 -183L306 133H70Q56 140 56 153Q56 168 72 173H327L406 327H72Q56 332 56 347Q56 360 70 367H426Q597 702 602 707Q605 716 618 716Q625 716 630 712T636 703T638 696Q638 692 471 367H707Q722 359 722 347Q722 336 708 328L451 327L371 173H708Q722 163 722 153Q722 140 707 133H351Q175 -210 170 -212Q166 -215 159 -215Z"></path></g><g data-mml-node="mn" transform="translate(7143.9,0)"><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z"></path></g></g></g></g><g data-mml-node="mo" transform="translate(8965.9,0) translate(0 250)"></g></g></g></g></svg></mjx-container></div>
<p>What is crucial here is that the SMT solver <strong>Z3 has a strict contract with the
user</strong>. When we give it a set of constraints and ask it to check their
satisfiability, it will apply sound algorithms to arrive at one of the three
answers:</p>
<ul>
<li>
<p><strong>sat</strong>: there is a solution to the above constraints, i.e., an assignment of
values to the variables that makes the constraints true,</p>
</li>
<li>
<p><strong>unsat</strong>: there is no solution, and</p>
</li>
<li>
<p><strong>unknown</strong>: the constraints are too hard, or it took the solver too long to
solve them.</p>
</li>
</ul>
<p>In contrast to PBT, it is not just like &quot;I tried a few random inputs and did not
find a bug&quot;. If Z3 answers <code>sat</code>, there is indeed a solution to the constraints,
and the solver gives it to us as a model. If it returns <code>unsat</code>, there is no
solution. Whenever you see <code>unknown</code>, it's a bad day. Sometimes, it also
indicates a bug in the solver itself, as I've <a href="https://github.com/Z3Prover/z3/issues?q=is%3Aissue+state%3Aclosed+author%3Akonnov&amp;utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">reported to the Z3 developers a
few times</a>. However, Z3 is pretty reliable in my experience, and
producing an <code>unknown</code> is an achievement, unless you set very tight timeouts,
or use tricky non-linear arithmetic.</p>
<p>If you are further interested in how Z3 actually solved the above constraints, a
simple answer is that it used something like the <a href="https://en.wikipedia.org/wiki/Simplex_algorithm?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Simplex algorithm</a> for
integer linear programming. The constraints are linear in our case, so Z3 could
apply this algorithm to find a solution. Most likely, Z3 used a more recent
algorithm, but the idea is similar.</p>
<p>In case you really want to know how SMT solvers work under the hood, I recommend
starting with the book on <a href="https://www.decision-procedures.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Decision Procedures</a> by Kroening and Strichman.</p>
<h2 id="5.-conclusions" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/pbt-adversarial-llms.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#5.-conclusions">5. Conclusions</a></h2>
<p>We all have to <strong>learn how to write high-quality properties and understand the
boundaries of the &quot;magic&quot; tools</strong>. <strong>Even if you don't use LLMs, your peers
will.</strong></p>
<p><strong>How to learn writing good properties</strong>? We can play with property-based
testing. However, <strong>PBT tools are not reliable teachers</strong>. By their random
nature, a PBT tool may miss a bug on one run and find it on another run. Don't
get me wrong. <strong>Property-based testing has its value</strong>, as many other testing
and verification techniques. However, <strong>PBT is not a silver bullet</strong>. It may
miss bugs, especially if the input generator does not cover the right input
space well enough.</p>
<p><strong>Shall we use interactive provers like <a href="https://lean-lang.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Lean</a> and <a href="https://rocq-prover.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Rocq</a></strong>? Learning how to
prove code correct definitely helps! However, <strong>these tools only tell us that the
proof does not go through</strong>. It would not give us a counterexample.
State-of-the-art provers also recommend using PBT for bug finding.</p>
<p>In my opinion, <strong>model checking is the best way to learn how to write good
properties</strong>. You can write as many properties as you like, and the model
checker will produce you counterexamples, or not. Importantly, model checkers
come with a guarantee of not having a bug in their <em>search scope</em>, if they
terminate.  See my blog post on the <a href="https://protocols-made-fun.com/value.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">value of model
checking</a> on that.</p>
<p>Usually, I recommend people to start with <a href="https://github.com/tlaplus/tlaplus?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">TLC</a>. It works by state enumeration
and easy to understand. If your search scope is small, TLC is a good learning
tool. In our example, the search scope is astronomical. In this case,
<a href="https://apalache-mc.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache</a> is there to help.</p>
<p>By the way, our example was so simple, that we could encode it in <a href="https://github.com/Z3Prover/z3?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Z3</a>
directly via its python bindings. We could use other model checkers. If you do
that, let me know!</p>
<h2 id="6.-bonus%3A-hypothesis-%2B-crosshair" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/pbt-adversarial-llms.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#6.-bonus%3A-hypothesis-%2B-crosshair">6. Bonus: Hypothesis + Crosshair</a></h2>
<p>Hypothesis offers an integration with <a href="https://crosshair.readthedocs.io/en/latest/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Crosshair</a>, which is a symbolic
execution engine for Python using Z3. I did not explore this integration in
depth. Claude told me that it is sufficient to just add this import to the test:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">import</span> hypothesis_crosshair_provider
</code></pre>
<p>Well, this did not help me to find the violation of identity for <code>add256</code>.  If
you know how to make Crosshair work with Hypothesis, please let me know!</p>
<p>When we run Crosshair directly on the <code>add256</code> implementation, it finds the
issue with identity right away:</p>
<pre><code class="hljs language-sh">$ poetry run crosshair check tests.test_add_crosshair.check_add256_identity
.../python/tests/test_add_crosshair.py:12: error: <span class="hljs-literal">false</span> when calling check_add256_identity(115792089237316195423570985008687907853269984665640564039457584007913129639936) (<span class="hljs-built_in">which</span> returns False)
</code></pre>
<p>The Crosshair test looks like follows:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> crosshair.core_and_libs <span class="hljs-keyword">import</span> standalone_statespace
<span class="hljs-keyword">from</span> pbt_add <span class="hljs-keyword">import</span> add256


<span class="hljs-keyword">def</span> <span class="hljs-title function_">check_add256_identity</span>(<span class="hljs-params">a: <span class="hljs-built_in">int</span></span>) -&gt; <span class="hljs-built_in">bool</span>:
    <span class="hljs-string">&quot;&quot;&quot;
    Check identity property for add256: a + 0 = a.
    
    pre: a &gt;= 0
    post: _
    &quot;&quot;&quot;</span>
    <span class="hljs-keyword">return</span> add256(a, <span class="hljs-number">0</span>) == a <span class="hljs-keyword">and</span> add256(<span class="hljs-number">0</span>, a) == a
</code></pre>
<!-- References -->
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>I've never met Scott Wlaschin in real life, online or offline. I hope he would not mind me referring to him by his first name. <a href="https://protocols-made-fun.com/pbt-adversarial-llms.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#fnref1" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
]]></content>
    <author>
      <name>Igor Konnov</name>
      <email>igor@konnov.phd</email>
      <uri>https://konnov.phd/?utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site</uri>
    </author>
    
    
    <category term="pbt" />
    
    
    
  </entry>
  
  
  <entry>
    <title type="html">Interactive Symbolic Testing of TFTP with TLA+ and Apalache</title>
    <link href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" rel="alternate" type="text/html" title="Interactive Symbolic Testing of TFTP with TLA+ and Apalache" />
    <published>2025-12-15T00:00:00.000Z</published>
    <updated>2025-12-15T00:00:00.000Z</updated>
    <id>https://protocols-made-fun.com/tftp-symbolic-testing.html</id>
    <content type="html" xml:base="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"><![CDATA[<p><strong>Author:</strong> <a href="https://konnov.phd/?utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site">Igor Konnov</a></p>
<p><strong>Date:</strong> December 15, 2025</p>
<p><em>Note: I mostly stopped using LLMs for proof-reading my texts, so you know
it is not all generated. Enjoy my typos and weird grammar!</em></p>
<p><strong>Abstract.</strong> As promised in the <a href="https://protocols-made-fun.com/small-scope.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">blog post on small-scope
hypothesis</a>, I am continuing with the main body of the talk that I
presented at the internal Nvidia FM Week 2025. This blog post is rather long. If
you do not want to read the whole post, here are the most exciting new
developments:</p>
<ul>
<li>
<p>A <strong>new JSON-RPC server API</strong> for <a href="https://apalache-mc.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache</a>, which allows external tools
and scripts to drive the symbolic execution of TLA<sup>+</sup> specifications
and interact with the solver.  Read the section on <a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#4-the-new-json-rpc-api-of-apalache">The new JSON-RPC API of
Apalache</a>.</p>
</li>
<li>
<p>A new approach to <strong>conformance testing of TLA<sup>+</sup> specifications and
real implementations</strong>, called <strong>interactive symbolic testing</strong>. This approach
is inspired by the work of <a href="https://www.mcmil.net/pubs/SIGCOMM19.pdf?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">McMillan and Zuck (2019)</a> on testing of the
QUIC protocol with IVy and SMT. Read the section on <a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#3-interactive-symbolic-testing-with-smt">Interactive symbolic
testing with SMT</a>.</p>
</li>
<li>
<p>A case study on <strong>testing multiple open-source implementations of TFTP</strong>,
including unexpected (but not harmful) deviations from the protocol. This case
study includes the experience report on using Claude to bootstrap the harness
for testing TFTP implementations against the TLA<sup>+</sup> specification.
Read the section on <a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#7-bootstrapping-the-testing-harness-with-claude">Bootstrapping the testing harness with
Claude</a> and <a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#9-testing-against-adversarial-behavior">Testing against
adversarial behavior</a>.  My point is
not <strong>not to brainwash you into LLMs</strong>, but to <strong>show what works for me and
what does not</strong>.</p>
</li>
<li>
<p>The specification and the test harness are <strong>openly available</strong>. Check the
<a href="https://github.com/konnov/tftp-symbolic-testing?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Github repository</a>.</p>
</li>
</ul>
<p>In this blog post, I am using TLA<sup>+</sup>. The same tooling and results
equally apply to <a href="https://github.com/informalsystems/quint?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Quint</a>.</p>
<p><strong>Contents:</strong></p>
<ol>
<li><a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#1-introduction">Introduction</a></li>
<li><a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#2-model-based-testing-and-trace-validation">Model-based testing and trace validation</a></li>
<li><a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#3-interactive-symbolic-testing-with-smt">Interactive symbolic testing with SMT</a></li>
<li><a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#4-the-new-json-rpc-api-of-apalache">The new JSON-RPC API of Apalache</a></li>
<li><a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#5-case-study-tftp-protocol">Case study: TFTP protocol</a></li>
<li><a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#6-initial-tla-specification-of-tftp">Initial TLA<sup>+</sup> specification of TFTP</a></li>
<li><a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#7-bootstrapping-the-testing-harness-with-claude">Bootstrapping the testing harness with Claude</a></li>
<li><a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#8-debugging-the-tla-specification-with-the-implementation">Debugging the TLA<sup>+</sup> specification with the implementation</a></li>
<li><a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#9-testing-against-adversarial-behavior">Testing against adversarial behavior</a></li>
<li><a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#10-the-specification-as-a-differential-testing-oracle">The specification as a differential testing oracle</a></li>
<li><a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#11-prior-work">Prior Work</a></li>
<li><a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#12-conclusions">Conclusions</a></li>
</ol>
<h2 id="1.-introduction" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#1.-introduction">1. Introduction</a></h2>
<p>This work aims at demonstrating how to answer the following two questions with
<a href="https://apalache-mc.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache</a>:</p>
<p class="highlight-question"><strong><em>
  1. How to test the actual implementation against its TLA<sup>+</sup> specification?
</em></strong></p>
<p class="highlight-question"><strong><em>
  2. How to test the TLA<sup>+</sup> specification against the actual implementation?
</em></strong></p>
<p>For long time, these questions have been mostly ignored by the TLA<sup>+</sup>
community. Over the last 4-5 years, researchers started to look into these two
questions and found out that having a connection between the specification and
the implementation is much more useful than it was initially thought. (The
engineers were telling this to me all the time!) Check <a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#10-prior-work">the prior work
section</a> for the papers and talks on this topic.  Roughly
speaking, the approaches follow the two ideas:</p>
<ul>
<li>
<p><strong>Model-based testing (MBT)</strong>. The TLA<sup>+</sup> specification is used to
generate test cases that are then executed against the implementation. This is
an answer to question 1 above. The state exploration is driven by the
specification. Hence, we are testing, whether the implementation matches the
inputs and outputs, as produced by the specification.</p>
</li>
<li>
<p><strong>Trace validation (TV)</strong>. The traces are collected from the implementation
and checked against the TLA<sup>+</sup> specification. This is an answer to
question 2 above. State exploration is driven by the implementation, e.g., by
executing the existing test suites, or just by running the system for some
time. Hence, we are testing whether the specification matches the inputs and
outputs of the implementation. Alternatively, we may check whether the
implementation states may be lifted to the specification states, in order to
produce a feasible trace in the specification.</p>
</li>
</ul>
<p>If you re-read the description of MBT and TV above, you may notice that there
are two more dimensions of how to do testing:</p>
<ul>
<li>
<p><strong>State-based</strong>. In this case, we have to establish a relation between the
implementation states and the specification states in each step of the trace.
This usually done by defining mapping functions, either from the implementation
states to the specification states, or vice versa. Notice that mapping an
implementation state to a specification state is usually much easier, as it
involves <em>state abstraction</em> (e.g., dropping some variables). Mapping a
specification state to an implementation state is more difficult, as it
involves <em>state concretization</em>, e.g., choosing a representative concrete value
for each abstract value in the specification state. For example, if the
specification says <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="10.849ex" height="2.262ex" role="img" focusable="false" viewBox="0 -750 4795.2 1000"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g><g data-mml-node="mo" transform="translate(849.8,0)"><path data-c="2208" d="M84 250Q84 372 166 450T360 539Q361 539 377 539T419 540T469 540H568Q583 532 583 520Q583 511 570 501L466 500Q355 499 329 494Q280 482 242 458T183 409T147 354T129 306T124 272V270H568Q583 262 583 250T568 230H124V228Q124 207 134 177T167 112T231 48T328 7Q355 1 466 0H570Q583 -10 583 -20Q583 -32 568 -40H471Q464 -40 446 -40T417 -41Q262 -41 172 45Q84 127 84 250Z"></path></g><g data-mml-node="mo" transform="translate(1794.6,0)"><path data-c="5B" d="M118 -250V750H255V710H158V-210H255V-250H118Z"></path></g><g data-mml-node="mn" transform="translate(2072.6,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(500,0)"></path></g><g data-mml-node="mo" transform="translate(3072.6,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="mn" transform="translate(3517.2,0)"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(500,0)"></path></g><g data-mml-node="mo" transform="translate(4517.2,0)"><path data-c="5D" d="M22 710V750H159V-250H22V-210H119V710H22Z"></path></g></g></g></svg></mjx-container></span>, then we have to choose a concrete value
for <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.025ex;" xmlns="http://www.w3.org/2000/svg" width="1.294ex" height="1.025ex" role="img" focusable="false" viewBox="0 -442 572 453"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><path data-c="1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path></g></g></g></svg></mjx-container></span> in this range, e.g., at random.</p>
</li>
<li>
<p><strong>Action-based</strong>. In this case, we have to establish a relation between the
implementation actions and the specification actions. Again, we would need to
define mappings. Interestingly, in my experience, defining action mappings is
way easier than defining state mappings.</p>
</li>
</ul>
<h2 id="2.-model-based-testing-and-trace-validation" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#2.-model-based-testing-and-trace-validation">2. Model-based testing and trace validation</a></h2>
<h3 id="2.1.-model-based-testing-in-one-picture" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#2.1.-model-based-testing-in-one-picture">2.1. Model-based testing in one picture</a></h3>
<p>Without going into too many details, the following picture illustrates the main
idea of model-based testing. We generate an &quot;interesting&quot; trace with a model
checker, e.g., with <a href="https://apalache-mc.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache</a>. This trace is fed to the test harness that:
(1) does action concretization, (2) executes the actions against the
implementation. The moment the implementation refuses to replay an action, we
know that there is a divergence. Notice that we often do not even need to query
the system for its current state, as we only care about the actions.</p>
<picture>
  <img class="responsive-img"
    src="https://protocols-made-fun.com/img/mbt.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" alt="Model-based testing">
</picture>
<p>One downside of this approach is that the model checker can be quickly overwhelmed
by the many possible action interleavings unless the search scope is further
restricted. In my experience, the SMT solver Z3 slows down dramatically when it
must solve two problems simultaneously:</p>
<ol>
<li>
<p>Choose a sequence of actions (a schedule) to explore, and</p>
</li>
<li>
<p>Find variable assignments (states) that produce a feasible trace for the
chosen schedule.</p>
</li>
</ol>
<p>When a schedule is fixed, the SMT solver must solve far fewer constraints
because it mainly propagates values through the actions. If the solver must also
pick a schedule, it must backtrack along two axes: (1) schedules and (2) states.
This increases solving times in practice.</p>
<p>To mitigate this, Apalache lets you randomly sample schedules and execute them
symbolically. To enumerate different &quot;interesting&quot; schedules, the user can
define a view operator, which usually projects state variables to more abstract
values. The model checker will then produce traces projected onto those views.
This works significantly better for test generation in practice. However, this
exploration strategy is fixed and cannot be changed without modifying Apalache
itself.</p>
<h3 id="2.2.-trace-validation-in-one-picture" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#2.2.-trace-validation-in-one-picture">2.2. Trace validation in one picture</a></h3>
<p>Trace validation is conceptually simpler than model-based testing. We simply
execute the system under test (SUT) and collect traces. These traces are then
mapped to the abstract states, if necessary, and checked against the
specification.</p>
<picture>
  <img class="responsive-img"
    src="https://protocols-made-fun.com/img/tv.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" alt="Trace validation">
</picture>
<p>This approach has been tried in multiple projects that use the exhaustive-state
model checker <a href="https://github.com/tlaplus/tlaplus?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">TLC</a> as the back-end solver. See <a href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#10-prior-work">the prior work
section</a>.</p>
<p>Trace validation also has its challenges:</p>
<ol>
<li>
<p>We need a good test suite, in order to produce &quot;interesting&quot; traces.
However, test cases are usually written for the happy-path scenarios. Hence,
it is easy to miss handling of error cases and faults. <a href="https://www.youtube.com/watch?v=DO8MvouV29M&amp;utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Srinidhi Nagendra et
al. (2025)</a> address this issue by fuzzing the tests.</p>
</li>
<li>
<p>Someone has to instrument the SUT to trace the relevant events. In some
cases, it easy to do, e.g., by tracing message exchanges, as presented by
<a href="https://www.youtube.com/watch?v=NZmON-XmrkI&amp;utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Markus Kuppe et. al. (2024)</a>. In other cases, it may be quite difficult
to do, e.g., when we want to dump the internal states of the SUT. In a
concurrent system this may require a global lock and traversing large data
structures. In a distributed system, this may further require a distributed
snapshot or using vector clocks.</p>
</li>
<li>
<p>We have to run the whole system to collect traces. It is hard to isolate one
component, e.g., one network node.</p>
</li>
</ol>
<h2 id="3.-interactive-symbolic-testing-with-smt" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#3.-interactive-symbolic-testing-with-smt">3. Interactive symbolic testing with SMT</a></h2>
<p>As we can see, both model-based testing and trace validation in their above
formulation are non-interactive. They both require a complete trace to be
produced first, and <strong>there is no feedback loop between the specification and
the implementation</strong>.</p>
<p>There is a third way to do conformance testing that leverages SMT solvers, yet
receives feedback from the implementation during the testing. I will call it
<strong>interactive symbolic testing</strong>. I think the first time I heard about this
approach was from <a href="https://www.losa.fr/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Giuliano Losa</a>, when he explained the paper by <a href="https://www.mcmil.net/pubs/SIGCOMM19.pdf?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Ken
McMillan and Leonore Zuck (2019)</a> to me. If you have not read this paper
yet, I highly recommend doing so. On the naming side, McMillan and Zuck call
their approach &quot;specification-based testing&quot;. I find this name to be a bit
non-descriptive, as MBT is also specification-based.</p>
<p>The idea is to generate an action with the SMT solver by following the
specification, execute it against the implementation, and then feed the results
back to the SMT solver to generate the next action. This way, we can
systematically explore the protocol specification while getting feedback from
the implementation.</p>
<p>The picture below illustrates this approach, by approximately following the
internal transition executor of Apalache.</p>
<picture>
  <img class="responsive-img"
    src="https://protocols-made-fun.com/img/symbolic-testing.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" alt="Symbolic testing">
</picture>
<p>To implement this approach to testing with Apalache, we would have to find a way
for Apalache and the test harness to communicate. My experience with development
of Apalache shows that <strong>fixing exploration strategies inside the model checker
is not a good idea</strong>. People always want to tweak them a bit for their purposes.
Given this observation, <a href="https://blltprf.xyz/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Thomas Pani</a> and I have decided to implement a simple
server API for Apalache that would allow external tools to drive the symbolic
execution of TLA<sup>+</sup> specifications.</p>
<h2 id="4.-the-new-json-rpc-api-of-apalache" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#4.-the-new-json-rpc-api-of-apalache">4. The new JSON-RPC API of Apalache</a></h2>
<p><a href="https://blltprf.xyz/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Thomas</a> and I wanted to have a lightweight API that we could use
from any programming language without writing too much boilerplate code. At this
point, every engineer would whisper: hey, you need gRPC, I've got some. Well, we
tried gRPC in the integration of <a href="https://apalache-mc.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache</a> with <a href="https://github.com/informalsystems/quint?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Quint</a>. It is hard to call
gRPC lightweight.</p>
<p>So we have decided to go with <a href="https://www.jsonrpc.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">JSON-RPC</a> this time, which is a very simple
protocol that works over HTTP/HTTPS. Implementing a JSON-RPC server is quite
straightforward.  Since Apalache is written in Scala, which is JVM-compatible,
we can use the well-known and battle-tested libraries. Perhaps, a bit
unexpectedly for a Scala project, I've decided to implement this server with
<a href="https://jetty.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Jetty</a> for serving the HTTP requests and <a href="https://github.com/FasterXML/jackson?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Jackson</a> for JSON serialization.
(The reason is that we have already burnt ourselves with fancy but poorly
supported libraries in Scala.) The resulting server is lightweight and fast.
Moreover, it can be tested with command-line tools like <a href="https://curl.se/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">curl</a>.</p>
<p>The state-chart diagram of the Apalache JSON-RPC server for a single session is
shown below.</p>
<picture>
  <img class="responsive-img"
    src="https://protocols-made-fun.com/img/apalache-api.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" alt="Apalache JSON-RPC API">
</picture>
<p>To see a detailed description of this API, check <a href="https://github.com/apalache-mc/apalache/tree/main/json-rpc?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache JSON-RPC</a>.  Just to
give you the taste of it, here is how you start the server without having
anything installed but Docker:</p>
<pre><code class="hljs language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">docker pull ghcr.io/apalache-mc/apalache</span>
<span class="hljs-meta prompt_">$ </span><span class="language-bash">docker run --<span class="hljs-built_in">rm</span> /tmp:/var/apalache -p 8822:8822 \
    ghcr.io/apalache-mc/apalache:latest \
    server --server-type=explorer</span>
</code></pre>
<p>Now, we create a new Apalache session with a TLA<sup>+</sup> specification (in a
separate tab):</p>
<pre><code class="hljs language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">SPEC=`<span class="hljs-built_in">cat</span> &lt;&lt;<span class="hljs-string">EOF | base64</span></span>
---- MODULE Inc ----
EXTENDS Integers
VARIABLE
  \* @type: Int;
  x
Init == I:: x = 0
Next == (A:: (x &lt; 3 /\\ x&#x27; = x + 1)) \\/ (B:: (x &gt; -3 /\\ x&#x27; = x - 1))
Inv3 == Inv:: x /= 0
\* @type: () =&gt; &lt;&lt;Bool, Bool, Bool&gt;&gt;;
View == &lt;&lt;x &lt; 0, x = 0, x &gt; 0&gt;&gt;
=====================
EOF`
<span class="hljs-meta prompt_">$ </span><span class="language-bash"><span class="hljs-string">curl -X POST http://localhost:8822/rpc \
  -H &quot;Content-Type: application/json&quot; \
  -d &#x27;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;method&quot;:&quot;loadSpec&quot;,&quot;params&quot;:{&quot;sources&quot;: [ &quot;&#x27;${SPEC}&#x27;&quot; ],</span></span>
       &quot;invariants&quot;: [&quot;Inv3&quot;], &quot;exports&quot;: [&quot;View&quot;]},&quot;id&quot;:1}&#x27;
</code></pre>
<p>Is not that amazing? No protobuf, no code generation, just pure shell and
readable JSON.</p>
<p>Having the specification loaded, we load the predicate <code>Init</code> into the solver
context, which is encoded as transition 0:</p>
<pre><code class="hljs language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">curl -X POST http://localhost:8822/rpc \
  -H <span class="hljs-string">&quot;Content-Type: application/json&quot;</span> \
  -d <span class="hljs-string">&#x27;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;method&quot;:&quot;assumeTransition&quot;,&quot;params&quot;:{&quot;sessionId&quot;:&quot;1&quot;,</span></span>
       &quot;transitionId&quot;:0,&quot;checkEnabled&quot;:true},&quot;id&quot;:2}&#x27;
</code></pre>
<p>Assuming that the previous call returned <code>ENABLED</code>, we switch to the next
step, which applies the effect of <code>Init</code> to the current symbolic state:</p>
<pre><code class="hljs language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">curl -X POST http://localhost:8822/rpc \
  -H <span class="hljs-string">&quot;Content-Type: application/json&quot;</span> \
  -d <span class="hljs-string">&#x27;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;method&quot;:&quot;nextStep&quot;,&quot;params&quot;:{&quot;sessionId&quot;:&quot;1&quot;},&quot;id&quot;:3}&#x27;</span></span>
</code></pre>
<p>Now, we can check the invariant <code>Inv3</code> against all states that satisfy <code>Init</code>:</p>
<pre><code class="hljs language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">curl -X POST http://localhost:8822/rpc \
  -H <span class="hljs-string">&quot;Content-Type: application/json&quot;</span> \
  -d <span class="hljs-string">&#x27;{&quot;jsonrpc&quot;:&quot;2.0&quot;,&quot;method&quot;:&quot;checkInvariant&quot;,</span></span>
       &quot;params&quot;:{&quot;sessionId&quot;:&quot;1&quot;,&quot;invariantId&quot;:0},&quot;id&quot;:3}&#x27;
</code></pre>
<p>Since invariant <code>Inv3</code> is violated by the initial state, the server returns
<code>VIOLATED</code>, along with a counter-example trace:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;jsonrpc&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2.0&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;id&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">3</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;result&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;sessionId&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;invariantStatus&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;VIOLATED&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;trace&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;#meta&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;format&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ITF&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;varTypes&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">&quot;x&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Int&quot;</span> <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;format-description&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://apalache-mc.org/docs/adr/015adr-trace.html&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;description&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Created by Apalache on Thu Dec 11 16:56:47 CET 2025&quot;</span>
      <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;vars&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span> <span class="hljs-string">&quot;x&quot;</span> <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;states&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span> <span class="hljs-punctuation">{</span>
          <span class="hljs-attr">&quot;#meta&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">&quot;index&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">0</span> <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
          <span class="hljs-attr">&quot;x&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">&quot;#bigint&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;0&quot;</span> <span class="hljs-punctuation">}</span>
      <span class="hljs-punctuation">}</span> <span class="hljs-punctuation">]</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<p>The trace is encoded in the <a href="https://apalache-mc.org/docs/adr/015adr-trace.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">ITF format</a>, which is a simple JSON-based
format for TLA<sup>+</sup> and Quint traces.</p>
<p>Had the invariant been violated on a deeper trace, we would have to assume more
transitions by calling <code>assumeTransition</code> and <code>nextStep</code> multiple times.</p>
<p>If you want to access this API from Python right away, use two helper libraries:</p>
<ul>
<li>
<p><a href="https://github.com/konnov/apalache-rpc-client/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">apalache-rpc-client</a> for interacting with the JSON-RPC server of
Apalache, and</p>
</li>
<li>
<p><a href="https://github.com/konnov/itf-py/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">itf-py</a> for serializing and deserializing ITF traces.</p>
</li>
</ul>
<h2 id="5.-case-study%3A-tftp-protocol" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#5.-case-study%3A-tftp-protocol">5. Case study: TFTP protocol</a></h2>
<p>To experiment with interactive symbolic testing and the new JSON-RPC API, I
wanted to choose a relatively simple network protocol that had multiple
implementations. After several sessions with ChatGPT, I ended up with the
Trivial File Transfer Protocol (TFTP) as a reasonable target for this small
project.</p>
<p>The Wikipedia page on <a href="https://en.wikipedia.org/wiki/Trivial_File_Transfer_Protocol?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">TFTP</a> gives us a good overview of the protocol. In
short, TFTP is a simple protocol to transfer files over UDP. It supports reading
and writing files from a remote server. It is mostly used for booting from the
network. The protocol is simple enough to be specified in TLA<sup>+</sup>
without too much effort, yet it has enough complexity to make the testing effort
interesting. Actually, I've only specified reading requests (RRQ) and no writing
requests (WRQ) to keep the scope manageable.</p>
<p>You can find more detailed specifications in the original <a href="https://www.rfc-editor.org/rfc/rfc1350?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">RFC 1350</a>, as well
as in its extensions <a href="https://www.rfc-editor.org/rfc/rfc2347?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">RFC 2347</a>, <a href="https://www.rfc-editor.org/rfc/rfc2348?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">RFC 2348</a>, and <a href="https://www.rfc-editor.org/rfc/rfc2349?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">RFC 2349</a>. RFC 1350
defines a simple non-negotated version of the protocol. Below is an example of
such an interaction between the client and the server. Notice that the client
first sends a read request (RRQ) to the server on the control port 69, which
responds with the first data block (DATA) on a newly allocated ephemeral port.
The client acknowledges (ACK) the received data block on the same ephemeral
port.  This continues until the server sends the last data block, which is
smaller than the maximum block size (512 bytes by default).</p>
<picture>
  <img class="responsive-img"
    src="https://protocols-made-fun.com/img/rrq1350.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
    alt="Read request and transfer as per RFC 1350">
</picture>
<p>Further, <a href="https://www.rfc-editor.org/rfc/rfc2347?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">RFC 2347</a> defines an option negotiation phase that happens right after the
read request. The client and the server may negotiate options like block size,
timeout, and transfer size. <a href="https://www.rfc-editor.org/rfc/rfc2348?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">RFC 2348</a> defines the block size option, while
<a href="https://www.rfc-editor.org/rfc/rfc2349?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">RFC 2349</a> defines the transfer size option. Below is an example interaction with
option negotiation:</p>
<picture>
  <img class="responsive-img"
    src="https://protocols-made-fun.com/img/rrq2347.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
    alt="Read request and transfer as per RFC 2347">
</picture>
<p>The cool thing about TFTP is that it has multiple open-source implementations of
TFTP clients and servers in different programming languages. Here are some of
them:</p>
<ul>
<li>
<p><a href="https://kernel.googlesource.com/pub/scm/network/tftp/tftp-hpa/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">tftp-hpa</a> is the canonical implementation of TFTP for Linux (and UNIX?) in C.</p>
</li>
<li>
<p><a href="https://github.com/madmartin/atftp?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">atftpd</a> is advanced TFTP, which is intended for fast boot in large
clusters, also in C.</p>
</li>
<li>
<p><a href="http://www.thekelleys.org.uk/dnsmasq/doc.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">dnsmasq</a> is a lightweight DNS and DHCP server that also includes a TFTP
server, in C.</p>
</li>
<li>
<p><a href="https://github.com/altugbakan/rs-tftpd?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">rs-tftpd</a> (Rust) is an implementation of a TFTP server in Rust.</p>
</li>
<li>
<p><a href="https://github.com/pin/tftp?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">gotfpd</a> (Go) is an implementation of a TFTP server in Go.</p>
</li>
<li>
<p>busybox also has its minimalistic implementation for file reads.</p>
</li>
</ul>
<h2 id="6.-initial-tla%2B-specification-of-tftp" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#6.-initial-tla%2B-specification-of-tftp">6. Initial TLA<sup>+</sup> specification of TFTP</a></h2>
<p>In the first stage of this experiment, I read the RFCs and wrote a
TLA<sup>+</sup> specification of the TFTP protocol. At that stage, I did not
introduce packet loss, duplication, or reordering. I just wanted to have a
simple working specification that I could use for testing the implementations.
<strong>This stage took me just two days.</strong> Well, I have been writing plenty of
TLA<sup>+</sup> specifications in the past.</p>
<p>You can check this initial specification in the <a href="https://github.com/konnov/tftp-symbolic-testing/tree/6fb00d1878b7e37a629868ac25b853d95b16cbdc?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">initial commit</a> of the
<a href="https://github.com/konnov/tftp-symbolic-testing?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">testing repo</a>. The main body of the specification lives in <code>tftp.tla</code>,
which imports several several auxiliary modules:</p>
<ul>
<li>
<p><code>typedefs.tla</code> defines the types of the data structures and the basic
constructors for these data structures. Since I am using Apalache, the
specification needs type definitions. Luckily, these days, I just write the
type definitions in comments and let Claude generate the auxilliary operators
such as constructors and accessors. If you already have an untyped
specification, Claude is good at figuring out the types in the agent mode. Just
use <a href="https://github.com/apalache-mc/apalache/blob/main/prompts/type-annotation-assistant.md?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">this prompt</a>.</p>
</li>
<li>
<p><code>util.tla</code> defines common utility definitions such as <code>Min</code>, <code>Max</code>, and
option conversions.</p>
</li>
</ul>
<p>Finally, <code>MC2_tftp.tla</code> defines a protocol instance of two clients and one
server. If you stumble upon the definitions that end with <code>View</code> there, ignore
them. They are not essential for this blog post. I used them to experiment with
more advanced symbolic exploration scripts.</p>
<p>If you are not familiar with TLA<sup>+</sup>, or your TLA<sup>+</sup> skills are
rusty, I recommend giving one of the definitions and this prompt to ChatGPT. It
actually explains TLA<sup>+</sup> quite well:</p>
<pre><code class="hljs">Assume that I am a software engineer. I don't know TLA+ but know Golang or Rust.
Explain me this TLA+ snippet using my knowledge: ...
</code></pre>
<p>To see the kinds of actions this initial specification had, have a look at the
definition of <code>Next</code> in <code>tftp.tla</code>:</p>
<pre><code class="hljs language-tlaplus"><span class="hljs-title function_">Next</span> <span class="hljs-operator">==</span>
    <span class="hljs-comment">\* the actions by the clients</span>
    <span class="hljs-operator">\/</span>  <span class="hljs-keyword">\E</span> srcIp <span class="hljs-keyword">\in</span> CLIENT_IPS, srcPort <span class="hljs-keyword">\in</span> PORTS:
            <span class="hljs-keyword">\E</span> filename <span class="hljs-keyword">\in</span> <span class="hljs-keyword">DOMAIN</span> FILES, timeout <span class="hljs-keyword">\in</span> <span class="hljs-number">1</span><span class="hljs-operator">..</span><span class="hljs-number">255</span>:
                <span class="hljs-comment">\* &quot;man tftpd&quot;: 65464 is the theoretical maximum for block size</span>
                <span class="hljs-comment">\* https://linux.die.net/man/8/tftpd</span>
                <span class="hljs-keyword">\E</span> tsize <span class="hljs-keyword">\in</span> <span class="hljs-number">0</span><span class="hljs-operator">..</span>FILES[filename], blksize <span class="hljs-keyword">\in</span> <span class="hljs-number">0</span><span class="hljs-operator">..</span><span class="hljs-number">65464</span>:
                    <span class="hljs-comment">\* choose a subset of the options to request</span>
                    <span class="hljs-keyword">\E</span> optionKeys <span class="hljs-keyword">\in</span> <span class="hljs-keyword">SUBSET</span> OPTIONS_RFC2349:
                        <span class="hljs-keyword">LET</span> options <span class="hljs-operator">==</span>
                            mk_options(optionKeys, blksize, tsize, timeout)
                        <span class="hljs-keyword">IN</span>
                        ClientSendRRQ(srcIp, srcPort, filename, options)
    <span class="hljs-operator">\/</span>  <span class="hljs-keyword">\E</span> udp <span class="hljs-keyword">\in</span> packets:
            <span class="hljs-operator">\/</span> ClientRecvDATA(udp)
            <span class="hljs-operator">\/</span> ClientRecvOACK(udp)
            <span class="hljs-operator">\/</span> ClientRecvErrorAndCloseConn(udp)
    <span class="hljs-operator">\/</span>  <span class="hljs-keyword">\E</span> ipPort <span class="hljs-keyword">\in</span> <span class="hljs-keyword">DOMAIN</span> clientTransfers:
            ClientTimeout(ipPort)
    <span class="hljs-comment">\* the server</span>
    <span class="hljs-operator">\/</span>  <span class="hljs-keyword">\E</span> udp <span class="hljs-keyword">\in</span> packets:
            <span class="hljs-operator">\/</span> ServerRecvRRQ(udp)
            <span class="hljs-operator">\/</span> ServerSendDATA(udp)
            <span class="hljs-operator">\/</span> ServerRecvAckAndCloseConn(udp)
            <span class="hljs-operator">\/</span> ServerRecvErrorAndCloseConn(udp)
    <span class="hljs-operator">\/</span>  <span class="hljs-keyword">\E</span> ipPort <span class="hljs-keyword">\in</span> <span class="hljs-keyword">DOMAIN</span> serverTransfers:
            ServerTimeout(ipPort)
    <span class="hljs-comment">\* handle the clock and timeouts</span>
    <span class="hljs-operator">\/</span>  <span class="hljs-keyword">\E</span> delta <span class="hljs-keyword">\in</span> <span class="hljs-number">1</span><span class="hljs-operator">..</span><span class="hljs-number">255</span>:
            AdvanceClock(delta)
</code></pre>
<div class="tip-box">
  <div class="tip-header"><strong>Tip:</strong></div>
  <div class="tip"><p>Do not spend too much time on reading this
initial specification. I misunderstood several thigs about TFTP from the
RFCs, which I fixed later. Especially, the timeouts are completely wrong
in this initial version. Good that the actual implementations helped me to
find these mistakes!</p>
</div>
</div>
<p><strong>Falsy invariants</strong>. As I always do, I also specified &quot;falsy invariants&quot; to
produce interesting examples. For example, using the invariant
<code>RecvThreeDataBlocksEx</code> below, I can easily produce a trace where a client
receives three data blocks from the server.</p>
<pre><code class="hljs language-tla"><span class="hljs-comment">\* Check this falsy invariant to see an example of a client receiving 3 blocks.</span>
<span class="hljs-title function_">RecvThreeDataBlocksEx</span> <span class="hljs-operator">==</span>
    <span class="hljs-operator">~</span>(<span class="hljs-keyword">\E</span> p <span class="hljs-keyword">\in</span> <span class="hljs-keyword">DOMAIN</span> clientTransfers:
        <span class="hljs-keyword">Len</span>(clientTransfers[p].blocks) <span class="hljs-operator">&gt;=</span> <span class="hljs-number">3</span>)
</code></pre>
<p>If you want to try it right way without installing anything, just do this with
docker:</p>
<pre><code class="hljs language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">git <span class="hljs-built_in">clone</span> git@github.com:konnov/tftp-symbolic-testing.git</span>
<span class="hljs-meta prompt_">$ </span><span class="language-bash">git checkout 6fb00d1878b7e37a629868ac25b853d95b16cbdc</span>
<span class="hljs-meta prompt_">$ </span><span class="language-bash">docker pull ghcr.io/apalache-mc/apalache</span>
<span class="hljs-meta prompt_">$ </span><span class="language-bash">docker run --<span class="hljs-built_in">rm</span> -v `<span class="hljs-built_in">pwd</span>`:/var/apalache ghcr.io/apalache-mc/apalache \
  check --inv=RecvThreeDataBlocksEx MC2_tftp.tla</span>
</code></pre>
<p><strong>Trace visualization.</strong>
Since Apalache emits traces in the <a href="https://apalache-mc.org/docs/adr/015adr-trace.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">ITF format</a>, which has a very simple
schema in JSON, it was easy for me to convince Claude to produce a Python script
that would convert ITF traces to human-readable state charts in Mermaid. Here is
just an example of such a trace produced by Apalache when checking the invariant
<code>RecvThreeDataBlocksEx</code> in Mermaid:</p>
<pre><code class="hljs">sequenceDiagram
    participant ip10_0_0_3_port65000 as 10.0.0.3:65000
    participant ip10_0_0_1_port10000 as 10.0.0.1:10000
    participant ip10_0_0_1_port69 as 10.0.0.1:69

    ip10_0_0_3_port65000-&gt;&gt;ip10_0_0_1_port69: RRQ(file1, blksize=0, timeout=4)
    ip10_0_0_1_port10000-&gt;&gt;ip10_0_0_3_port65000: DATA(blk=1, 512B)
    ip10_0_0_3_port65000-&gt;&gt;ip10_0_0_1_port10000: ACK(blk=1)
    ip10_0_0_1_port10000-&gt;&gt;ip10_0_0_3_port65000: DATA(blk=2, 512B)
    ip10_0_0_3_port65000-&gt;&gt;ip10_0_0_1_port10000: ACK(blk=2)
    ip10_0_0_1_port10000-&gt;&gt;ip10_0_0_3_port65000: DATA(blk=3, 0B)
    ip10_0_0_3_port65000-&gt;&gt;ip10_0_0_1_port10000: ACK(blk=3)
</code></pre>
<p>This is how it looks like when rendered by <a href="https://www.mermaidchart.com/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Mermaid</a>:</p>
<picture>
  <img class="responsive-img"
    src="https://protocols-made-fun.com/img/tftp3.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
    alt="Visualized trace of TFTP client receiving three data blocks">
</picture>
<p><strong>Note on abstractions.</strong> Similar to the <a href="https://www.mcmil.net/pubs/SIGCOMM19.pdf?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">McMillan and Zuck</a>, I tried
to avoid unnecessary abstractions and approximations in the specification.  If
you look at the type definition of a TFTP packet in
<a href="https://github.com/konnov/tftp-symbolic-testing/blob/6fb00d1878b7e37a629868ac25b853d95b16cbdc/spec/typedefs.tla?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"><code>typedefs.tla</code></a>,
you will see that all fields except <code>data</code> are modeled as strings and integers:</p>
<pre><code class="hljs language-tlaplus">  <span class="hljs-operator">/</span><span class="hljs-operator">/</span> TFTP Packet Types
  @typeAlias: tftpPacket <span class="hljs-operator">=</span>
    <span class="hljs-operator">/</span><span class="hljs-operator">/</span> Read Request (RFC <span class="hljs-number">1350</span>, Figure <span class="hljs-number">5</span><span class="hljs-operator">-</span><span class="hljs-number">1</span>, RFC <span class="hljs-number">2347</span>).
    <span class="hljs-operator">/</span><span class="hljs-operator">/</span> See RFCs <span class="hljs-number">2348</span><span class="hljs-operator">-</span><span class="hljs-number">2349</span> for the options.
      RRQ({ opcode: Int, filename: Str, mode: Str, options: Str <span class="hljs-operator">-</span>&gt; Int })
    <span class="hljs-operator">/</span><span class="hljs-operator">/</span> Write Request (RFC <span class="hljs-number">1350</span>, Figure <span class="hljs-number">5</span><span class="hljs-operator">-</span><span class="hljs-number">1</span>, RFC <span class="hljs-number">2347</span>).
    <span class="hljs-operator">/</span><span class="hljs-operator">/</span> See RFCs <span class="hljs-number">2348</span><span class="hljs-operator">-</span><span class="hljs-number">2349</span> for the options.
    | WRQ({ opcode: Int, filename: Str, mode: Str, options: Str <span class="hljs-operator">-</span>&gt; Int })
    <span class="hljs-operator">/</span><span class="hljs-operator">/</span> Acknowledgment (RFC <span class="hljs-number">1350</span>, Figure <span class="hljs-number">5</span><span class="hljs-operator">-</span><span class="hljs-number">3</span>)
    | ACK({ opcode: Int, blockNum: Int })
    <span class="hljs-operator">/</span><span class="hljs-operator">/</span> Option Acknowledgment (RFC <span class="hljs-number">2347</span>)
    | OACK({ opcode: Int, options: Str <span class="hljs-operator">-</span>&gt; Int })
    <span class="hljs-operator">/</span><span class="hljs-operator">/</span> Data packet (RFC <span class="hljs-number">1350</span>, Figure <span class="hljs-number">5</span><span class="hljs-operator">-</span><span class="hljs-number">2</span>)
    <span class="hljs-operator">/</span><span class="hljs-operator">/</span> In our specification, we simply pass the length of data instead of the
    <span class="hljs-operator">/</span><span class="hljs-operator">/</span> data itself. The test harness should pass the actual data.
    | DATA({ opcode: Int, blockNum: Int, data: Int })
    <span class="hljs-operator">/</span><span class="hljs-operator">/</span> Error packet (RFC <span class="hljs-number">1350</span>, Figure <span class="hljs-number">5</span><span class="hljs-operator">-</span><span class="hljs-number">4</span>)
    | ERROR({ opcode: Int, errorCode: Int, msg: Str })
  ;
</code></pre>
<p>Thinking about it now, I could even model <code>data</code> as a sequence of bytes, but it
was obvious to me that only the length of <code>data</code> matters for the protocol logic.</p>
<h2 id="7.-bootstrapping-the-testing-harness-with-claude" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#7.-bootstrapping-the-testing-harness-with-claude">7. Bootstrapping the testing harness with Claude</a></h2>
<p>Now, we have the initial TLA<sup>+</sup> specification of TFTP and the standard
implementation <a href="https://kernel.googlesource.com/pub/scm/network/tftp/tftp-hpa/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">tftp-hpa</a>, which is the default <code>tftpd</code> server on Linux.</p>
<p>I wanted to avoid running the TFTP server on my laptop. What if I accidentally
find a bug that corrupts my file system? So I have decided to run the server and
the client harnesses in Docker containers. This way, I could easily reset the
SUT and have an isolated network for the TFTP server and clients.</p>
<p>Below is the architecture of the test harness that I had in mind. It's quite a
bit overengineered for testing TFTP. I also wanted to experiment with Docker
networking and managing multiple containers for potential future projects.</p>
<pre><code class="hljs">┌──────────────────────────────────────────────────────────────────┐
│                        Host Machine                              │
│                                                                  │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │ harness.py                                                 │  │
│  │  - Coordinates symbolic execution                          │  │
│  │  - Manages Apalache server                                 │  │
│  │  - Controls Docker containers                              │  │
│  │  - Generates and saves test runs                           │  │
│  └────────┬────────────────────────┬──────────────────────────┘  │
│           │                        │                             │
│           ▼                        ▼                             │
│  ┌─────────────────┐     ┌──────────────────────────┐            │
│  │ Apalache Server │     │  Docker Manager          │            │
│  │  (port 8822)    │     │  - Network: 172.20.0.0/24│            │
│  └─────────────────┘     └──────────┬───────────────┘            │
│                                     │                            │
└─────────────────────────────────────┼────────────────────────────┘
                                      │
                         ┌────────────┴──────────────┐
                         │   Docker Network          │
                         │   (172.20.0.0/24)         │
                         │                           │
         ┌───────────────┼───────────────────────────┼─────────────┐
         │               │                           │             │
         ▼               ▼                           ▼             │
  ┌─────────────┐ ┌─────────────┐          ┌─────────────┐         │
  │ TFTP Server │ │  Client 1   │          │  Client 2   │         │
  │ 172.20.0.10 │ │ 172.20.0.11 │          │ 172.20.0.12 │         │
  │             │ │             │          │             │         │
  │ tftp-hpa    │ │ Python      │          │ Python      │         │
  │ Port: 69    │ │ TCP: 15001  │          │ TCP: 15002  │         │
  │ Data:1024-27│ │ (control)   │          │ (control)   │         │
  └─────────────┘ └─────────────┘          └─────────────┘         │
         ▲               │                           │             │
         │               │    UDP TFTP packets       │             │
         └───────────────┴───────────────────────────┘             │
                                                                   │
                         Docker Containers                         │
                                                                   │
                         tftp-test-harness:latest                  │
                                                                   │
└──────────────────────────────────────────────────────────────────┘
</code></pre>
<p><strong>LLMs will do the work?</strong> As you could have guessed, I had no interest in
writing the Docker files and the test harness from scratch. Having heard from so
many people that LLMs are so amazing, I have decided to give Claude a try at
generating the test harness.</p>
<p>Hence, I spent about four hours writing a very detailed prompt for Claude that
explained how I want the test harness to look like (the above architecture
diagram is actually generated by Claude from my prompt).</p>
<p><strong>Pushing the button!</strong> So I've run Claude in the agent mode with my prompt and
went for a coffee break. You can see the first generated version in <a href="https://github.com/konnov/tftp-symbolic-testing/commit/063da7d2b79c07dfb64225da852440c98b76c41e3?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">this
commit</a>.
The result looked so exciting and amazing until I looked at <code>CHECKLIST.md</code>:</p>
<pre><code class="hljs">## Notes

- The framework is complete and production-ready
- Remaining work is mostly about connecting components
- Each task is independent and can be tackled separately
- Estimated effort: 4-8 hours for core integration (tasks 1-4)
- Additional 2-4 hours for polish and testing (tasks 5-8)
</code></pre>
<p>What is going on? Claude left me homework? I was also baffled by the hourly
estimates: Are these Claude hours or my hours? In the hindsight, the estimate
was surprisingly accurate. It took me about 1.5 days to make this code do the
first test run that made the harness exchange UDP packets with the TFTP server.</p>
<p>Then I looked at <code>harness.py</code>, which was supposed to be &quot;complete and
production-ready&quot;. Guess what? The main loop was left as a TODO!</p>
<pre><code class="hljs language-python">        <span class="hljs-comment"># <span class="hljs-doctag">TODO:</span> Implement actual TFTP operation execution</span>
        <span class="hljs-comment"># This would involve:</span>
        <span class="hljs-comment"># - Querying Apalache for the transition details</span>
        <span class="hljs-comment"># - Sending commands to the TFTP client in Docker</span>
        <span class="hljs-comment"># - Collecting UDP packet responses</span>
        <span class="hljs-comment"># - Parsing the responses</span>
</code></pre>
<p>The overall structure was there, but the most important pieces were left as
TODOs. Fine. It did the tedious part at least. So I started to chat with Claude
again to implement the missing pieces. If you look at the commit history, you
will see plenty of spaghetti code generated by Claude. In the end, it became a
bit better after my guidance, but I had to rewrite it at some point.</p>
<p>Even though I am making jokes about LLMs here, I must say that Claude really
helped me to debug the Docker setup and produce the python code for
communicating over UDP in modern Python. I could easily lose a couple of days
there.</p>
<p>Of course, the exploration logic was totally broken. After all, there is not
much for LLMs to learn from. We are doing something new here!</p>
<p><strong>1.5 days later.</strong> Something was working, but even the happy path was not
there. So I had to do the baby steps with Claude. Here are just a few examples
from my Copilot chat:</p>
<blockquote>
<p>Let's implement sending the RRQ packet over the wire.</p>
</blockquote>
<blockquote>
<p>...</p>
</blockquote>
<blockquote>
<p>Now I am receiving a response like below. This is good! What I want you to
do next. Decode the response and construct the expected packet for the TLA+
specification. Save this as the expectation that we will use in the next step.</p>
</blockquote>
<blockquote>
<p>...</p>
</blockquote>
<blockquote>
<p>You should not construct a TLA+ expression. Rather, convert the packet to an
ITF value using itf-py.</p>
</blockquote>
<blockquote>
<p>...</p>
</blockquote>
<blockquote>
<p>Can you implement this case for receiving OACK from the server and sending ACK
by the client to the server.</p>
</blockquote>
<p><strong>2 more days later.</strong> I had the happy path working. At this point, I was tired
of reading the harness logs. So I needed some form of visualization for each
run. Obviously, I wanted to have the same kind of Mermaid diagrams as before.</p>
<p>So I asked Claude to generate a script that would reconstruct the sequence
diagram from the harness logs. Well, it took me longer than expected. At some
point, Claude was producing quite convoluted log parsers with regular
expressions and python loops. Of course, it needs a human to define a simple log
format instead.</p>
<p>Below is an example of such a test run, visualized from the log by the generated
script in Mermaid:</p>
<picture>
  <img class="responsive-img"
    src="https://protocols-made-fun.com/img/tftp-happy.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
    alt="Trace visualization of the testing run">
</picture>
<p>If you look at the above diagram carefully, you will notice that server responses
come in two flavors:</p>
<ol>
<li>
<p>The dashed arrows indicate that the client has received the UDP packet from
the UDP socket.</p>
</li>
<li>
<p>The solid arrows indicate that the UDP packet was successfully replayed
with the TLA<sup>+</sup> specification.</p>
</li>
</ol>
<h2 id="8.-debugging-the-tla%2B-specification-with-the-implementation" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#8.-debugging-the-tla%2B-specification-with-the-implementation">8. Debugging the TLA<sup>+</sup> specification with the implementation</a></h2>
<p>At that point, the tests started to produce actual interactions between the
TLA<sup>+</sup> specification (as solved by Apalache and Z3) and the real TFTP
server. This brought a lot of surprises! I am going to present some of them
below.</p>
<p>In this debugging session, I am keeping the scorecard of how many times the
TLA<sup>+</sup> specification was wrong versus how many times the implementation
(tftp-hpa) was wrong. The scorecard at this point looks like this:</p>
<table>
<thead>
<tr>
<th>Spec bugs</th>
<th>Implementation bugs</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>0</td>
</tr>
</tbody>
</table>
<p>Actually, <a href="https://kernel.googlesource.com/pub/scm/network/tftp/tftp-hpa/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">tftp-hpa</a> is a quite mature implementation, so I was not expecting
any bugs there. Keep reading to see what I found.</p>
<h3 id="8.1.-sending-errors-on-read-request" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#8.1.-sending-errors-on-read-request">8.1. Sending errors on read request</a></h3>
<p>The first surprise came from my misunderstanding of how exactly TFTP is supposed
to reply to a malformed read request (<code>RRQ</code>). Since a client sends <code>RRQ</code> to the
control port 69 of the server, I thought that the server would reply with an
error packet (<code>ERROR</code>) from the port 69, instead of introducing a new ephemeral
port.</p>
<p>This is what <a href="https://www.rfc-editor.org/rfc/rfc2347?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">RFC 2347</a> says about option negotiation:</p>
<blockquote>
<p>...the server should simply omit the option from the <code>OACK</code>, respond with an
alternate value, or send an <code>ERROR</code> packet, with error code 8, to terminate the
transfer.</p>
</blockquote>
<p>No explanation about the port from which the <code>ERROR</code> packet is sent. Well, my
understanding was wrong. The server always allocates a new ephemeral port for
sending the <code>ERROR</code> packet. This kind of makes sense, as the implementation simply
forks on a new request. One score to the implementation:</p>
<table>
<thead>
<tr>
<th>Spec bugs</th>
<th>Implementation bugs</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>0</td>
</tr>
</tbody>
</table>
<div class="tip-box">
  <div class="tip-header"><strong>Tip:</strong></div>
  <div class="tip"><p>Actually, as I found later, the spec was not
always wrong, as the busybox implementation always uses port 69!</p>
</div>
</div>
<h3 id="8.2.-the-server-may-send-duplicate-packets" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#8.2.-the-server-may-send-duplicate-packets">8.2. The server may send duplicate packets</a></h3>
<p>Well, I knew that, but was lazy to write an action in the specification that
would handle duplicate packets. This is a typical shortcut when writing a
specification, since duplicate packets do not change the specification state and
considered &quot;stuttering&quot; steps. The server implementation retransmitted a <code>DATA</code>
packet, which produced a deviation in the TLA<sup>+</sup> specification. Another
score to the implementation:</p>
<table>
<thead>
<tr>
<th>Spec bugs</th>
<th>Implementation bugs</th>
</tr>
</thead>
<tbody>
<tr>
<td>2</td>
<td>0</td>
</tr>
</tbody>
</table>
<p>Formally speaking, this action does not affect protocol safety, so it is
tempting to simply skip duplicates. However, in conformance testing, we have to
handle all possible actions of the implementation, even if they produce stuttering
steps in the theory of TLA<sup>+</sup>.</p>
<h3 id="8.3.-input-output-conformance-does-not-work-with-udp!" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#8.3.-input-output-conformance-does-not-work-with-udp!">8.3. Input-output conformance does not work with UDP!</a></h3>
<p>The next issue was quite interesting. When I read the papers on input-output
conformance testing from the 1990s, there was always an assumption that the
system under test (SUT) is input-enabled. This means that the SUT can always
accept any input at any time and respond to it, possibly, with an error message.
This assumption makes sense for synchronous systems (such as vending machines?),
where the tester can wait for the SUT to be ready to accept the input.</p>
<p>However, TFTP is not like that at all. The client may send an <code>ERROR</code> packet at
any point in time, and the server does not have to reply to it! This is exactly
a deviating test run I saw produced by the harness.</p>
<p>So instead of waiting for a reply from the server on each client action, the
test harness has to optimistically send the next UDP packet and then retrieve
the UDP packets from the server (remember that they live in Docker!).</p>
<p>This is where Claude was useful again. It helped me to collect the UDP packets
on the Docker client. Before taking the next step, the harness would retrieve
the buffered UDP packets from the Docker clients and replay these packets in the
TLA<sup>+</sup> specification, in arbitrary order.</p>
<p>This makes our testing approach a bit more sensitive to the timing of extracting
the buffered UDP packets, but it worked for TFTP.</p>
<table>
<thead>
<tr>
<th>Spec bugs</th>
<th>Implementation bugs</th>
</tr>
</thead>
<tbody>
<tr>
<td>3</td>
<td>0</td>
</tr>
</tbody>
</table>
<h3 id="8.4.-the-server-recycles-an-ephemeral-port-on-error" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#8.4.-the-server-recycles-an-ephemeral-port-on-error">8.4. The server recycles an ephemeral port on ERROR</a></h3>
<p>Another interesting deviation happened when the server recycled an ephemeral
port. <a href="https://www.rfc-editor.org/rfc/rfc1350?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">RFC 1350</a> explains how the server allocates ephemeral ports:</p>
<blockquote>
<p>In order to create a connection, each end of the connection chooses a TID for
itself, to be used for the duration of that connection. The TID's chosen for a
connection should be randomly chosen, so that the probability that the same
number is chosen twice in immediate succession is very low.</p>
</blockquote>
<p>Well, in our test run, the event of low probability happened (actually, I gave
the TFTP server a small range of ports to use):</p>
<picture>
  <img class="responsive-img"
    src="https://protocols-made-fun.com/img/tftp-fix5.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
    alt="Recycling ephemeral ports on error">
</picture>
<p>Actually, this theme of reusing the same ephemeral port happened multiple times
in the following debugging iterations. It is probably the most problematic
aspect of the protocol, as there is no notion of a session in TFTP. Another
score to the implementation:</p>
<table>
<thead>
<tr>
<th>Spec bugs</th>
<th>Implementation bugs</th>
</tr>
</thead>
<tbody>
<tr>
<td>4</td>
<td>0</td>
</tr>
</tbody>
</table>
<h3 id="8.5.-the-server-recycles-an-ephemeral-port-on-success" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#8.5.-the-server-recycles-an-ephemeral-port-on-success">8.5. The server recycles an ephemeral port on success</a></h3>
<p>Guess what? A very similar thing happened on a successful file transfer as well.
Here is a pruned version of the trace that shows this behavior (the initial
sequence of <code>RRQ</code>-<code>OACK</code>-<code>DATA</code>-<code>ACK</code> is omitted for brevity):</p>
<picture>
  <img class="responsive-img"
    src="https://protocols-made-fun.com/img/tftp-fix6.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
    alt="Recycling ephemeral ports on success">
</picture>
<p>This behavior seems to be consistent with <a href="https://www.rfc-editor.org/rfc/rfc1350?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#section-6">Section 6 of RFC 1350</a>, though it
seems to be ambiguous to me. Anyway, another score to the implementation:</p>
<table>
<thead>
<tr>
<th>Spec bugs</th>
<th>Implementation bugs</th>
</tr>
</thead>
<tbody>
<tr>
<td>5</td>
<td>0</td>
</tr>
</tbody>
</table>
<h3 id="8.6.-mixing-the-protocol-versions" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#8.6.-mixing-the-protocol-versions">8.6. Mixing the protocol versions</a></h3>
<p>TFTP essentially has two versions: the original version defined in RFC 1350 and
the extended version with option negotiation defined in RFC 2347. In combination
with packet duplication, this produced a very interesting deviation. I've not
saved the full trace, but here is what happened. The server processes an RRQ
with options and sends an OACK, as per RFC 2347. After that, the TLA<sup>+</sup>
specification of the server receives an earlier RRQ without options and sends a
DATA packet in response, as per RFC 1350. This corrupts the internal state of
the server in the specification.</p>
<p>Obviously, this is caused by non-determinism in the TLA<sup>+</sup>
specification, which allows the protocol to behave according to both protocol
versions at the same time. I had to fix the specification by disallowing the
server to behave according to RFC 1350, when it receives an RRQ with options.
One score to the implementation:</p>
<table>
<thead>
<tr>
<th>Spec bugs</th>
<th>Implementation bugs</th>
</tr>
</thead>
<tbody>
<tr>
<td>6</td>
<td>0</td>
</tr>
</tbody>
</table>
<h3 id="8.7.-more-deviations-on-the-specification-side" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#8.7.-more-deviations-on-the-specification-side">8.7. More deviations on the specification side</a></h3>
<p>At some point, I got tired of collecting the precise deviations. They still can
be recovered from the commit log though. Here are some of the further deviations
on the specification side that I fixed:</p>
<ul>
<li>
<p>The client must send <code>tsize = 0</code> in RRQ.</p>
</li>
<li>
<p>The server should send default timeout if it's not specified in the options.</p>
</li>
<li>
<p>The server may send invalid (e.g., outdated) packets.</p>
</li>
<li>
<p>My understanding of TFTP timeouts was wrong. I thought that a timeout was
meant to close a transfer session. Instead, timeouts in TFTP are just
triggering packet retransmissions. The number of retries is not specified in
the RFCs. In practice, tftp-hpa seems to retry 5 times before giving up.</p>
</li>
<li>
<p>The server specification should store transfers for  triplets <code>(clientIP, clientPort, serverPort)</code> instead of pairs <code>(clientIP, clientPort)</code>.</p>
</li>
</ul>
<p>In the end, the implementation scored another 7 points, before tests started to
work.</p>
<table>
<thead>
<tr>
<th>Spec bugs</th>
<th>Implementation bugs</th>
</tr>
</thead>
<tbody>
<tr>
<td>13</td>
<td>0</td>
</tr>
</tbody>
</table>
<p>It looks like my TLA<sup>+</sup> specification was a bit sloppy, in comparison
to the mature implementation of <code>tftp-hpa</code>. I have not designed this protocol
and did not give much thought to it. Obviously, the engineers have spent much
more time thinking about its behavior. You can check the specification in
<a href="https://github.com/konnov/tftp-symbolic-testing?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">the repository</a>.</p>
<h2 id="9.-testing-against-adversarial-behavior" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#9.-testing-against-adversarial-behavior">9. Testing against adversarial behavior</a></h2>
<p>At some point I thought: My clients are too well-behaved! They never lose,
duplicate, or reorder packets. What if they start to misbehave within the
protocol boundaries? Would I be able to find bugs in the implementation? Yes, I
did. Keep reading.</p>
<p>Hence, I have added one more action that simply lets a client retransmit a
previously sent packet in <code>Next</code>:</p>
<pre><code class="hljs language-tlaplus">    <span class="hljs-operator">\/</span> <span class="hljs-keyword">\E</span> udp <span class="hljs-keyword">\in</span> packets:
        ClientSendDup(udp)
</code></pre>
<p>Below is the action <code>ClientSendDup</code>. It does not change the specification state
at all. However, it produces an action that retransmits a packet in the harness:</p>
<pre><code class="hljs language-tlaplus"><span class="hljs-comment">\* A client resends a duplicate packet that it sent in the past.</span>
<span class="hljs-comment">\* This is to test for the Sourcerer&#x27;s Apprentice syndrome.</span>
<span class="hljs-comment">\* @type: $udpPacket =&gt; Bool;</span>
ClientSendDup(_udp) <span class="hljs-operator">==</span>
    ClientSendDup::
    <span class="hljs-operator">/\</span> _udp.destIp <span class="hljs-operator">=</span> SERVER_IP
    <span class="hljs-operator">/\</span> lastAction&#x27; <span class="hljs-operator">=</span> ActionRecvSend(_udp)
    <span class="hljs-operator">/\</span> <span class="hljs-keyword">UNCHANGED</span> &lt;&lt;packets, serverTransfers, clientTransfers, clock&gt;&gt;
</code></pre>
<p>You can find the complete specification <a href="https://github.com/konnov/tftp-symbolic-testing/tree/main/spec?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">here</a>.</p>
<p><strong>Protocol deviation.</strong> It mostly worked as expected. However, a few traces were
reporting deviations. Here is one of them. It's pretty long. Look for an
explanation below.</p>
<picture>
  <img class="responsive-img"
    src="https://protocols-made-fun.com/img/tftp-malformed-ack.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"
    alt="The implementation diverging from the specification">
</picture>
<p>The last UDP packet is an acknowledgment for block 1 from the server. If
you think about the protocol, the server should never send an ACK in the
sessions associated with read requests (RRQ). ACK packets are only sent by the
clients.  Yet, this is what was happening. To double check this, I've asked
Claude to capture the traffic in pcap files in the Docker containers. Indeed,
Wireshark was showing the ACK packet from the server. Moreover, the packet was
malformed. It looked like the option acknowledgment (OACK) packet, but had the
first bytes of an ACK packet. Sounds like memory corruption!</p>
<p>Here is the core sequence of events that produced this behavior (a few details
removed):</p>
<ol>
<li>
<p>The client sends <code>RRQ(&quot;file1&quot;, blksize=NN)</code> to the server (172.20.0.10:69).</p>
</li>
<li>
<p>The server sends a few OACK packets to the client.</p>
</li>
<li>
<p>The client erroneously sends <code>ACK(1)</code> to the server, which is a duplicate
packet from an earlier transfer. It could be simply a delayed packet though.</p>
</li>
<li>
<p>The server responds with <code>ACK(1)</code> of length 64, which is basically the
<code>OACK</code> packet with the first 4 bytes coming from <code>ACK(1)</code>.</p>
</li>
</ol>
<p><strong>Investigation.</strong> Luckily, the source code is readily available. I've looked
into the function <code>tftp_sendfile</code> of <code>tftp-hpa</code> that handles read requests.
Indeed, the option negotiation loop receives the option acknowledgment packet
<code>OACK</code> and waits for an <code>ACK</code> from the client. There are two cases:</p>
<ul>
<li>
<p>When it receives an <code>ACK</code> for block 0, it breaks out of the loop and continues with sending data blocks. <strong>This is the happy path.</strong></p>
</li>
<li>
<p>When it receives an acknowledgment for a block other than 0, block, it simply
continues the loop, retransmitting <code>OACK</code>. The issue is that <strong>the code uses
the same buffer</strong> for sending <code>OACK</code> and receiving <code>ACK</code> packets via different
pointers! Hence, it later sends an <code>OACK</code> packet that is corrupted with the
contents of the <code>ACK</code> packet. <strong>I don't think I would have found this by code
review!</strong></p>
</li>
</ul>
<p>Just for fun, I checked it with Claude. It could not identify this issue. The
trick is that the same buffer is pointed to by two different pointers, so Claude
is not clever enough to track this aliasing. When I explained the issue to
Claude, it was ecstatic: You have found a critical!</p>
<p>I've continued looking for the blast radius of this bug. Even though it somewhat
of memory corruption, it cannot crash the server, as the code is still writing
to the same buffer, allocated by the server itself. All it can do is to produce
malformed packets. Hence, it could probably crash a sloppy client, but would not
do much harm to a well-behaved client and itself. Moreover, if a client crashes
in such a case, anybody else on the network could have sent the malformed ACK as
well.</p>
<p>So this is a bug (from the specification p.o.v.), but it does not result in a
vulnerability. In any case, it was a deviation from the protocol specification.
Finally, one point to the specification!</p>
<table>
<thead>
<tr>
<th>Spec bugs</th>
<th>Implementation bugs</th>
</tr>
</thead>
<tbody>
<tr>
<td>13</td>
<td>1</td>
</tr>
</tbody>
</table>
<p><strong>Contacting the author.</strong> To be on the safe side, before writing this blog
post, I've contacted the author of tftp-hpa. As I expected, he also replied that
TFTP is an unencrypted unauthenticated protocol, so we should not expect much
security there.</p>
<h2 id="10.-the-specification-as-a-differential-testing-oracle" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#10.-the-specification-as-a-differential-testing-oracle">10. The specification as a differential testing oracle</a></h2>
<p>After finding the above implementation bug, I have decided to test other TFTP
implementations as well. This is where Claude was super useful again. I just
asked it to generate Dockerfiles for other implementations, which it did
quickly. It happened that a similar issue existed in another implementation. I
could not figure out the root cause in the source code of that other
implementation, as it is a bit harder to read than <code>tftp-hpa</code>. Hence, not giving
the details here.</p>
<p>Except this second deviation, the other implementations worked fine. Overall,
the specification scored another point:</p>
<table>
<thead>
<tr>
<th>Spec bugs</th>
<th>Implementation bugs</th>
</tr>
</thead>
<tbody>
<tr>
<td>13</td>
<td>2</td>
</tr>
</tbody>
</table>
<p>What I find really interesting here. Whenever I talk to engineers about formal
specifications, they tell me that they would like to do <strong>differential testing</strong>
instead of writing specifications. Meaning that they would like to compare the
behavior of one implementation against another implementation. However,
differential testing is not magic. It requires test inputs to compare the
implementations. Hence, <strong>if the test suite is missing adversarial test cases,
both implementations may pass the tests</strong>, even though they are both wrong.</p>
<p>What we did here with the TLA<sup>+</sup> specification is something more than
just differential testing. First, we have debugged the specification against
<code>tftp-hpa</code>, so we have extracted its expected behavior into a relatively small
and precise formal specification. Second, we have used this specification to
produce the tests for another implementation!</p>
<h2 id="11.-prior-work" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#11.-prior-work">11. Prior Work</a></h2>
<p>In this section, I've collected the previous work on model-based testing and
trace validation with TLA<sup>+</sup>:</p>
<ul>
<li>
<p>Nagendra et. al. Model guided fuzzing of distributed systems (2025).
Check <a href="https://www.youtube.com/watch?v=DO8MvouV29M&amp;utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">the talk</a>.</p>
</li>
<li>
<p>Cirstea, Kuppe, Merz, Loillier. Validating Traces of Distributed Systems
Against TLA+ Specifications (2024). Check the
<a href="https://arxiv.org/abs/2404.16075?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">arxiv paper</a>.</p>
</li>
<li>
<p>Chamayou et. al. Validating System Executions with the TLA+ Tools (2024).
See <a href="https://www.youtube.com/watch?v=NZmON-XmrkI&amp;utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">the talk</a>.</p>
</li>
<li>
<p>Halterman. Verifiability Gap: Why We Need More From Our Specs and
How We Can Get It (2020).
See <a href="https://www.youtube.com/watch?v=itcj9j2yWQo&amp;utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">the talk</a>.</p>
</li>
<li>
<p>Davis et al. eXtreme Modelling in Practice (2020).
See <a href="https://www.youtube.com/watch?v=IIGzXX72weQ&amp;utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">the talk</a>.</p>
</li>
<li>
<p>Kupriyanov, Konnov. Model-based testing with TLA+ and Apalache (2020).
See <a href="https://www.youtube.com/watch?v=aveoIMphzW8&amp;utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">the talk</a>.</p>
</li>
<li>
<p>Pressler. Verifying Software Traces Against a Formal Specification with
TLA<sup>+</sup> and TLC (2018).
Check <a href="https://pron.github.io/files/Trace.pdf?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">the paper</a>.</p>
</li>
</ul>
<p>I am pretty sure that this list is incomplete, so please let me know if you are
aware of any other relevant work.</p>
<h2 id="12.-conclusions" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/tftp-symbolic-testing.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#12.-conclusions">12. Conclusions</a></h2>
<p>This was a lot of text! Thank you for reading it till the end. It may look like
this project took me eternity to complete. In reality, <strong>it took me about two
weeks of part-time work</strong> to do it from the start to the end. On one hand, I
could probably do some parts of it faster, if I did not rely too much on Claude
for generating the test harness. On the other hand, <strong>Claude quickly generates
the code to start and stop services, parse their logs, etc.</strong> All the things
Docker were done by Claude, and I did not have to touch them. This is the work
that I find annoying and LLMs just do. In this experiment, I've burned all of my
monthly premium requests included in the Copilot plan. To be fair, I also had to
add a few features to the new Apalache API, as I was still experimenting with
it.</p>
<p>What I find interesting in the approach outlined here is that it presents a
(relatively) <strong>lightweight way to testing real-world protocols</strong>. Thinking of
fuzzing in this context, <strong>I don't think a standard fuzzer would have found the
above deviations in TFTP</strong>. Indeed, the implementation was not crashing. Nor it
was accessing memory out of bounds. It was just producing malformed packets
occasionally. To detect this, <strong>we needed a test oracle</strong> that would tell us,
whether a deviation happened. Writing such an oracle manually would be tedious
and error-prone. Instead, we have used <strong>a formal specification as a precise and
unambiguous oracle</strong>. Unambiguous does not mean deterministic though. Our oracle
is non-deterministic, but it precisely defines the allowed behaviors of the
protocol.</p>
<p>In addition to that, <a href="https://kernel.googlesource.com/pub/scm/network/tftp/tftp-hpa/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">tftp-hpa</a> is not just a piece of code that was written
by a startup over a weekend, or generated by an LLM. It is <strong>a very mature
project that has been written by professionals in the times when people had time
to think</strong>. They took care of the <a href="https://en.wikipedia.org/wiki/Sorcerer%27s_Apprentice_syndrome?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Sorcerer's Apprentice Syndrome</a>. This is
why I was quite surprised to see an unexpected packet from the server.</p>
<p>On the Apalache side, we finally have a symbolic approach that <strong>scales much
better than bounded model checking</strong>! In my experiments with TFTP, the new JSON
RPC API was showing the signs of <strong>slowing down only after about 200 steps</strong> of
symbolic execution. This is a huge improvement over the previous approach, where
Apalache was slowing down after about 10-20 steps. It is easy to see why. We
feed the concrete responses from the implementation into the SMT context, which
immediately produce a lot of simplifications.</p>
<p>We can <strong>improve this even further to essentially unlimited number of steps</strong>.
All what is needed is to keep the concrete trace on the harness side and
initialize the SMT context with the last state of the trace. We can do it every
step, or every <code>N</code> steps. The cool thing is that it all can be done outside of
Apalache, on the harness side! This opens the door to <strong>quick experimentation</strong>
with various strategies of mixing <strong>symbolic and concrete execution</strong>.</p>
]]></content>
    <author>
      <name>Igor Konnov</name>
      <email>igor@konnov.phd</email>
      <uri>https://konnov.phd/?utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site</uri>
    </author>
    
    
    <category term="tlaplus" />
    
    
    
  </entry>
  
  
  <entry>
    <title type="html">Formal Verification of the Aztec Governance Protocol</title>
    <link href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" rel="alternate" type="text/html" title="Formal Verification of the Aztec Governance Protocol" />
    <published>2025-12-09T00:00:00.000Z</published>
    <updated>2025-12-09T00:00:00.000Z</updated>
    <id>https://protocols-made-fun.com/aztec-governance.html</id>
    <content type="html" xml:base="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"><![CDATA[<p><strong>Authors:</strong> <a href="https://blltprf.xyz/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Thomas Pani</a>, <a href="https://konnov.phd/?utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site">Igor Konnov</a></p>
<p><strong>Date:</strong> December 9, 2025</p>
<h2 id="1.-introduction" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#1.-introduction">1. Introduction</a></h2>
<p>In August 2025, <a href="https://aztec-labs.com/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Aztec Labs</a> engaged <a href="https://blltprf.xyz/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Thomas Pani</a> and <a href="https://konnov.phd/?utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site">Igor Konnov</a> to formally specify and verify the new <strong>Aztec Governance Protocol</strong> – the core on-chain system that governs <a href="https://aztec.network/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Aztec Network</a>.</p>
<p>Over the course of five weeks, we reviewed every line of code in scope and developed a <strong>precise formal specification, verified automatically</strong> with <a href="https://github.com/apalache-mc/apalache?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache</a>. The result: scalable, massively parallel automated verification that explored the entire protocol state space to <strong>formally confirm correctness and uncover subtle, cross-contract issues</strong> that conventional audits or fuzzing can easily miss.</p>
<p>The team at Aztec Labs reviewed our findings and addressed all of them.</p>
<h3 id="at-a-glance-metrics" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#at-a-glance-metrics">At-a-Glance Metrics</a></h3>
<p>For the impatient reader, here are some key figures:</p>
<table>
<thead>
<tr>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>125 invariants</strong> specified across <strong>10 contracts</strong>, <strong>8 libraries</strong>, and <strong>8 interfaces</strong></td>
</tr>
<tr>
<td><strong>992 verification conditions</strong> checked in total</td>
</tr>
<tr>
<td><strong>72 physical cores / 368 GiB RAM</strong>, running for <strong>321 CPU-days</strong> (≈ 2 weeks)</td>
</tr>
<tr>
<td><strong>Findings:</strong> <span style="background-color:#C48F00; color:#fff; padding:2px 6px; border-radius:6px;">5 Medium</span> • <span style="background-color:#2F7D32; color:#fff; padding:2px 6px; border-radius:6px;">3 Low</span> • <span style="background-color:#005A9E; color:#fff; padding:2px 6px; border-radius:6px;">6 Info</span>  <a href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#9-findings">(jump ahead)</a></td>
</tr>
<tr>
<td><strong>Final complete verification run</strong> in <strong>576 CPU-hours</strong> (≈ 1 calendar day)</td>
</tr>
<tr>
<td><strong>Contract size:</strong> <strong>~2 kLOC</strong> Solidity</td>
</tr>
<tr>
<td><strong>Specification size:</strong> <strong>~4 kLOC</strong> Quint (incl. traceability comments)</td>
</tr>
</tbody>
</table>
<p>These runtimes are comparable to large-scale fuzzing campaigns – but with a crucial difference: <strong>formal verification explores every possible transaction symbolically</strong>, offering <em>exhaustive</em> reasoning rather than probabilistic coverage.</p>
<h3 id="highlights" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#highlights">Highlights</a></h3>
<p>Some of the key highlights from this article include:</p>
<ul>
<li><strong>Representative issue: Governance Insolvency</strong> with root-cause analysis and fix (<a href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#91-governance-insolvency">§9.1</a>)</li>
<li>Choosing the <strong>right tools</strong> (<a href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#4-choosing-the-right-tools">§4</a>)</li>
<li><strong>Bootstrapping the formal specification with AI</strong> (<a href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#52-bootstrapping-the-formal-specification-with-ai">§5.2</a>)</li>
<li><strong>Making verification scale</strong> with compositional reasoning and inductive invariants (<a href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#53-compositional-reasoning">§5.3</a>, <a href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#73-inductive-invariants-making-verification-scale">§7.3</a>)</li>
<li><strong>Showing that the protocol can progress</strong>: witnesses of liveness (<a href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#74-witnesses-of-liveness-proving-the-protocol-can-progress">§7.4</a>)</li>
</ul>
<p>Our formal report can be accessed via <a href="https://github.com/konnov/aztec-governance-formal-verification-2025q3/blob/e313681ade9f9e96d0e83a5120a670a1e1e07188/reports/Aztec-Governance-Protocol.pdf?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">this link</a> and the specifications can be found via <a href="https://github.com/konnov/aztec-governance-formal-verification-2025q3?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">this link</a>.</p>
<h2 id="2.-overview-of-aztec-governance" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#2.-overview-of-aztec-governance">2. Overview of Aztec Governance</a></h2>
<p>Aztec Network's governance is implemented as a suite of on-chain Solidity contracts. We summarize its multi-contract architecture, which required <strong>compositional analysis to verify</strong>, in the diagram below. The current implementation extends and formalizes the concepts from the <a href="https://forum.aztec.network/t/request-for-comments-aztec-governance/7413?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Aztec Governance RFC</a> – see <a href="https://docs.aztec.network/the_aztec_network/concepts/governance?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Aztec Governance</a> for the canonical documentation. This post reflects the protocol as of the <a href="https://github.com/AztecProtocol/aztec-packages/blob/8b10b2b220de38c9e2e2e2b7d05d7383701ba070?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">commit used in our engagement</a>. Parts of the codebase have evolved since our engagement.</p>
<p><img src="https://protocols-made-fun.com/assets/images/aztec-governance.webp?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" alt="Aztec diagram: contract architecture of Governance, GSE, Registry, Proposer, Slasher, and flows"></p>
<p>(We highlight the key contracts, with no special meaning attached to the colors.)</p>
<p><strong>Rollups and Registry.</strong> Aztec Network manages a system of rollups recorded in the <a href="https://github.com/AztecProtocol/aztec-packages/blob/8b10b2b220de38c9e2e2e2b7d05d7383701ba070/l1-contracts/src/governance/Registry.sol?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Registry</a>, which directs inflationary rewards to a single, designated <em>canonical rollup</em>.</p>
<p><strong>GovernanceProposer.</strong> <a href="https://github.com/AztecProtocol/aztec-packages/blob/8b10b2b220de38c9e2e2e2b7d05d7383701ba070/l1-contracts/src/governance/proposer/GovernanceProposer.sol?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">GovernanceProposer</a> (derived from <a href="https://github.com/AztecProtocol/aztec-packages/blob/8b10b2b220de38c9e2e2e2b7d05d7383701ba070/l1-contracts/src/governance/proposer/EmpireBase.sol?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">EmpireBase</a>) forms the foundational layer of the voting system, implementing a round-based signaling mechanism to determine which proposals advance to <code>Governance</code>.</p>
<p><strong>Governance.</strong> The <a href="https://github.com/AztecProtocol/aztec-packages/blob/8b10b2b220de38c9e2e2e2b7d05d7383701ba070/l1-contracts/src/governance/Governance.sol?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Governance</a> contract manages the full proposal lifecycle, including submission, voting, and execution. Given their critical role in managing the Aztec Network, <code>Governance</code> incorporates access control, such as whitelisting beneficiaries that participate in voting. On top of that, it implements an emergency proposal mechanism which requires a substantial token lock.</p>
<p><strong>Governance Staking Escrow (GSE).</strong> Governance stakes and corresponding voting rights are managed by the <em>Governance Staking Escrow</em> (<a href="https://github.com/AztecProtocol/aztec-packages/blob/8b10b2b220de38c9e2e2e2b7d05d7383701ba070/l1-contracts/src/governance/GSE.sol?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">GSE</a>) contract. <code>GSE</code> enables seamless migration of staked assets to new canonical chains, addressing the &quot;cold-start&quot; problem by ensuring immediate operational support during network upgrades. Proposals made through <code>GovernanceProposer</code> tie back to <code>GSE</code> during execution, verifying that at least two-thirds of the total stake is allocated to the latest rollup.</p>
<p><strong>SlashingProposer and Slasher.</strong> The <a href="https://github.com/AztecProtocol/aztec-packages/blob/8b10b2b220de38c9e2e2e2b7d05d7383701ba070/l1-contracts/src/core/slashing/SlashingProposer.sol?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">SlashingProposer</a> contract, also derived from <a href="https://github.com/AztecProtocol/aztec-packages/blob/8b10b2b220de38c9e2e2e2b7d05d7383701ba070/l1-contracts/src/governance/proposer/EmpireBase.sol?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">EmpireBase</a>, uses the same round-based signaling mechanism to determine which slashing proposals are forwarded to the <a href="https://github.com/AztecProtocol/aztec-packages/blob/8b10b2b220de38c9e2e2e2b7d05d7383701ba070/l1-contracts/src/core/slashing/Slasher.sol?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Slasher</a> contract.</p>
<p><strong>Libraries.</strong> The main contracts are supported by a set of custom libraries that use storage-layout compression for gas optimization. These libraries enable the system to retrieve historical, checkpointed state for computing voting power, implement a custom checkpointed set data structure built on OpenZeppelin's <code>Checkpoints.Trace224</code> library, implement the vote-tallying algorithm, and provide helper functions that encode the proposal lifecycle state machine.</p>
<h2 id="3.-attack-surface" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#3.-attack-surface">3. Attack Surface</a></h2>
<p>The attack surface of the Governance Protocol is significant – with <strong>over 40 external state-mutating functions across multiple contracts</strong> in scope. Moreover, problematic scenarios typically:</p>
<ul>
<li>involve multiple contracts,</li>
<li>exercise them over several transactions, and</li>
<li>can even involve multiple instances of the same contract (e.g., several Rollups, several Governance contracts, etc.).</li>
</ul>
<p><strong>Reasoning about time.</strong> <code>Governance</code> and <code>GovernanceProposer</code> use the block timestamp to organize signaling and voting phases. <code>GovernanceProposer</code> slots are short (<strong>fractions of a minute</strong>), while <code>Governance</code> voting periods are much longer (<strong>minutes to days</strong>). We must therefore reason about long time horizons interrupted by short-lived events.</p>
<p><strong>Malicious external inputs.</strong> To make things harder, we also considered scenarios in which a canonical rollup produces <strong>erroneous readings</strong> from time to time, e.g., due to a fault. For example, what if a canonical rollup starts to return slot numbers from the past (or far in the future)?</p>
<p>This poses both a <strong>challenge and an opportunity</strong>: standard techniques such as fuzzing, random simulation, or bounded model checking would not get us far – the state and action spaces are prohibitively large.</p>
<h2 id="4.-choosing-the-right-tools" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#4.-choosing-the-right-tools">4. Choosing the Right Tools</a></h2>
<p>With the attack surface in view, the next question was tooling: how to verify the protocol logic without drowning in bytecode.</p>
<h3 id="4.1.-protocol-level-specification" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#4.1.-protocol-level-specification">4.1. Protocol-Level Specification</a></h3>
<p>From an engineer's perspective, the ideal solution would be to verify correctness directly at the implementation level – that is, to automatically reason about the Solidity code itself. Tools such as <a href="https://www.certora.com/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Certora Prover</a>, <a href="https://kontrol.runtimeverification.com/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Kontrol</a>, <a href="https://github.com/a16z/halmos?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Halmos</a>, and <a href="https://hevm.dev/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">HEVM</a> aim to do exactly this by automating formal reasoning over smart contracts. These tools are remarkable engineering achievements, but their task is inherently complex: among other things, they must reason precisely about stack behavior, memory, storage, and external calls – all the way down to the EVM bytecode.</p>
<p>Before diving into such low-level reasoning, however, we believe it is essential to <strong>ensure that the protocol's logic is sound</strong>. If high-level properties of the protocol are violated, then verifying bit-level correctness provides limited value. Once the protocol logic is verified, attention can shift to the implementation.</p>
<p>In this project, we focused on <strong>specifying the Aztec Governance Protocol at the logic level</strong>. Our <strong>main objectives</strong> were to:</p>
<ul>
<li>specify the high-level behavior of the protocol,</li>
<li>identify its core invariants, and</li>
<li>prove these invariants correct (or demonstrate violations through counterexamples).</li>
</ul>
<h3 id="4.2.-languages-and-tools" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#4.2.-languages-and-tools">4.2. Languages and Tools</a></h3>
<p>Several specification languages could serve this purpose. For instance, we could have expressed the protocol directly in an interactive theorem prover like <a href="https://lean-lang.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Lean</a> or <a href="https://rocq-prover.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Rocq</a>. However, both would offer little automation, which would make limited progress feasible within our one-month timeframe. (Recently, Lean has seen exciting developments such as the <a href="https://lean-lang.org/doc/reference/latest/The--grind--tactic/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"><code>grind</code> tactic</a> and research by the <a href="https://verse-lab.github.io/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">VERSE group</a>. We may explore these in a future engagement!)</p>
<p><strong>TLA<sup>+</sup> and its tooling.</strong> <a href="https://tlapl.us/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">TLA<sup>+</sup></a> is perhaps the most well-known practical specification language. It is supported by two model checkers (<a href="https://github.com/tlaplus/tlaplus?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">TLC</a> and <a href="https://github.com/apalache-mc/apalache?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache</a>) and an interactive theorem prover <a href="https://proofs.tlapl.us/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">TLAPS</a>. We use the methodology of TLA<sup>+</sup> to reason about the Governance Protocol as a collection of interacting state machines over large state spaces. Since many engineers find the syntax of TLA<sup>+</sup> confusing, we use the surface syntax <a href="https://github.com/informalsystems/quint?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Quint</a> to write the specifications. As co-authors of both <strong>Quint</strong> and the <strong>Apalache</strong> model checker – together with Gabriela Moreira, Shon Feder, Jure Kukovec, and others – we have a deep understanding of their internals and how to apply them to large-scale protocol verification. This expertise was essential for scaling our analysis to a system as complex as Aztec Governance.</p>
<h2 id="5.-specification-decisions-and-challenges" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#5.-specification-decisions-and-challenges">5. Specification Decisions and Challenges</a></h2>
<p>With the goals and tools defined, the next step was to translate the Governance Protocol into a precise, analyzable specification.</p>
<h3 id="5.1.-writing-the-specification" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#5.1.-writing-the-specification">5.1. Writing the Specification</a></h3>
<p><strong>Modeling the states.</strong> Before specifying the contract behavior, we must decide how to model the contract states. We first define the shape of individual contract states. For example, below is the state of a <code>GovernanceProposer</code>.</p>
<pre><code class="hljs language-ts"><span class="hljs-comment">// GovernanceProposer contract state</span>
<span class="hljs-keyword">type</span> <span class="hljs-title class_">GovernanceProposerState</span> = {
  <span class="hljs-comment">// the state of the parent EmpireBase contract</span>
  <span class="hljs-attr">empireBase</span>: <span class="hljs-title class_">EmpireBaseState</span>,
  <span class="hljs-comment">// mapping(uint256 proposalId =&gt; address proposer)</span>
  <span class="hljs-attr">proposalProposer</span>: <span class="hljs-title class_">Uint256</span> -&gt; <span class="hljs-title class_">Address</span>,
  <span class="hljs-comment">// immutable config (set in constructor)</span>
  <span class="hljs-attr">REGISTRY</span>: <span class="hljs-title class_">Address</span>,
  <span class="hljs-attr">GSE</span>: <span class="hljs-title class_">Address</span>
}
</code></pre>
<p>You can see that many concepts from Solidity (like mappings) are seamlessly expressed in Quint. The full protocol state – including all relevant contracts – is captured by <code>EvmState</code>. In our case, an EVM state is structured as follows:</p>
<pre><code class="hljs language-ts"><span class="hljs-keyword">type</span> <span class="hljs-title class_">EvmState</span> = {
  <span class="hljs-attr">block_timestamp</span>: <span class="hljs-title class_">Uint256</span>,
  <span class="hljs-comment">// all possible instances of ERC20 used as assets</span>
  <span class="hljs-attr">assets</span>: <span class="hljs-title class_">Address</span> -&gt; <span class="hljs-title class_">ERC20State</span>,
  <span class="hljs-comment">// all possible instances of Governance</span>
  <span class="hljs-attr">governances</span>: <span class="hljs-title class_">Address</span> -&gt; <span class="hljs-title class_">GovernanceState</span>,
  <span class="hljs-comment">// all possible instances of GovernanceProposer</span>
  <span class="hljs-attr">governanceProposers</span>: <span class="hljs-title class_">Address</span> -&gt; <span class="hljs-title class_">GovernanceProposerState</span>,
  <span class="hljs-comment">// all possible instances of GSE</span>
  <span class="hljs-attr">gses</span>: <span class="hljs-title class_">Address</span> -&gt; <span class="hljs-title class_">GSEState</span>,
  <span class="hljs-comment">// all possible instances of Registry</span>
  <span class="hljs-attr">registries</span>: <span class="hljs-title class_">Address</span> -&gt; <span class="hljs-title class_">RegistryState</span>,
  <span class="hljs-comment">// all possible instances of RewardDistributor</span>
  <span class="hljs-attr">rewardDistributors</span>: <span class="hljs-title class_">Address</span> -&gt; <span class="hljs-title class_">RewardDistributorState</span>,
  <span class="hljs-comment">// all instances of Slasher</span>
  <span class="hljs-attr">slashers</span>: <span class="hljs-title class_">Address</span> -&gt; <span class="hljs-title class_">SlasherState</span>,
  <span class="hljs-comment">// all instances of SlashingProposer</span>
  <span class="hljs-attr">slashingProposers</span>: <span class="hljs-title class_">Address</span> -&gt; <span class="hljs-title class_">SlashingProposerState</span>,
  <span class="hljs-comment">// IEmperor(...).getCurrentSlot() for each rollup</span>
  <span class="hljs-attr">rollupSlot</span>: <span class="hljs-title class_">IHaveVersion</span> -&gt; int,
  <span class="hljs-comment">// IEmperor(...).getCurrentProposer() for each rollup</span>
  <span class="hljs-attr">rollupProposer</span>: <span class="hljs-title class_">IHaveVersion</span> -&gt; <span class="hljs-title class_">Address</span>,
  <span class="hljs-comment">// mapping rollup addresses to their versions</span>
  <span class="hljs-comment">// Corresponds to _rollup.getVersion() call in Registry.sol:53</span>
  <span class="hljs-attr">ROLLUP_VERSIONS</span>: <span class="hljs-title class_">IHaveVersion</span> -&gt; <span class="hljs-title class_">Uint256</span>,
  <span class="hljs-comment">// mapping rollup address to the reward distributors that they create</span>
  <span class="hljs-attr">REGISTRY_REWARD_DISTRIBUTORS</span>: <span class="hljs-title class_">IHaveVersion</span> -&gt; <span class="hljs-title class_">IRewardDistributor</span>
}
</code></pre>
<p>As you can see, we do not have to focus on nitty-gritty low-level details – like how storage is laid out in EVM. <em><strong>This frees us to focus on protocol logic and high-level correctness, rather than low-level implementation concerns. It also makes the reasoning problem more tractable for automated verification.</strong></em></p>
<p><strong>Modeling the contract functions.</strong> The contract functions are simply pure functions over the EVM state. For instance, we define the function <code>initiateWithdraw</code> in Quint as:</p>
<pre><code class="hljs language-ts"><span class="hljs-comment">// Governance.sol#L341</span>
pure def <span class="hljs-title class_">Governance</span>::<span class="hljs-title function_">initiateWithdraw</span>(<span class="hljs-attr">__evm_state</span>: <span class="hljs-title class_">EvmState</span>,
      <span class="hljs-attr">__self</span>: <span class="hljs-title class_">IGovernance</span>, <span class="hljs-attr">__msg_sender</span>: <span class="hljs-title class_">Address</span>,
      <span class="hljs-attr">_to</span>: <span class="hljs-title class_">Address</span>, <span class="hljs-attr">_amount</span>: <span class="hljs-title class_">Uint256</span>): <span class="hljs-title class_">Result</span>[<span class="hljs-title class_">EvmState</span>] = {
  val __state = __evm_state.<span class="hljs-property">governances</span>.<span class="hljs-title function_">get</span>(__self)
  val config = __state.<span class="hljs-property">configuration</span>

  <span class="hljs-comment">// ConfigurationLib.sol#L36:</span>
  <span class="hljs-comment">//   Timestamp.wrap(Timestamp.unwrap(_self.votingDelay) / 5) +</span>
  <span class="hljs-comment">//     _self.votingDuration + _self.executionDelay;</span>
  val withdrawDelay = config.<span class="hljs-property">votingDelay</span> / <span class="hljs-number">5</span>
      + config.<span class="hljs-property">votingDuration</span> + config.<span class="hljs-property">executionDelay</span>

  <span class="hljs-comment">// L342: _initiateWithdraw(msg.sender, _to, _amount,</span>
  <span class="hljs-comment">//                         configuration.withdrawalDelay());</span>
  <span class="hljs-title class_">Governance</span>::<span class="hljs-title function_">_initiateWithdraw</span>(__evm_state, __self, __msg_sender,
                                _to, _amount, withdrawDelay)
}
</code></pre>
<p>In Quint, we explicitly model all side-effects of the Solidity code, including exceptions and reverts. While it makes our specification more verbose, all branches and assignments become immediately visible at the code level – auditors do this in their heads all the time. For example, <code>_initiateWithdraw</code> computes and returns an updated <code>EvmState</code>, unless it reverts:</p>
<pre><code class="hljs language-ts"><span class="hljs-comment">// Governance.sol#L694</span>
pure def <span class="hljs-title class_">Governance</span>::<span class="hljs-title function_">_initiateWithdraw</span>(<span class="hljs-attr">__evm_state</span>: <span class="hljs-title class_">EvmState</span>,
      <span class="hljs-attr">__self</span>: <span class="hljs-title class_">IGovernance</span>, <span class="hljs-attr">_from</span>: <span class="hljs-title class_">Address</span>, <span class="hljs-attr">_to</span>: <span class="hljs-title class_">Address</span>,
      <span class="hljs-attr">_amount</span>: <span class="hljs-title class_">Uint256</span>, <span class="hljs-attr">_delay</span>: <span class="hljs-title class_">Timestamp</span>): <span class="hljs-title class_">Result</span>[<span class="hljs-title class_">EvmState</span>] = {
  val __state = __evm_state.<span class="hljs-property">governances</span>.<span class="hljs-title function_">get</span>(__self)
  <span class="hljs-comment">// L695: users[_from].sub(_amount);</span>
  val fromAmount = __state.<span class="hljs-property">users</span>.<span class="hljs-title function_">getOrElse</span>(_from, <span class="hljs-attr">checkpoints</span>::constructor)
  val userTraceOrError = <span class="hljs-attr">checkpoints</span>::<span class="hljs-title function_">sub</span>(__evm_state, fromAmount, _amount)
  <span class="hljs-keyword">if</span> (<span class="hljs-title function_">isErr</span>(userTraceOrError)) {
    <span class="hljs-title function_">err</span>(__evm_state, userTraceOrError.<span class="hljs-property">err</span>)
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// L696: total.sub(_amount);</span>
    val totalTraceOrError = <span class="hljs-attr">checkpoints</span>::<span class="hljs-title function_">sub</span>(__evm_state, __state.<span class="hljs-property">total</span>, _amount)
    <span class="hljs-keyword">if</span> (<span class="hljs-title function_">isErr</span>(totalTraceOrError)) {
      <span class="hljs-title function_">err</span>(__evm_state, totalTraceOrError.<span class="hljs-property">err</span>)
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-comment">// L698: uint256 withdrawalId = withdrawalCount++;</span>
      <span class="hljs-comment">// L700: withdrawals[withdrawalId] = Withdrawal({...});</span>
      val withdrawal = {
          <span class="hljs-attr">amount</span>: _amount,
          <span class="hljs-attr">unlocksAt</span>: __evm_state.<span class="hljs-property">block_timestamp</span> + _delay,
          <span class="hljs-attr">recipient</span>: _to, <span class="hljs-attr">claimed</span>: <span class="hljs-literal">false</span>
      }

      val __state1 = {
        ...__state,
        <span class="hljs-attr">users</span>: __state.<span class="hljs-property">users</span>.<span class="hljs-title function_">put</span>(_from, userTraceOrError.<span class="hljs-property">v</span>),  <span class="hljs-comment">// L695</span>
        <span class="hljs-attr">total</span>: totalTraceOrError.<span class="hljs-property">v</span>,                           <span class="hljs-comment">// L696</span>
        <span class="hljs-attr">withdrawals</span>: __state.<span class="hljs-property">withdrawals</span>.<span class="hljs-title function_">append</span>(withdrawal),  <span class="hljs-comment">// L700</span>
      }
      <span class="hljs-title function_">ok</span>({...__evm_state,
        <span class="hljs-attr">governances</span>: __evm_state.<span class="hljs-property">governances</span>.<span class="hljs-title function_">put</span>(__self, __state1)
      })
    }
  }
}
</code></pre>
<p><strong>Modeling transactions.</strong> We model transactions, e.g., initiated by externally-owned accounts (EOAs), via Quint <em>actions</em>:</p>
<pre><code class="hljs language-ts">action governance_initiate_withdraw = {
  nondet _g = evm.<span class="hljs-property">governances</span>.<span class="hljs-title function_">keys</span>().<span class="hljs-title function_">oneOf</span>()
  nondet _sender = <span class="hljs-variable constant_">ALL_SENDERS</span>.<span class="hljs-title function_">oneOf</span>()
  nondet _to = <span class="hljs-variable constant_">ALL_ADDRESSES</span>.<span class="hljs-title function_">oneOf</span>()
  nondet _amount = <span class="hljs-number">0.</span><span class="hljs-title function_">to</span>(<span class="hljs-variable constant_">MAX_UINT256</span>).<span class="hljs-title function_">oneOf</span>()
  val result = <span class="hljs-title class_">Governance</span>::<span class="hljs-title function_">initiateWithdraw</span>(evm, _g, _sender, _to, _amount)
  all {
    <span class="hljs-title function_">is_valid_sender</span>(_sender) and <span class="hljs-title function_">isOk</span>(result),
    evm<span class="hljs-string">&#x27; = result.v,
    // ...
  }
}
</span></code></pre>
<p>This directly controls the domains from which input parameters are drawn. When we run the Quint randomized simulator, non-deterministic values are sampled uniformly at random. When we run the Apalache model checker, it uses logic constraints in the <a href="https://github.com/Z3Prover/z3?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Z3 SMT solver</a> to reason about all possible non-deterministic values at once.</p>
<h3 id="5.2.-bootstrapping-the-formal-specification-with-ai" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#5.2.-bootstrapping-the-formal-specification-with-ai">5.2. Bootstrapping the Formal Specification with AI</a></h3>
<p>The above specification looks a bit machine-generated. This is not far from the truth. We used an LLM to produce the initial specifications, given the source code in Solidity and the Quint data types.</p>
<p>Obviously, an LLM cannot make high-level modeling decisions, like how to structure the EVM state, or how best to turn Solidity into functional definitions – this <strong>requires years of practical experience</strong>. We developed a custom system prompt that gives the LLM clear instructions and examples for translating Solidity into Quint. (It's an internal tool we refine and apply with clients when we bootstrap their specifications.)</p>
<p>Of course, as with all AI assistants, we had to carefully proofread the translation results. Also, we were fortunate to have the model checker <a href="https://github.com/apalache-mc/apalache?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache</a> on our side – it automatically pointed us to some inconsistencies in the translation. Compared to writing the specification by hand, this approach allowed us to bootstrap the project very quickly and to start evaluating the protocol early on.</p>
<h3 id="5.3.-compositional-reasoning" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#5.3.-compositional-reasoning">5.3. Compositional Reasoning</a></h3>
<p>Some security researchers believe that formal verification does not scale to more than 1–2 smart contracts, or to exploit scenarios longer than 1–2 external calls deep. We have organized our specification in such a way that the verification tools can deal with the behavior of 10–20 smart contracts, and arbitrarily long transaction sequences. <strong>This level of scalability requires not just formal verification expertise, but a deep understanding of how model checkers and provers work internally</strong> and interact with protocol architecture. It builds directly on our prior formal verification work – including <a href="https://protocols-made-fun.com/zksync-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">zkSync Governance</a>, <a href="https://protocols-made-fun.com/chonkybft.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">ChonkyBFT</a>, and <a href="https://arxiv.org/abs/2501.07958?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Ethereum 3-slot-finality</a> – <strong>where we pushed verification tools to reason compositionally across complex systems</strong>. More on this in Section <a href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#73-inductive-invariants-making-verification-scale">7.3. Inductive Invariants: Making Verification Scale</a>.</p>
<h2 id="6.-protocol-invariants" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#6.-protocol-invariants">6. Protocol Invariants</a></h2>
<p>From Aztec's documentation and source code, we extracted and formalized <strong>125 key invariants</strong> of the Governance Protocol. To get a taste of the invariants, here are a few examples in English (more of them are in the <a href="https://github.com/konnov/aztec-governance-formal-verification-2025q3/blob/e313681ade9f9e96d0e83a5120a670a1e1e07188/reports/Aztec-Governance-Protocol.pdf?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">report</a>):</p>
<ul>
<li><strong>GOV-16</strong>: If the proposal has not been active yet, then no votes have been cast.</li>
<li><strong>GOV-20:</strong> The timestamps in the <code>users</code> traces are ordered.</li>
<li><strong>GOV-26</strong>: For each timestamp <code>t</code>, <code>total[t]</code> equals the sum of the users' voting
power at <code>t</code>.</li>
<li><strong>GP-02-01</strong>: For each submitted proposal in <code>proposalProposer</code>, there is round accounting for a corresponding executed proposal (i.e., submitted to Governance).</li>
<li><strong>GP-08</strong>: A proposal cannot be executed without a quorum.</li>
<li><strong>GSE-17:</strong> for each proposal that the <code>delegatee</code> has <code>powerUsed</code> on, Governance contains that proposal.</li>
<li><strong>GSE-19:</strong> <code>powerUsed</code> cannot exceed the attester’s voting power at the time of the proposal's <code>pendingThrough</code>.</li>
<li><strong>GSE-23:</strong> <code>delegation.supply</code> at each checkpoint is the sum of all <code>delegation.ledgers[instance].supply</code> at that time.</li>
<li><strong>SP-10</strong>: lastSignalSlot is in the valid range.
This range is <code>[round * ROUND_SIZE, (round + 1) * ROUND_SIZE)</code>.</li>
<li><strong>SP-11</strong>: The number of signals is correct. It does not exceed <code>lastSignalSlot % ROUND_SIZE + 1</code>.</li>
</ul>
<p><strong>Formalized invariants in Quint.</strong> We formalized all 125 invariants in Quint as
well. For example, the <code>Governance</code> contract should uphold the <strong>Solvency Invariant</strong>
(<a href="https://www.certora.com/blog/the-holy-grail?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">'The Holy Grail'</a>, as coined by FV researchers at
<a href="https://www.certora.com/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Certora</a>):</p>
<pre><code class="hljs language-ts"><span class="hljs-comment">// GOV-28: Solvency: Governance holds enough balance to cover all future</span>
<span class="hljs-comment">// withdrawals.</span>
pure def <span class="hljs-title function_">governance_solvency_inv</span>(<span class="hljs-attr">_evm</span>: <span class="hljs-title class_">EvmState</span>, <span class="hljs-attr">ga</span>: <span class="hljs-title class_">IGovernance</span>): bool = {
  pure val g = _evm.<span class="hljs-property">governances</span>.<span class="hljs-title function_">get</span>(ga)
  and {
    <span class="hljs-comment">// the withdrawals that happen in the future</span>
    pure val payable = g.<span class="hljs-property">withdrawals</span>.<span class="hljs-title function_">indices</span>().<span class="hljs-title function_">fold</span>(<span class="hljs-number">0</span>, <span class="hljs-function">(<span class="hljs-params">sum, i</span>) =&gt;</span> {
      pure val withdrawal = g.<span class="hljs-property">withdrawals</span>[i]
      sum + <span class="hljs-keyword">if</span> (withdrawal.<span class="hljs-property">claimed</span>) <span class="hljs-number">0</span> <span class="hljs-keyword">else</span> withdrawal.<span class="hljs-property">amount</span>
    })
    <span class="hljs-comment">// the total user&#x27;s balance, add payable, is below the contract&#x27;s balance</span>
    pure val asset = _evm.<span class="hljs-property">assets</span>.<span class="hljs-title function_">get</span>(g.<span class="hljs-property">ASSET</span>)
    g.<span class="hljs-property">total</span>.<span class="hljs-title function_">latest</span>() + payable &lt;= asset.<span class="hljs-property">balances</span>.<span class="hljs-title function_">getOrElse</span>(ga, <span class="hljs-number">0</span>)
  }
}
</code></pre>
<p>Turns out, the solvency invariant is actually violated under certain conditions. We will get back to it in Section <a href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#91-governance-insolvency">9.1. Governance Insolvency</a>.</p>
<p>With the key invariants defined, we started verifying them using Quint and Apalache.</p>
<h2 id="7.-formal-verification-workflow" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#7.-formal-verification-workflow">7. Formal Verification Workflow</a></h2>
<p>As soon as parts of the specification stabilized, we began verification – moving from randomized simulation to full symbolic and inductive reasoning.</p>
<h3 id="7.1.-randomized-simulator%3A-stuck-at-unproductive-inputs" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#7.1.-randomized-simulator%3A-stuck-at-unproductive-inputs">7.1. Randomized Simulator: Stuck at Unproductive Inputs</a></h3>
<p>The <strong>Quint randomized simulator</strong> operates similarly to property-based testing for implementation languages: it assigns concrete values to <code>nondet</code> declarations and resolves non-deterministic control choices by selecting one branch at random.</p>
<p><strong>Limitations.</strong> We briefly experimented with this approach, but it proved ineffective for our purposes. The simulator's uniform random sampling consistently failed to produce valid configurations that would even satisfy the protocol's initial state:</p>
<pre><code class="hljs language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">quint run --max-samples=100000 --max-steps=10  --invariant=past_signals \
    spec/slashing_proposer_machine.qnt</span>
An example execution:

[ok] No violation found (768ms at 130208 traces/second).
Trace length statistics: max=0, min=0, average=0.00
</code></pre>
<p>We believe the randomized simulator could be improved in future versions. If you'd like to explore in more detail why this happens – and how it could be mitigated – check out our workshop <a href="https://blltprf.xyz/blog/25-min-solidity-fuzzer/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"><em>25-Minute Solidity Fuzzer: Fuzzing Smarter, Not Harder</em></a>.</p>
<p>In its current form, however, it did not help us uncover issues. This led us to use the <strong>symbolic analysis tools in Apalache</strong>, which can reason over all possible inputs symbolically rather than sampling concrete ones.</p>
<h3 id="7.2.-symbolic-random-walks%3A-scaling-up" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#7.2.-symbolic-random-walks%3A-scaling-up">7.2. Symbolic Random Walks: Scaling Up</a></h3>
<p>With symbolic random walks (part of Apalache), we quickly checked several invariants. The following run revealed an issue: the system could receive outdated (&quot;past&quot;) signals when the canonical rollup was faulty:</p>
<pre><code class="hljs language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">quint verify --random-transitions=<span class="hljs-literal">true</span> --max-steps=10 \
  --invariant=past_signals spec/slashing_proposer_machine.qnt</span>
...
[violation] Found an issue (22181ms)
</code></pre>
<p>When an invariant is violated, Apalache produces a counterexample with all details needed to understand the issue. We omit it here because it is quite verbose.</p>
<p><strong>Limitations.</strong> Even though this approach proved to be quite useful in bootstrapping and debugging our specification, it reached its limits when we began dealing with multiple contracts. This limitation stems from the protocol's scale – with over 40 external functions, many of which can be invoked at nearly any point in time, the number of possible symbolic paths grows combinatorially with path length. We then moved to proving <em>inductive invariants</em> automatically.</p>
<h3 id="7.3.-inductive-invariants%3A-making-verification-scale" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#7.3.-inductive-invariants%3A-making-verification-scale">7.3. Inductive Invariants: Making Verification Scale</a></h3>
<p>To scale our formal verification efforts further, we specified 125 invariants that together capture any arbitrary state of the Governance Protocol. For example, below is the invariant <code>gse_rollups_inv</code> that groups the invariants <code>GSE-28</code> to <code>GSE-32</code>:</p>
<pre><code class="hljs language-ts">pure def <span class="hljs-title function_">gse_rollups_inv</span>(<span class="hljs-attr">evm</span>: <span class="hljs-title class_">EvmState</span>, <span class="hljs-attr">gsea</span>: <span class="hljs-variable constant_">IGSE</span>): bool = {
  val gse = evm.<span class="hljs-property">gses</span>.<span class="hljs-title function_">get</span>(gsea)
  val chkpts = gse.<span class="hljs-property">rollups</span>.<span class="hljs-property">_checkpoints</span>
  and {
    <span class="hljs-comment">// GSE-28: `rollups` is an ordered checkpointed trace with ascending timestamps</span>
    <span class="hljs-title function_">_trace_is_ordered</span>(gse.<span class="hljs-property">rollups</span>),
    chkpts.<span class="hljs-title function_">indices</span>().<span class="hljs-title function_">forall</span>(<span class="hljs-function"><span class="hljs-params">i</span> =&gt;</span> and {
      <span class="hljs-comment">// GSE-29: `rollups` values are rollup addresses</span>
      chkpts[i].<span class="hljs-property">_value</span>.<span class="hljs-title function_">in</span>(<span class="hljs-variable constant_">ROLLUP_ADDRESSES</span>),
      <span class="hljs-comment">// GSE-30: the bonus instance does not appear in the `rollups` history</span>
      chkpts[i].<span class="hljs-property">_value</span> != <span class="hljs-variable constant_">BONUS_INSTANCE_ADDRESS</span>,
      <span class="hljs-comment">// GSE-31: `rollups` values are registered in `instances`</span>
      chkpts[i].<span class="hljs-property">_value</span>.<span class="hljs-title function_">in</span>(gse.<span class="hljs-property">instances</span>.<span class="hljs-title function_">keys</span>()),
    })
  }
}
</code></pre>
<p>The following command checks that all protocol invariants (<code>all_inv</code>) – including <code>gse_rollups_inv</code> – hold whenever the protocol is in a state that satisfies <code>all_inv</code> and one of the contracts makes a single step:</p>
<pre><code class="hljs language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">./scripts/quint-inductive.sh spec/invariant_model.qnt 31 32 5 100 all_inv</span>
</code></pre>
<p>Beware that the above command runs over 900 verification runs in parallel (in the example above using at most 5 CPUs at once). This can easily overwhelm your laptop. If you want to reproduce our experiments, read the next section on our experimental setup.</p>
<p><strong>Scalable verification.</strong> The <strong>core technique</strong> that enables this level of scalability is the use of <strong>inductive invariants</strong>. Instead of exploring all possible symbolic paths of the specification from an initial state (this approach, used by most code-level symbolic tools, is called <em>symbolic execution</em>), we start with a much richer set of states (captured by the inductive invariant <code>all_inv</code>) and simply enumerate all possible external functions and make them execute exactly once from any state in the inductive invariant. By assuming that <code>all_inv</code> holds in an arbitrary state and showing that it still holds after symbolically executing any single transaction, our check <strong>extends inductively to all possible executions</strong>.</p>
<p><strong>Note for sticklers.</strong> We still have to show that the initial states satisfy the inductive invariant. In our case, this is easy. Essentially, the initial state of the protocol is an &quot;empty&quot; EVM state where none of the governance contracts are deployed yet.</p>
<h3 id="7.4.-witnesses-of-liveness%3A-proving-the-protocol-can-progress" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#7.4.-witnesses-of-liveness%3A-proving-the-protocol-can-progress">7.4. Witnesses of Liveness: Proving the Protocol Can Progress</a></h3>
<p>When verifying safety, there is always a risk that we introduce a bug in the specification that restricts the protocol behavior too much. This would still keep the protocol &quot;safe&quot; from the verification point of view, but, obviously, the protocol would not do as many useful things as it is meant to do. To avoid this pitfall, we introduce &quot;falsy invariants&quot; that instruct Apalache to generate a witness of the protocol reaching an &quot;interesting&quot; state. Below is an example to produce an execution to a state in which at least one governance proposal has been executed:</p>
<pre><code class="hljs language-ts"><span class="hljs-comment">// Check this invariant to find an example of having at least one executed proposal:</span>
<span class="hljs-comment">// quint verify --max-steps=0 --invariant=gov_proposals_executed_ex \</span>
<span class="hljs-comment">//   spec/invariant_model.qnt</span>
val gov_proposals_executed_ex = {
  <span class="hljs-title function_">not</span>(evm.<span class="hljs-property">governances</span>.<span class="hljs-title function_">keys</span>().<span class="hljs-title function_">forall</span>(<span class="hljs-function"><span class="hljs-params">ga</span> =&gt;</span> {
    val g = evm.<span class="hljs-property">governances</span>.<span class="hljs-title function_">get</span>(ga)
    g.<span class="hljs-property">proposals</span>.<span class="hljs-title function_">indices</span>().<span class="hljs-title function_">exists</span>(<span class="hljs-function"><span class="hljs-params">proposalId</span> =&gt;</span> {
      val proposal = g.<span class="hljs-property">proposals</span>[proposalId]
      proposal.<span class="hljs-property">cachedState</span> == <span class="hljs-title class_">ProposalState</span>_Executed
    })
  }))
}
</code></pre>
<p>This ability to automatically generate an execution trace to an 'interesting' state is a <strong>superpower of symbolic model checkers like Apalache</strong> – a functionality that would be <strong>far more difficult</strong> to automate with an interactive theorem prover such as Lean or Rocq. (Provers have property-based testing tools, but they are not tuned to bug finding in distributed protocols like <a href="https://github.com/apalache-mc/apalache?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache</a>.)</p>
<h2 id="8.-experimental-setup-and-verification-runs" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#8.-experimental-setup-and-verification-runs">8. Experimental Setup and Verification Runs</a></h2>
<p><strong>Experimental setup.</strong> As mentioned above, checking the inductive invariant of our specification produces 992 verification tasks in total (for the combinations of a specific invariant and an external function call). Apalache decomposes invariant checking into smaller tasks, so we employ <a href="https://www.gnu.org/software/parallel/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">GNU parallel</a> to <strong>massively parallelize the verification</strong>. We use two servers to run the experiments:</p>
<ol>
<li>AMD Ryzen 9 5950X processor (16 physical, 32 logical cores), 128 GB memory</li>
<li>2× Intel Xeon Platinum 8280 processor (56 physical, 112 logical cores total), pinned at 3.1 GHz, 240 GB memory</li>
</ol>
<p><strong>Verification Runs.</strong> Some of the verification tasks take a few minutes to check, and some of them take a few hours. This is caused by the nature of the SMT constraints. It is well-known that SMT solvers, including Z3, are challenged by non-linear integer arithmetic – in this project, they naturally appear, e.g., as part of Aztec's vote tallying logic.</p>
<p>Instead of writing many words, we simply show you the plot below. It visualizes the running times of <a href="https://github.com/apalache-mc/apalache?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache</a> when checking the 992 verification tasks. The X-axis shows the number of verification conditions solved (roughly, individual constraints in the inductive invariant), sorted from fastest to slowest. Each point corresponds to one verification condition. The Y-axis represents the running time per verification condition, formatted in human-readable units (milliseconds to hours). Notice the logarithmic scale!</p>
<p><img src="https://protocols-made-fun.com/assets/images/aztec-governance-verification-times.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" alt="aztec-gov-plot-all"></p>
<p>As we can see from the plot, over 85% of the verification conditions are checked in less than 10 minutes each, about 7% are checked in several hours, and about 8% of the verification conditions require plenty of running time.</p>
<p><strong>Timeouts.</strong> As it happens with SMT solvers, 3% of our verification conditions time out. These are the runs at the end of the &quot;hockey stick&quot;. We capped the running time of Z3 at 12 hours. Since we are decomposing the inductive invariant into smaller pieces, these problematic conditions are well-localized. We have investigated these conditions. They all have to do with non-linear arithmetic.</p>
<p>Below is an example of such an invariant. Notice that the very last expression involves modulo over a non-constant value, since <code>ROUND_SIZE</code> is initialized in the contract constructor.</p>
<pre><code class="hljs language-ts"><span class="hljs-comment">// GovernanceProposer invariant on last signals and total signals</span>
pure def <span class="hljs-title function_">governance_proposer_signal_inv</span>(<span class="hljs-attr">evm</span>: <span class="hljs-title class_">EvmState</span>,
                                        <span class="hljs-attr">ga</span>: <span class="hljs-title class_">IGovernanceProposer</span>): bool = {
  val gp = evm.<span class="hljs-property">governanceProposers</span>.<span class="hljs-title function_">get</span>(ga)
  gp.<span class="hljs-property">empireBase</span>.<span class="hljs-property">rounds</span>.<span class="hljs-title function_">keys</span>().<span class="hljs-title function_">forall</span>(<span class="hljs-function"><span class="hljs-params">rollup</span> =&gt;</span> {
    gp.<span class="hljs-property">empireBase</span>.<span class="hljs-property">rounds</span>.<span class="hljs-title function_">get</span>(rollup).<span class="hljs-title function_">keys</span>().<span class="hljs-title function_">forall</span>(<span class="hljs-function"><span class="hljs-params">round</span> =&gt;</span> {
      val rollupRounds = gp.<span class="hljs-property">empireBase</span>.<span class="hljs-property">rounds</span>.<span class="hljs-title function_">get</span>(rollup)
      val accounting = rollupRounds.<span class="hljs-title function_">get</span>(round)
      and {
        <span class="hljs-comment">// GP-12: ...</span>
        <span class="hljs-comment">// ...</span>
        <span class="hljs-comment">// GP-13: The number of signals is in the right range</span>
        <span class="hljs-comment">// It does not exceed `lastSignalSlot % ROUND_SIZE + 1`.</span>
        <span class="hljs-comment">// This property is very hard for Z3. It is not falsified.</span>
        and {
          gp.<span class="hljs-property">empireBase</span>.<span class="hljs-property">ROUND_SIZE</span> &lt;= <span class="hljs-variable constant_">MAX_ROUND_SIZE</span>,
          totalSignalCount &gt;= <span class="hljs-number">0</span>,
          totalSignalCount &lt;= gp.<span class="hljs-property">empireBase</span>.<span class="hljs-property">ROUND_SIZE</span>,
          totalSignalCount &lt;=
            (accounting.<span class="hljs-property">lastSignalSlot</span> % gp.<span class="hljs-property">empireBase</span>.<span class="hljs-property">ROUND_SIZE</span>) + <span class="hljs-number">1</span>,
        }
      }
    })
  })
}
</code></pre>
<p>We classify the small number of the verification conditions that time out as <em>not falsified</em> rather than verified. Usually, we recommend verifying such conditions with a theorem prover such as Lean or Rocq. Another solution is to fix these non-constant values to known production configuration values to gain further confidence.</p>
<h2 id="9.-findings" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#9.-findings">9. Findings</a></h2>
<p>Our verification of the Aztec Governance Protocol uncovered <strong>five Medium</strong>, <strong>three Low</strong>, and <strong>six Informational findings</strong>. Most arose from subtle cross-contract interactions that are difficult to identify through conventional testing, fuzzing, or simulation alone. We reported all issues to Aztec Labs, who acknowledged and/or fixed them in subsequent pull requests.</p>
<p>Below we explain one representative issue: a <strong>violation of the solvency invariant</strong>.</p>
<h3 id="9.1.-governance-insolvency" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#9.1.-governance-insolvency">9.1. Governance Insolvency</a></h3>
<p>Recall the solvency invariant from <a href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#6-protocol-invariants">Protocol Invariants</a>. When we check it, Apalache produces a counterexample. Below is the root cause of this issue:</p>
<pre><code class="hljs language-ts"><span class="hljs-keyword">function</span> <span class="hljs-title function_">deposit</span>(<span class="hljs-params">address _beneficiary, uint256 _amount</span>) external
        <span class="hljs-title function_">override</span>(<span class="hljs-title class_">IGovernance</span>) <span class="hljs-title function_">isDepositAllowed</span>(<span class="hljs-params">_beneficiary</span>) {
  <span class="hljs-variable constant_">ASSET</span>.<span class="hljs-title function_">safeTransferFrom</span>(msg.<span class="hljs-property">sender</span>, <span class="hljs-title function_">address</span>(<span class="hljs-variable language_">this</span>), _amount);
    <span class="hljs-comment">// &lt;--- if msg.sender == address(this), then the balances do not change</span>
  users[_beneficiary].<span class="hljs-title function_">add</span>(_amount);
    <span class="hljs-comment">// &lt;--- ...but the liabilities always get increased</span>
  total.<span class="hljs-title function_">add</span>(_amount);
  emit <span class="hljs-title class_">Deposit</span>(msg.<span class="hljs-property">sender</span>, _beneficiary, _amount);
}
</code></pre>
<p>In short, <strong>executing an approved governance proposal</strong> can invoke <code>Governance.deposit(...)</code>. Inside <code>deposit</code>, this performs an ERC-20 self-transfer – leaving token balances unchanged – while <strong>crediting <code>_beneficiary</code> and increasing <code>total</code></strong>. <code>Governance</code>'s liabilities go up, but its assets don't – the contract becomes <strong>insolvent</strong>. The diagram below illustrates the problematic scenario.</p>
<p><img src="https://protocols-made-fun.com/assets/images/gov-insolvency.svg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" alt="aztec-gov-plot-all"></p>
<p><strong>On ERC-20 approvals.</strong> Most ERC20 token implementations would require Governance to execute an explicit token approval for the self-transfer before the call to <code>deposit()</code> (while executing the governance proposal). Calling <code>ASSET</code> is forbidden by the current <code>Governance</code> implementation. However, certain tokens like <a href="https://etherscan.io/address/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#code">WETH</a> do <strong>not</strong> require approvals for <code>transferFrom()</code> if <code>from == msg.sender</code>.</p>
<p><strong>Resolution.</strong> We raised this finding with Aztec Labs who addressed it in PR <a href="https://github.com/AztecProtocol/aztec-packages/pull/16917?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">#16917</a> by forbidding Governance from calling <code>deposit()</code> itself. In addition, the <a href="https://etherscan.io/address/0xA27EC0006e59f245217Ff08CD52A7E8b169E62D2?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#code">current AZTEC token implementation</a> uses OpenZeppelin's ERC-20 implementation, which does require explicit approval of self-transfers.</p>
<h3 id="9.2.-other-findings" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#9.2.-other-findings">9.2. Other Findings</a></h3>
<p>For details on all our findings, refer to our <a href="https://github.com/konnov/aztec-governance-formal-verification-2025q3/blob/e313681ade9f9e96d0e83a5120a670a1e1e07188/reports/Aztec-Governance-Protocol.pdf?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">formal report</a>.</p>
<h2 id="10.-conclusion%3A-scalable-formal-verification-in-practice" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#10.-conclusion%3A-scalable-formal-verification-in-practice">10. Conclusion: Scalable Formal Verification in Practice</a></h2>
<p>Our formal verification of the Aztec Governance Protocol went far beyond a traditional audit. It was a <strong>compositional, protocol-level analysis</strong> using state-of-the-art tools and techniques that we helped create. We formally proved <strong>125 high-level invariants</strong> across a multi-contract system – reasoning over a search space beyond the reach of traditional testing and most formal verification tools. These invariants were automatically decomposed into 992 verification conditions, which let us further parallelize the verification task.</p>
<p>By combining <strong>inductive invariants, symbolic reasoning, and massive parallelization</strong> (321 CPU-days of compute), we showed that formal verification can scale to the complexity of modern, mission-critical smart contract systems. Our methodology enables <strong>exhaustive, automated reasoning</strong> about real-world governance mechanisms and other smart contract protocols.</p>
<p>For systems like Aztec Governance, where bugs are subtle but potentially catastrophic, <strong>deep understanding of the tools and underlying logic</strong> is essential. This project demonstrates that scalable, unbounded formal verification is not just theoretically possible – it’s practical today for mature, production-grade protocols.</p>
<h3 id="differential-testing%3A-spec-%2F-implementation-conformance" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/aztec-governance.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#differential-testing%3A-spec-%2F-implementation-conformance">Differential Testing: Spec / Implementation Conformance</a></h3>
<p>A natural next step would be to <strong>connect the formal protocol specification</strong> with the actual Solidity implementation to <strong>close the verification loop</strong> (known as <em>differential</em> or <em>conformance testing</em>). With our methodology, it suffices to check that each external function call in Solidity conforms to its formal specification in Quint. Traditionally, this is done by writing and proving pre- and post-conditions in Hoare logic – e.g., using <a href="https://www.certora.com/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Certora Prover</a>. We suggest that a <strong>more pragmatic approach</strong> is to <strong>fuzz external Solidity functions directly against the formal specification</strong>.</p>
<p><strong>Enabling diff testing in Apalache:</strong> We have just implemented a new <a href="https://github.com/apalache-mc/apalache/tree/main/json-rpc?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Apalache JSON-RPC API</a>, which enables interactive differential testing between implementation and specification. This delivers <strong>fast, actionable, and reproducible results</strong> while still providing a <strong>high level of assurance</strong> grounded in rigorous formal modelling.</p>
]]></content>
    <author>
      <name>Igor Konnov</name>
      <email>igor@konnov.phd</email>
      <uri>https://konnov.phd/?utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site</uri>
    </author>
    
    
    <category term="quint" />
    
    
    
  </entry>
  
  
  <entry>
    <title type="html">Small scope hypothesis revisited</title>
    <link href="https://protocols-made-fun.com/small-scope.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed" rel="alternate" type="text/html" title="Small scope hypothesis revisited" />
    <published>2025-12-02T00:00:00.000Z</published>
    <updated>2025-12-02T00:00:00.000Z</updated>
    <id>https://protocols-made-fun.com/small-scope.html</id>
    <content type="html" xml:base="https://protocols-made-fun.com/small-scope.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed"><![CDATA[<p><strong>Author:</strong> <a href="https://konnov.phd/?utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site">Igor Konnov</a></p>
<p><strong>Tags:</strong> specification tlaplus tlc</p>
<p>A couple of weeks ago, I gave a talk at the internal Nvidia FM Week 2025. Many
thanks to <a href="https://github.com/lemmy?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Markus Kuppe</a> for the organization and invitation! I am going to
write a longer blog post about interactive spec conformance testing with
Apalache later. Today, I want to talk a bit about the question posed by Markus
(to find the question, continue reading).</p>
<p>Let's talk about the small scope hypothesis. As formulated by Jackson in the
<a href="https://dspace.mit.edu/bitstream/handle/1721.1/149864/MIT-LCS-TR-735.pdf?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">technical report</a> (1997), this hypothesis reads as follows:</p>
<p class="highlight-question"><strong><em>
    "...most errors can be demonstrated by counterexamples within a small scope."
</em></strong></p>
<p>As you will see below, my example fits into this hypothesis quite well. However,
having spoken to many engineers over the years, I believe that there is a
mismatch between what engineers understand by &quot;small scope&quot; and what
verification engineers understand by &quot;small scope&quot;.</p>
<p>In this blog post, I've decided to try a <strong>new format</strong>. Since everyone is using
LLMs nowadays, I will follow the protocol. I will present the example and the
problem of finding a small scope. Then, it is your turn to decide how this blog
post should continue. If someone gives me an interesting example or insight in a
<a href="https://protocols-made-fun.com/small-scope.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#end">comment</a>, I will update this blog post accordingly.</p>
<h2 id="1.-example-1%3A-buggy-circular-buffer" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/small-scope.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#1.-example-1%3A-buggy-circular-buffer">1. Example 1: Buggy circular buffer</a></h2>
<h3 id="1.1.-the-specification" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/small-scope.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#1.1.-the-specification">1.1. The specification</a></h3>
<p>I started the talk with a TLA<sup>+</sup> specification of a <strong>buggy</strong> circular
buffer. You can find the full specification, the model checking models, and the
TLC configuration files <a href="https://github.com/konnov/cyclic-buffer-challenge/tree/main/tla?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">here</a>. The specification looks as follows:</p>
<pre><code class="hljs language-tlaplus"><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span> <span class="hljs-keyword">MODULE</span> BuggyCircularBuffer <span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span>
<span class="hljs-comment">(**
 * A very simple specification of a circular buffer with a bug.
 * Generated with ChatGPT and beautified by Igor Konnov, 2025.
 * ChatGPT learned abstraction so well that it omitted the actual buffer storage!
 *)</span>
<span class="hljs-keyword">EXTENDS</span> Integers

<span class="hljs-keyword">CONSTANTS</span>
    <span class="hljs-comment">\* Size of the circular buffer.</span>
    <span class="hljs-comment">\* @type: Int;</span>
    BUFFER_SIZE,
    <span class="hljs-comment">\* The set of possible buffer elements.</span>
    <span class="hljs-comment">\* @type: Set(Int);</span>
    BUFFER_ELEMS

<span class="hljs-keyword">ASSUME</span> BUFFER_SIZE &gt; <span class="hljs-number">0</span>

<span class="hljs-keyword">VARIABLES</span>
    <span class="hljs-comment">\* The integer buffer of size BUFFER_SIZE.</span>
    <span class="hljs-comment">\* @type: Int -&gt; Int;</span>
    buffer,
    <span class="hljs-comment">\* Index of the next element to POP.</span>
    <span class="hljs-comment">\* @type: Int;</span>
    head,
    <span class="hljs-comment">\* Index of the next free slot for PUSH.</span>
    <span class="hljs-comment">\* @type: Int;</span>
    tail,
    <span class="hljs-comment">\* Number of elements currently stored.</span>
    <span class="hljs-comment">\* @type: Int;</span>
    count

<span class="hljs-comment">\* Initial state</span>
<span class="hljs-title function_">Init</span> <span class="hljs-operator">==</span>
  <span class="hljs-operator">/\</span> buffer <span class="hljs-operator">=</span> [ i <span class="hljs-keyword">\in</span> <span class="hljs-number">0</span><span class="hljs-operator">..</span>(BUFFER_SIZE <span class="hljs-operator">-</span> <span class="hljs-number">1</span>) |<span class="hljs-operator">-</span>&gt; <span class="hljs-number">0</span> ]
  <span class="hljs-operator">/\</span> head <span class="hljs-operator">=</span> <span class="hljs-number">0</span>
  <span class="hljs-operator">/\</span> tail <span class="hljs-operator">=</span> <span class="hljs-number">0</span>
  <span class="hljs-operator">/\</span> count <span class="hljs-operator">=</span> <span class="hljs-number">0</span>

<span class="hljs-comment">\* Buggy PUT: Advance tail, increment count, but no fullness check!</span>
Put(x) <span class="hljs-operator">==</span>
  Put::
  <span class="hljs-keyword">LET</span> nextTail <span class="hljs-operator">==</span> (tail <span class="hljs-operator">+</span> <span class="hljs-number">1</span>) % BUFFER_SIZE <span class="hljs-keyword">IN</span>
  <span class="hljs-operator">/\</span> buffer&#x27; <span class="hljs-operator">=</span> [buffer <span class="hljs-keyword">EXCEPT</span> ![tail] <span class="hljs-operator">=</span> x]
  <span class="hljs-operator">/\</span> head&#x27; <span class="hljs-operator">=</span> head
  <span class="hljs-operator">/\</span> tail&#x27; <span class="hljs-operator">=</span> nextTail
  <span class="hljs-operator">/\</span> count&#x27; <span class="hljs-operator">=</span> count <span class="hljs-operator">+</span> <span class="hljs-number">1</span>

<span class="hljs-comment">\* GET: Only allowed when count &gt; 0.</span>
<span class="hljs-title function_">Get</span> <span class="hljs-operator">==</span>
  Get::
  <span class="hljs-keyword">LET</span> nextHead <span class="hljs-operator">==</span> (head <span class="hljs-operator">+</span> <span class="hljs-number">1</span>) % BUFFER_SIZE <span class="hljs-keyword">IN</span>
  <span class="hljs-operator">/\</span> count &gt; <span class="hljs-number">0</span>
  <span class="hljs-operator">/\</span> <span class="hljs-keyword">UNCHANGED</span> buffer
  <span class="hljs-operator">/\</span> head&#x27; <span class="hljs-operator">=</span> nextHead
  <span class="hljs-operator">/\</span> tail&#x27; <span class="hljs-operator">=</span> tail
  <span class="hljs-operator">/\</span> count&#x27; <span class="hljs-operator">=</span> count <span class="hljs-operator">-</span> <span class="hljs-number">1</span>

<span class="hljs-comment">\* Either Put or Get may happen in any step.</span>
<span class="hljs-title function_">Next</span> <span class="hljs-operator">==</span>
    <span class="hljs-operator">\/</span> <span class="hljs-keyword">\E</span> x <span class="hljs-keyword">\in</span> BUFFER_ELEMS:
        Put(x)
    <span class="hljs-operator">\/</span> Get

<span class="hljs-title function_">vars</span> <span class="hljs-operator">==</span> &lt;&lt;buffer, head, tail, count&gt;&gt;

<span class="hljs-comment">\* Complete specification</span>
<span class="hljs-title function_">Spec</span> <span class="hljs-operator">==</span> Init <span class="hljs-operator">/\</span> [][Next]_vars

<span class="hljs-comment">\* Safety property we *intend* to hold, but it is violated:</span>
<span class="hljs-comment">\* count must never exceed the buffer capacity.</span>
<span class="hljs-title function_">SafeInv</span> <span class="hljs-operator">==</span> count <span class="hljs-operator">&lt;=</span> BUFFER_SIZE
</code></pre>
<p>Since I wanted to experiment with different buffer sizes and potential buffer
elements, I have introduced two parameters in the specification:</p>
<ul>
<li><code>BUFFER_SIZE</code> is the size of the cyclic buffer, and</li>
<li><code>BUFFER_ELEMS</code> is the set of possible buffer elements.</li>
</ul>
<p>Now, my previous experience with introducing TLA<sup>+</sup> to engineers
suggests that there are two ways to set these parameters:</p>
<ol>
<li>
<p><strong>The Engineer's way:</strong> Set the parameters to relatively small yet
reasonable values. For example, <code>BUFFER_SIZE = 10</code> and <code>BUFFER_ELEMS = 0..255</code>.
These are not the minimal possible values, but they kind of make sense: The
buffer should hold up to 10 bytes. Obviously, <code>BUFFER_ELEMS</code> are
set to the minimal possible type in their programming language of choice, e.g.,
<code>char</code> in C, or <code>u8</code> in Rust.</p>
</li>
<li>
<p><strong>The Verification Engineer's way:</strong> Start with the smallest possible values
of the parameters, e.g., <code>BUFFER_SIZE = 2</code> and <code>BUFFER_ELEMS = {0, 1}</code>. The
idea is to check the specification in the smallest possible scope first. If there
are no bugs found, increase the parameters gradually until you reach the
reasonable values.</p>
</li>
</ol>
<h3 id="1.2.-checking-the-specification-engineer's-way" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/small-scope.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#1.2.-checking-the-specification-engineer's-way">1.2. Checking the specification Engineer's way</a></h3>
<p>To check the specification the Engineer's way, I have created the
TLA<sup>+</sup> model <a href="https://github.com/konnov/cyclic-buffer-challenge/blob/main/tla/MC10u8_BuggyCircularBuffer.tla?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">MC10u8_BuggyCircularBuffer.tla</a> with <code>BUFFER_SIZE = 10</code>
and <code>BUFFER_ELEMS = 0..255</code>. For technical reasons, we also need the TLC config
file <a href="https://github.com/konnov/cyclic-buffer-challenge/blob/main/tla/MC.cfg?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">MC.cfg</a>. Follow the links to see the details.  Further, I've run TLC on
this model to check the invariant <code>SafeInv</code>:</p>
<pre><code class="hljs language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">java -<span class="hljs-built_in">cp</span> tla2tools.jar <span class="hljs-string">&quot;-XX:+UseParallelGC&quot;</span> tlc2.TLC \
  -config MC.cfg MC10u8_BuggyCircularBuffer.tla</span>
</code></pre>
<p>I wanted to see how far TLC could go, so I gave it a machine with 128 GB of RAM
and 32 cores. TLC has explored around 3 billion states in about 40 minutes.
After consuming 400 GB of disk space, it has run out of disk space and
terminated. No bug was found. Is this surprising? Not really. In this
configuration, TLC has to enumerate <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.566ex;" xmlns="http://www.w3.org/2000/svg" width="24.8ex" height="2.452ex" role="img" focusable="false" viewBox="0 -833.9 10961.7 1083.9"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="28" d="M94 250Q94 319 104 381T127 488T164 576T202 643T244 695T277 729T302 750H315H319Q333 750 333 741Q333 738 316 720T275 667T226 581T184 443T167 250T184 58T225 -81T274 -167T316 -220T333 -241Q333 -250 318 -250H315H302L274 -226Q180 -141 137 -14T94 250Z"></path></g><g data-mml-node="msup" transform="translate(389,0)"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g><g data-mml-node="mn" transform="translate(533,363) scale(0.707)"><path data-c="38" d="M70 417T70 494T124 618T248 666Q319 666 374 624T429 515Q429 485 418 459T392 417T361 389T335 371T324 363L338 354Q352 344 366 334T382 323Q457 264 457 174Q457 95 399 37T249 -22Q159 -22 101 29T43 155Q43 263 172 335L154 348Q133 361 127 368Q70 417 70 494ZM286 386L292 390Q298 394 301 396T311 403T323 413T334 425T345 438T355 454T364 471T369 491T371 513Q371 556 342 586T275 624Q268 625 242 625Q201 625 165 599T128 534Q128 511 141 492T167 463T217 431Q224 426 228 424L286 386ZM250 21Q308 21 350 55T392 137Q392 154 387 169T375 194T353 216T330 234T301 253T274 270Q260 279 244 289T218 306L210 311Q204 311 181 294T133 239T107 157Q107 98 150 60T250 21Z"></path></g></g><g data-mml-node="msup" transform="translate(1325.6,0)"><g data-mml-node="mo"><path data-c="29" d="M60 749L64 750Q69 750 74 750H86L114 726Q208 641 251 514T294 250Q294 182 284 119T261 12T224 -76T186 -143T145 -194T113 -227T90 -246Q87 -249 86 -250H74Q66 -250 63 -250T58 -247T55 -238Q56 -237 66 -225Q221 -64 221 250T66 725Q56 737 55 738Q55 746 60 749Z"></path></g><g data-mml-node="TeXAtom" transform="translate(422,363) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mn"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(500,0)"></path></g></g></g><g data-mml-node="mo" transform="translate(2726.9,0)"><path data-c="2217" d="M229 286Q216 420 216 436Q216 454 240 464Q241 464 245 464T251 465Q263 464 273 456T283 436Q283 419 277 356T270 286L328 328Q384 369 389 372T399 375Q412 375 423 365T435 338Q435 325 425 315Q420 312 357 282T289 250L355 219L425 184Q434 175 434 161Q434 146 425 136T401 125Q393 125 383 131T328 171L270 213Q283 79 283 63Q283 53 276 44T250 35Q231 35 224 44T216 63Q216 80 222 143T229 213L171 171Q115 130 110 127Q106 124 100 124Q87 124 76 134T64 161Q64 166 64 169T67 175T72 181T81 188T94 195T113 204T138 215T170 230T210 250L74 315Q65 324 65 338Q65 353 74 363T98 374Q106 374 116 368T171 328L229 286Z"></path></g><g data-mml-node="mn" transform="translate(3449.1,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(500,0)"></path></g><g data-mml-node="mo" transform="translate(4671.3,0)"><path data-c="2217" d="M229 286Q216 420 216 436Q216 454 240 464Q241 464 245 464T251 465Q263 464 273 456T283 436Q283 419 277 356T270 286L328 328Q384 369 389 372T399 375Q412 375 423 365T435 338Q435 325 425 315Q420 312 357 282T289 250L355 219L425 184Q434 175 434 161Q434 146 425 136T401 125Q393 125 383 131T328 171L270 213Q283 79 283 63Q283 53 276 44T250 35Q231 35 224 44T216 63Q216 80 222 143T229 213L171 171Q115 130 110 127Q106 124 100 124Q87 124 76 134T64 161Q64 166 64 169T67 175T72 181T81 188T94 195T113 204T138 215T170 230T210 250L74 315Q65 324 65 338Q65 353 74 363T98 374Q106 374 116 368T171 328L229 286Z"></path></g><g data-mml-node="mn" transform="translate(5393.5,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(500,0)"></path></g><g data-mml-node="mo" transform="translate(6615.8,0)"><path data-c="2217" d="M229 286Q216 420 216 436Q216 454 240 464Q241 464 245 464T251 465Q263 464 273 456T283 436Q283 419 277 356T270 286L328 328Q384 369 389 372T399 375Q412 375 423 365T435 338Q435 325 425 315Q420 312 357 282T289 250L355 219L425 184Q434 175 434 161Q434 146 425 136T401 125Q393 125 383 131T328 171L270 213Q283 79 283 63Q283 53 276 44T250 35Q231 35 224 44T216 63Q216 80 222 143T229 213L171 171Q115 130 110 127Q106 124 100 124Q87 124 76 134T64 161Q64 166 64 169T67 175T72 181T81 188T94 195T113 204T138 215T170 230T210 250L74 315Q65 324 65 338Q65 353 74 363T98 374Q106 374 116 368T171 328L229 286Z"></path></g><g data-mml-node="mn" transform="translate(7338,0)"><path data-c="31" d="M213 578L200 573Q186 568 160 563T102 556H83V602H102Q149 604 189 617T245 641T273 663Q275 666 285 666Q294 666 302 660V361L303 61Q310 54 315 52T339 48T401 46H427V0H416Q395 3 257 3Q121 3 100 0H88V46H114Q136 46 152 46T177 47T193 50T201 52T207 57T213 61V578Z"></path><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(500,0)"></path></g><g data-mml-node="mo" transform="translate(8615.8,0)"><path data-c="2248" d="M55 319Q55 360 72 393T114 444T163 472T205 482Q207 482 213 482T223 483Q262 483 296 468T393 413L443 381Q502 346 553 346Q609 346 649 375T694 454Q694 465 698 474T708 483Q722 483 722 452Q722 386 675 338T555 289Q514 289 468 310T388 357T308 404T224 426Q164 426 125 393T83 318Q81 289 69 289Q55 289 55 319ZM55 85Q55 126 72 159T114 210T163 238T205 248Q207 248 213 248T223 249Q262 249 296 234T393 179L443 147Q502 112 553 112Q609 112 649 141T694 220Q694 249 708 249T722 217Q722 153 675 104T555 55Q514 55 468 76T388 123T308 170T224 192Q164 192 125 159T83 84Q80 55 69 55Q55 55 55 85Z"></path></g><g data-mml-node="msup" transform="translate(9671.5,0)"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g><g data-mml-node="TeXAtom" transform="translate(533,363) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mn"><path data-c="39" d="M352 287Q304 211 232 211Q154 211 104 270T44 396Q42 412 42 436V444Q42 537 111 606Q171 666 243 666Q245 666 249 666T257 665H261Q273 665 286 663T323 651T370 619T413 560Q456 472 456 334Q456 194 396 97Q361 41 312 10T208 -22Q147 -22 108 7T68 93T121 149Q143 149 158 135T173 96Q173 78 164 65T148 49T135 44L131 43Q131 41 138 37T164 27T206 22H212Q272 22 313 86Q352 142 352 280V287ZM244 248Q292 248 321 297T351 430Q351 508 343 542Q341 552 337 562T323 588T293 615T246 625Q208 625 181 598Q160 576 154 546T147 441Q147 358 152 329T172 282Q197 248 244 248Z"></path><path data-c="30" d="M96 585Q152 666 249 666Q297 666 345 640T423 548Q460 465 460 320Q460 165 417 83Q397 41 362 16T301 -15T250 -22Q224 -22 198 -16T137 16T82 83Q39 165 39 320Q39 494 96 585ZM321 597Q291 629 250 629Q208 629 178 597Q153 571 145 525T137 333Q137 175 145 125T181 46Q209 16 250 16Q290 16 318 46Q347 76 354 130T362 333Q362 478 354 524T321 597Z" transform="translate(500,0)"></path></g></g></g></g></g></svg></mjx-container></span>
states. (Thanks to <a href="https://blltprf.xyz/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Thomas Pani</a> for correcting the initially wrong estimate.)</p>
<p>Obviously, anyone who used TLC for some time would have asked the same question
as Markus did:</p>
<p class="highlight-question"><strong><em>
  What about the small scope hypothesis? Can we use smaller parameters?
</em></strong></p>
<p>The answer to this question is basically the second approach, which I called the
Verification Engineer's way.</p>
<div class="tip-box">
  <div class="tip-header"><strong>Tip:</strong></div>
  <div class="tip"><p>Apalache finds an invariant violation in 3
seconds, when running bounded model checking with the command <code>check</code>.
However, I do not want to distract us from the main point of this blog post.</p>
</div>
</div>
<h3 id="1.3.-checking-the-specification-verification-engineer's-way" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/small-scope.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#1.3.-checking-the-specification-verification-engineer's-way">1.3. Checking the specification Verification Engineer's way</a></h3>
<p>This time, we use the instance <a href="https://github.com/konnov/cyclic-buffer-challenge/blob/main/tla/MC2u1_BuggyCircularBuffer.tla?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">MC2u1_BuggyCircularBuffer.tla</a>
that has <code>BUFFER_SIZE = 2</code> and <code>BUFFER_ELEMS = {0, 1}</code>.
Let's run TLC on this instance to check the invariant <code>SafeInv</code>:</p>
<pre><code class="hljs language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">java -<span class="hljs-built_in">cp</span> tla2tools.jar <span class="hljs-string">&quot;-XX:+UseParallelGC&quot;</span> tlc2.TLC \
  -config MC.cfg MC2u1_BuggyCircularBuffer.tla</span>
...
Error: Invariant SafeInv is violated.
...
10 states generated, 10 distinct states found, 5 states left on queue.
</code></pre>
<p>Yay! Just after visiting 10 states, TLC has found a violation of the invariant!</p>
<p>So if we pick the right small scope, exhaustive model checking with TLC finds
the bug quite fast. In this example, it is hard to find a small scope that would
not reveal the bug. Of course, when we know that the bug exists, it is easy to
experiment with different values of the parameters and find the bug.</p>
<h3 id="1.4.-checking-the-specification-randomly" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/small-scope.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#1.4.-checking-the-specification-randomly">1.4. Checking the specification randomly</a></h3>
<p>Surprisingly, if we forget about exhaustive enumeration, TLC finds an
invariant violation for <code>BUFFER_SIZE = 10</code> and <code>BUFFER_ELEMS = 0..255</code> in less
than a second. To do this, we run TLC with the option <code>-simulate</code>, which simply
picks successor states at random:</p>
<pre><code class="hljs language-shell"><span class="hljs-meta prompt_">$ </span><span class="language-bash">java -<span class="hljs-built_in">cp</span> tla2tools.jar <span class="hljs-string">&quot;-XX:+UseParallelGC&quot;</span> tlc2.TLC \
  -simulate -config MC.cfg MC10u8_BuggyCircularBuffer.tla</span>
...
Error: Invariant SafeInv is violated.
...
</code></pre>
<p>This effectiveness of randomized search is actually not a one-off thing.
The Quint simulator <a href="https://github.com/konnov/cyclic-buffer-challenge/tree/main/quint?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#randomized-simulation">finds the bug</a> in less than a second.
Similarly, the Rust property-based testing with <a href="https://github.com/konnov/cyclic-buffer-challenge/tree/main/rust/proptest?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">proptest</a> finds the bug
almost immediately.</p>
<p>Interestingly, <strong>we did not have to tune the scope to be as tiny as possible</strong>,
as we did for exhaustive model checking. Maybe this is why some engineers want
to use property-based testing for every problem?</p>
<h2 id="2.-thinking-about-the-small-scope-hypothesis" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/small-scope.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#2.-thinking-about-the-small-scope-hypothesis">2. Thinking about the small scope hypothesis</a></h2>
<p>In Example 1, we indeed found several assignments to <code>BUFFER_SIZE</code> and
<code>BUFFER_ELEMS</code> that revealed an invariant violation. Actually, this bug is so
simple that almost any assignment to the parameters would reveal it. We could
even set <code>BUFFER_SIZE = 1</code> and <code>BUFFER_ELEMS = {0}</code> to find the bug! If you want
to push it further, think, whether <code>BUFFER_ELEMS = {}</code> would allow us to find an
invariant violation.</p>
<p>In fact, if we go back to <a href="https://alloytools.org/?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">Alloy</a>, the way Alloy restricts the scope is quite
different from what we did in Example 1. Alloy limits the number of elements of
each type in the specification. For example, if we had specified the circular
buffer in Alloy, we could restrict the search scope as follows:</p>
<ul>
<li>
<p>All integers, including <code>BUFFER_SIZE</code> and buffer indices, have the bit width
of 4.</p>
</li>
<li>
<p>The number of unique buffer elements is <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: 0;" xmlns="http://www.w3.org/2000/svg" width="2.119ex" height="1.887ex" role="img" focusable="false" viewBox="0 -833.9 936.6 833.9"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msup"><g data-mml-node="mn"><path data-c="32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path></g><g data-mml-node="mn" transform="translate(533,363) scale(0.707)"><path data-c="38" d="M70 417T70 494T124 618T248 666Q319 666 374 624T429 515Q429 485 418 459T392 417T361 389T335 371T324 363L338 354Q352 344 366 334T382 323Q457 264 457 174Q457 95 399 37T249 -22Q159 -22 101 29T43 155Q43 263 172 335L154 348Q133 361 127 368Q70 417 70 494ZM286 386L292 390Q298 394 301 396T311 403T323 413T334 425T345 438T355 454T364 471T369 491T371 513Q371 556 342 586T275 624Q268 625 242 625Q201 625 165 599T128 534Q128 511 141 492T167 463T217 431Q224 426 228 424L286 386ZM250 21Q308 21 350 55T392 137Q392 154 387 169T375 194T353 216T330 234T301 253T274 270Q260 279 244 289T218 306L210 311Q204 311 181 294T133 239T107 157Q107 98 150 60T250 21Z"></path></g></g></g></g></svg></mjx-container></span>.</p>
</li>
</ul>
<p>As a result, Alloy would consider all possible values of <code>BUFFER_SIZE</code> from 0 to
15, all possible values of buffer elements from 0 to 255, as well as all
possible combinations of buffers of size up to 15. This is a much more flexible
way to restrict the search space.  In case of TLC, we did not have this
flexibility: We had to give concrete values to <code>BUFFER_SIZE</code> and <code>BUFFER_ELEMS</code>.</p>
<div class="tip-box">
  <div class="tip-header"><strong>Tip:</strong></div>
  <div class="tip"><p>Apalache has data generators, which are closer to
the Alloy scopes in spirit, though they work slightly different from Alloy.</p>
</div>
</div>
<p>Hence, we have to distinguish between small scopes and small parameter
assignments in TLC. After thinking about this question a bit more, I've asked
myself:</p>
<p class="highlight-question"><strong><em>
  Are there examples of specifications that have a small scope for a specific
  invariant violation, but it is hard to find concrete parameter assignments
  within this scope?
</em></strong></p>
<p>Even though my intuition says &quot;yes&quot;, there must be plenty of such examples, I
could not come up with with non-artificial examples immediately. On top of my
head, I can think of the following directions to look for such examples:</p>
<ul>
<li>
<p>Examples from <strong>abstract interpretation</strong>. If we have non-trivial math with
overflows and underflows, it might be hard to find concrete assignments that
would trigger these overflows and underflows.</p>
</li>
<li>
<p>Examples from <strong>graph theory</strong>. For instance, <a href="https://en.wikipedia.org/wiki/Planar_graph?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">non-planar graphs</a>
must contain subgraphs that are subdivisions of <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.375ex;" xmlns="http://www.w3.org/2000/svg" width="2.908ex" height="1.92ex" role="img" focusable="false" viewBox="0 -683 1285.6 848.6"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D43E" d="M285 628Q285 635 228 637Q205 637 198 638T191 647Q191 649 193 661Q199 681 203 682Q205 683 214 683H219Q260 681 355 681Q389 681 418 681T463 682T483 682Q500 682 500 674Q500 669 497 660Q496 658 496 654T495 648T493 644T490 641T486 639T479 638T470 637T456 637Q416 636 405 634T387 623L306 305Q307 305 490 449T678 597Q692 611 692 620Q692 635 667 637Q651 637 651 648Q651 650 654 662T659 677Q662 682 676 682Q680 682 711 681T791 680Q814 680 839 681T869 682Q889 682 889 672Q889 650 881 642Q878 637 862 637Q787 632 726 586Q710 576 656 534T556 455L509 418L518 396Q527 374 546 329T581 244Q656 67 661 61Q663 59 666 57Q680 47 717 46H738Q744 38 744 37T741 19Q737 6 731 0H720Q680 3 625 3Q503 3 488 0H478Q472 6 472 9T474 27Q478 40 480 43T491 46H494Q544 46 544 71Q544 75 517 141T485 216L427 354L359 301L291 248L268 155Q245 63 245 58Q245 51 253 49T303 46H334Q340 37 340 35Q340 19 333 5Q328 0 317 0Q314 0 280 1T180 2Q118 2 85 2T49 1Q31 1 31 11Q31 13 34 25Q38 41 42 43T65 46Q92 46 125 49Q139 52 144 61Q147 65 216 339T285 628Z"></path></g><g data-mml-node="mn" transform="translate(882,-150) scale(0.707)"><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z"></path></g></g></g></g></svg></mjx-container></span> or <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.65ex;" xmlns="http://www.w3.org/2000/svg" width="4.153ex" height="2.195ex" role="img" focusable="false" viewBox="0 -683 1835.7 970.2"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D43E" d="M285 628Q285 635 228 637Q205 637 198 638T191 647Q191 649 193 661Q199 681 203 682Q205 683 214 683H219Q260 681 355 681Q389 681 418 681T463 682T483 682Q500 682 500 674Q500 669 497 660Q496 658 496 654T495 648T493 644T490 641T486 639T479 638T470 637T456 637Q416 636 405 634T387 623L306 305Q307 305 490 449T678 597Q692 611 692 620Q692 635 667 637Q651 637 651 648Q651 650 654 662T659 677Q662 682 676 682Q680 682 711 681T791 680Q814 680 839 681T869 682Q889 682 889 672Q889 650 881 642Q878 637 862 637Q787 632 726 586Q710 576 656 534T556 455L509 418L518 396Q527 374 546 329T581 244Q656 67 661 61Q663 59 666 57Q680 47 717 46H738Q744 38 744 37T741 19Q737 6 731 0H720Q680 3 625 3Q503 3 488 0H478Q472 6 472 9T474 27Q478 40 480 43T491 46H494Q544 46 544 71Q544 75 517 141T485 216L427 354L359 301L291 248L268 155Q245 63 245 58Q245 51 253 49T303 46H334Q340 37 340 35Q340 19 333 5Q328 0 317 0Q314 0 280 1T180 2Q118 2 85 2T49 1Q31 1 31 11Q31 13 34 25Q38 41 42 43T65 46Q92 46 125 49Q139 52 144 61Q147 65 216 339T285 628Z"></path></g><g data-mml-node="TeXAtom" transform="translate(882,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mn"><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z"></path></g><g data-mml-node="mo" transform="translate(500,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="mn" transform="translate(778,0)"><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z"></path></g></g></g></g></g></svg></mjx-container></span> (see
Kuratowski's theorem on <a href="https://en.wikipedia.org/wiki/Planar_graph?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed">planar graphs</a>). So if a bug shows up only in
non-planar graphs, there must be a small scope that reveals the bug.  However,
our concrete graph would have to contain a subdivision of <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.375ex;" xmlns="http://www.w3.org/2000/svg" width="2.908ex" height="1.92ex" role="img" focusable="false" viewBox="0 -683 1285.6 848.6"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D43E" d="M285 628Q285 635 228 637Q205 637 198 638T191 647Q191 649 193 661Q199 681 203 682Q205 683 214 683H219Q260 681 355 681Q389 681 418 681T463 682T483 682Q500 682 500 674Q500 669 497 660Q496 658 496 654T495 648T493 644T490 641T486 639T479 638T470 637T456 637Q416 636 405 634T387 623L306 305Q307 305 490 449T678 597Q692 611 692 620Q692 635 667 637Q651 637 651 648Q651 650 654 662T659 677Q662 682 676 682Q680 682 711 681T791 680Q814 680 839 681T869 682Q889 682 889 672Q889 650 881 642Q878 637 862 637Q787 632 726 586Q710 576 656 534T556 455L509 418L518 396Q527 374 546 329T581 244Q656 67 661 61Q663 59 666 57Q680 47 717 46H738Q744 38 744 37T741 19Q737 6 731 0H720Q680 3 625 3Q503 3 488 0H478Q472 6 472 9T474 27Q478 40 480 43T491 46H494Q544 46 544 71Q544 75 517 141T485 216L427 354L359 301L291 248L268 155Q245 63 245 58Q245 51 253 49T303 46H334Q340 37 340 35Q340 19 333 5Q328 0 317 0Q314 0 280 1T180 2Q118 2 85 2T49 1Q31 1 31 11Q31 13 34 25Q38 41 42 43T65 46Q92 46 125 49Q139 52 144 61Q147 65 216 339T285 628Z"></path></g><g data-mml-node="mn" transform="translate(882,-150) scale(0.707)"><path data-c="35" d="M164 157Q164 133 148 117T109 101H102Q148 22 224 22Q294 22 326 82Q345 115 345 210Q345 313 318 349Q292 382 260 382H254Q176 382 136 314Q132 307 129 306T114 304Q97 304 95 310Q93 314 93 485V614Q93 664 98 664Q100 666 102 666Q103 666 123 658T178 642T253 634Q324 634 389 662Q397 666 402 666Q410 666 410 648V635Q328 538 205 538Q174 538 149 544L139 546V374Q158 388 169 396T205 412T256 420Q337 420 393 355T449 201Q449 109 385 44T229 -22Q148 -22 99 32T50 154Q50 178 61 192T84 210T107 214Q132 214 148 197T164 157Z"></path></g></g></g></g></svg></mjx-container></span> or <span class="math-inline"><mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.65ex;" xmlns="http://www.w3.org/2000/svg" width="4.153ex" height="2.195ex" role="img" focusable="false" viewBox="0 -683 1835.7 970.2"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="msub"><g data-mml-node="mi"><path data-c="1D43E" d="M285 628Q285 635 228 637Q205 637 198 638T191 647Q191 649 193 661Q199 681 203 682Q205 683 214 683H219Q260 681 355 681Q389 681 418 681T463 682T483 682Q500 682 500 674Q500 669 497 660Q496 658 496 654T495 648T493 644T490 641T486 639T479 638T470 637T456 637Q416 636 405 634T387 623L306 305Q307 305 490 449T678 597Q692 611 692 620Q692 635 667 637Q651 637 651 648Q651 650 654 662T659 677Q662 682 676 682Q680 682 711 681T791 680Q814 680 839 681T869 682Q889 682 889 672Q889 650 881 642Q878 637 862 637Q787 632 726 586Q710 576 656 534T556 455L509 418L518 396Q527 374 546 329T581 244Q656 67 661 61Q663 59 666 57Q680 47 717 46H738Q744 38 744 37T741 19Q737 6 731 0H720Q680 3 625 3Q503 3 488 0H478Q472 6 472 9T474 27Q478 40 480 43T491 46H494Q544 46 544 71Q544 75 517 141T485 216L427 354L359 301L291 248L268 155Q245 63 245 58Q245 51 253 49T303 46H334Q340 37 340 35Q340 19 333 5Q328 0 317 0Q314 0 280 1T180 2Q118 2 85 2T49 1Q31 1 31 11Q31 13 34 25Q38 41 42 43T65 46Q92 46 125 49Q139 52 144 61Q147 65 216 339T285 628Z"></path></g><g data-mml-node="TeXAtom" transform="translate(882,-150) scale(0.707)" data-mjx-texclass="ORD"><g data-mml-node="mn"><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z"></path></g><g data-mml-node="mo" transform="translate(500,0)"><path data-c="2C" d="M78 35T78 60T94 103T137 121Q165 121 187 96T210 8Q210 -27 201 -60T180 -117T154 -158T130 -185T117 -194Q113 -194 104 -185T95 -172Q95 -168 106 -156T131 -126T157 -76T173 -3V9L172 8Q170 7 167 6T161 3T152 1T140 0Q113 0 96 17Z"></path></g><g data-mml-node="mn" transform="translate(778,0)"><path data-c="33" d="M127 463Q100 463 85 480T69 524Q69 579 117 622T233 665Q268 665 277 664Q351 652 390 611T430 522Q430 470 396 421T302 350L299 348Q299 347 308 345T337 336T375 315Q457 262 457 175Q457 96 395 37T238 -22Q158 -22 100 21T42 130Q42 158 60 175T105 193Q133 193 151 175T169 130Q169 119 166 110T159 94T148 82T136 74T126 70T118 67L114 66Q165 21 238 21Q293 21 321 74Q338 107 338 175V195Q338 290 274 322Q259 328 213 329L171 330L168 332Q166 335 166 348Q166 366 174 366Q202 366 232 371Q266 376 294 413T322 525V533Q322 590 287 612Q265 626 240 626Q208 626 181 615T143 592T132 580H135Q138 579 143 578T153 573T165 566T175 555T183 540T186 520Q186 498 172 481T127 463Z"></path></g></g></g></g></g></svg></mjx-container></span>,
which is far from an arbitrary graph. Unfortunately, I do not know any
concurrent or distributed algorithm that would have something to do with
planar or non-planar graphs.</p>
</li>
</ul>
<h2 id="3.-your-turn" tabindex="-1"><a class="header-anchor" href="https://protocols-made-fun.com/small-scope.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#3.-your-turn">3. Your turn</a></h2>
<p>It is your turn to decide how this blog post should continue. If someone gives
me an interesting example or insight in a <a href="https://protocols-made-fun.com/small-scope.html?utm_source=protocols_made_fun&amp;utm_medium=feed&amp;utm_campaign=pmf_feed#end">comment</a>, I will
update this blog post accordingly.</p>
<!-- references -->
]]></content>
    <author>
      <name>Igor Konnov</name>
      <email>igor@konnov.phd</email>
      <uri>https://konnov.phd/?utm_source=protocols_made_fun&amp;utm_medium=referral&amp;utm_campaign=pmf_site</uri>
    </author>
    
    
    <category term="tlaplus" />
    
    
    
  </entry>
  
</feed>
