Skip to content

Instantly share code, notes, and snippets.

@zmanian
Last active May 20, 2026 04:41
Show Gist options
  • Select an option

  • Save zmanian/14d77cf1deda43a5b889bc22f8f0cfaf to your computer and use it in GitHub Desktop.

Select an option

Save zmanian/14d77cf1deda43a5b889bc22f8f0cfaf to your computer and use it in GitHub Desktop.
Crosslink Quint specs for nil-precommit resampling, fork finality, dynamic sigma, telemetry, and accountability witnesses

Crosslink Quint Specs

This directory contains focused Quint models for Crosslink/Tenderlink round recovery and Crosslink finality value semantics.

CrosslinkResampling.qnt is not a full Tenderlink implementation spec yet. It isolates the Ebb-and-Flow-specific question that came up during fork recovery design:

  • Crosslink proposals sample a moving PoW stream, modelled as Stream(round).
  • A 2f + 1 PRECOMMIT nil quorum is modelled as a round-abandon certificate.
  • The sticky model keeps a same-round Crosslink proposal cache across the round increment and preserves same-round Tendermint lock/valid state.
  • The resampling model treats the nil precommit quorum as an unlock certificate for the abandoned round: it clears the same-round proposal cache, validValue, and lockedValue, so the next proposer can sample the current stream.

The model preserves older Tendermint locks. A nil precommit certificate only clears state whose round is exactly the abandoned round; it does not erase earlier safety-carrying locks. It also keeps the Tendermint quorum-intersection argument explicit: retained locks must be backed by value precommit evidence, and a nil certificate can coexist with at most f correct same-round value locks, not with a commit-capable value-lock quorum. The model also ports the upstream Tendermint accountability shape over the Crosslink evidence surface: transition-carried proposal, prevote, and precommit evidence feed equivocation and amnesia predicates, while a nil-precommit certificate for the abandoned round is treated as valid unlock evidence rather than amnesia.

CrosslinkBaseline.qnt packages the current fixed-sigma/sticky behavior as a named baseline variant. It reuses the focused Tenderlink model with ResampleOnNilPrecommit = false, then separates the positive baseline witness from the known stream-change limitation: a stable stream decides through the normal propose/prevote/precommit path, while a changed stream carries the stale sample and can block a fresh decision because same-round Tendermint state is not cleared.

CrosslinkBaselineTenderlink.qnt gives that baseline an upstream-shaped parameter shell: BaselineCorr, BaselineFaulty, BaselineN, BaselineT, valid and invalid snapshot sets, round/proposer parameters, and the Crosslink-specific fixed-sigma rule BaselineStream(round) = ancestor(bestTip(round), height(bestTip(round)) - sigma). It now exposes the full faulty-init evidence surface through BaselineInitWithFaultyEvidence, BaselineFaultyInitDomainWellFormed, and BaselineFaultyInitSafety shell names, plus BaselineNext as the baseline-prefixed transition step. It also exposes upstream-shaped baseline-prefixed aliases for round initialization, proposal handling, prevote and precommit quorum handlers, timeout paths, nil/stream-change precommit paths, round advance, catchup, decision, and state/quorum accessors so the shell can test those transition surfaces directly. It still delegates the core sticky transition semantics to the focused model, so a full upstream-identical Tendermint transition port remains future work.

CrosslinkBaselineModels.qnt adds small baseline instances over that shell, including stable and forking n4_f1 fixtures, a forking n5_f1 fixture, above-live-boundary n4_f2/n5_f2 fixtures, and a proper n7_f2 fixture. The f = 2 witnesses distinguish cases where correct validators cannot form a 2f+1 value quorum from the n7_f2 case where the normal decision path remains available.

CrosslinkBaselineTest.qnt adds upstream-style smoke tests for the baseline shell: parameterized faulty-init coverage, fixed-sigma sampling, normal decision, no double proposal, StartRound-style round initialization, nil prevote quorum precommit-nil handling, the split nil-valid-round and concrete-valid-round proposal handlers, timeout-driven nil votes and round advance, future-round catchup, deriving a fresh head - sigma value after a fork switch, and the sticky baseline witness that still carries the stale fixed-sigma sample. The forking baseline test also includes a .fail() witness for the false claim that every proposal remains the current fixed-sigma sample after a stream switch.

CrosslinkBaselineAccountability.qnt makes the baseline accountability projection explicit. It checks that a nil-precommit certificate does not clear same-round value locks in the sticky baseline, and that conflicting value commits without unlock evidence expose Tendermint-style amnesia evidence. It also includes focused faulty-evidence witnesses showing that faulty proposals, prevotes, and nil/value precommit conflicts are carried into the same equivocation predicates used by the accountability model. Its tiny faulty-init model adapts the upstream Tendermint pattern of nondeterministically injecting faulty proposal, prevote, and precommit powersets in the initial state, while keeping the instance small enough for the baseline proof gate. A second faulty-init model lifts the same idea into the fixed-sigma/forking n4_f1 parameter surface with a bounded faulty-evidence domain so symbolic checking remains tractable. Representative bounded n4_f2, n5_f2, and n7_f2 faulty-init models add tractable f=2 symbolic gates without using the full powerset. The single-faulty n4_f2 model then ranges over one arbitrary faulty proposal, prevote, and precommit from the full n4_f2 domain, giving a stronger symbolic abstraction than the representative fixtures while still avoiding the full-powerset blowup. The pair-faulty n4_f2 model extends that tractable symbolic abstraction to two arbitrary faulty proposals, prevotes, and precommits from the same full domain. The triple-faulty n4_f2 model extends the same tractable frontier to three arbitrary faulty proposals, prevotes, and precommits while avoiding the full powerset that exhausted Apalache in local probes. Single-, pair-, and triple-faulty n5_f2 models carry the full-domain symbolic abstraction into the larger above-live-boundary f=2 surface where correct validators can form f+1 catchup evidence but not a 2f+1 value quorum. Additional quick-check harnesses run the full faulty-evidence domain over the fixed-sigma/forking n4_f1, n4_f2, n5_f1, n5_f2, and proper n7_f2 BFT-boundary surfaces so larger parameterized instances are exercised without adding those full powersets to the symbolic gate. The same file also includes counterexample tests for false agreement, amnesia, equivocation, no-conflicting-commit, agreement-or-amnesia, amnesia-implies-equivocation, amnesia-without-equivocation, and undecided max-round claims, so the accountability predicates are checked against known-bad assertions.

CrosslinkBaselineBftHeights.qnt gives the baseline variant a named heighted-finality model. It checks that fixed-sigma finality advances through consecutive BFT consensus heights while rejecting skipped consensus heights and fork decisions after a prefix is final, and includes a .fail() witness for the false claim that a fork-finality attempt remains valid after prefix finality.

CrosslinkBaselineFinality.qnt composes that baseline Tenderlink behavior with Crosslink finality. It shows that the fixed-sigma/sticky protocol can finalize a tail-confirmed stable-stream decision, and records the Crosslink-level failure mode when a stream change leaves the protocol carrying the stale sample while finality remains at the prior prefix.

CrosslinkBaselinePowSampling.qnt pins the baseline stream to an explicit fixed head - sigma PoW sample. It derives Stream(round) from a best-tip schedule and ancestor map, then shows that a fork switch can roll back the previous fixed-sigma sample while the sticky baseline still carries that sample into the next round. It also includes a long-reorg fixture where rollback depth exceeds sigma, a generated work-competition fixture where an adversarially released branch becomes the selected best tip, and a repeated generated work-competition fixture with two adversarial fork switches.

baseline-completeness-audit.md maps the current baseline artifacts against the upstream Tendermint Quint example and tracks the remaining work before the baseline reaches upstream-style completeness rather than focused witness coverage.

baseline-upstream-crosswalk.md is the line-item map from the current upstream Tendermint Quint surface to the baseline Crosslink artifacts. Use it as the working checklist for deciding whether a baseline change is closing an upstream completeness gap, documenting an intentional Crosslink deviation, or adding a Crosslink-only proof obligation.

CrosslinkForkFinality.qnt is a separate value-semantics model. It abstracts PoW snapshots as a finite fork tree, then checks that Crosslink finality can skip heights on one branch while rejecting finalization of a fork after a block is final.

CrosslinkPowForkSchedule.qnt is the first step from fixed PoW fixtures toward generated reorg schedules. It derives rollback depth from a bounded sequence of best-tip changes, then checks whether a selected sigma is deep enough to survive that fork switch.

CrosslinkPowBranchCompetition.qnt replaces the hand-declared best-tip switch with a bounded branch-competition fixture. Published tips compete by honest plus adversarial work, a hidden adversarial branch cannot become best until it is published, and releasing an outworking adversarial branch derives the same rollback-depth signal used by dynamic sigma.

CrosslinkComposed.qnt connects those two pieces: a Tenderlink decision over a resampled PoW snapshot becomes the input to Crosslink finality, which can then advance to a tail-confirmed snapshot while preserving the finalized prefix.

CrosslinkBftHeights.qnt adds the missing BFT-height dimension for finality. It checks that successive Tenderlink decisions at consecutive consensus heights can update Crosslink finality directly, while rejecting skipped consensus heights and fork decisions after a prefix is final.

CrosslinkDynamicSigma.qnt sketches the third Crosslink variant: a dynamic-sigma controller. It treats the percentage of total PoW hash power that is participating in Crosslink as an explicit controller input. Low hash-power participation raises the minimum sigma floor because the finalizers' observed PoW stream is less representative of the global longest-chain race. Round failures can still raise sigma, but they are not the only signal; below a critical hash-participation threshold, the model forces the maximum sigma and marks the controller state as degraded. The bounded fixture also makes the head - sigma sample explicit: base sigma sees adjacent tip churn, while a raised sigma samples a deeper ancestor that remains stable across the same round boundary. The controller also accepts an observed reorg-depth schedule: if an adversarial or accidental reorg reaches the current depth, sigma moves to the next configured floor even when hash participation is healthy and the previous BFT round did not fail. It also includes a bounded stochastic-risk score over hash-power coverage, recent round-failure rate, block-interval variance, and observed rollback depth; the score can raise sigma when combined signals become risky even if no single hard floor fires.

CrosslinkDynamicSigmaCalibration.qnt gives the dynamic-sigma controller a bounded calibration contract. It treats hash-power participation, round-failure rate, block-interval variance, and observed reorg depth as measured windows, then checks that the selected risk weights and thresholds classify those windows into the expected sigma floors.

CrosslinkDynamicSigmaTelemetry.qnt makes that calibration contract more production-shaped. It derives participation from Crosslink-participating PoW work over total observed PoW work, requires conservative coverage and round-failure estimates, and adds an explicit acceptable rollback-risk target plus expected-loss budget that the selected sigma must satisfy whenever the configured ladder can satisfy both.

CrosslinkDynamicSigmaHysteresis.qnt models the stability policy that should sit on top of those telemetry-derived floors. Worsening evidence, including a drop in Crosslink-participating hash power, can raise sigma immediately. Improving evidence lowers sigma only after enough stable lower-risk windows, and then only one ladder step at a time.

dynamic-sigma-telemetry-integration.md maps those telemetry inputs to production data sources and documents the consensus-safety requirements before a deployed controller can replace the prototype's fixed sigma parameter. zebra-crosslink/src/dynamic_sigma.rs is the matching pure Rust controller prototype: it derives conservative coverage and round-failure estimates from raw counters, validates telemetry windows, and selects the same sigma floor as the Quint telemetry fixture. It also includes a proposal-carried evidence verifier that rejects selected sigma values below the controller-required floor. The pure module now exposes proposal-evidence selection helpers for raw telemetry and production-shaped components, with optional hysteresis, and the Tenderlink prototype path calls those helpers rather than duplicating controller selection logic. DynamicSigmaTelemetryComponents and DynamicSigmaRoundCounters are the first production-shaped assembly boundary: they require explicit total and Crosslink-participating hash work, reject inconsistent round counters, and only then build raw controller telemetry. DynamicSigmaRoundEvent accumulates started, decided, nil-precommit, stale-proposal, timeout, invalid-proposal, and mixed-evidence labels into those counters, while rejecting failure-reason overcounts. observed_hash_work_participation aggregates source-side PoW work observations into the total-work denominator and verified-participating numerator, so work without objective Crosslink participation evidence is counted conservatively as non-participating. The regression tests now include a skewed window where fewer high-work non-participating observations outweigh more participating observations, ensuring the sigma input is percentage of hash power rather than observation count. DynamicSigmaDecision also exposes the same work-threshold view as a conservative lower-bound participation percentage, rounded down so diagnostics cannot overstate threshold coverage. hash_work_observation_from_header and hash_work_observations_from_headers now derive that source signal from PoW headers by converting compact difficulty into work and treating the current non-null Crosslink fat pointer as the objective participation marker. This keeps the dynamic-sigma input as a work-weighted participating hash-power percentage, and the _with_verifier helpers let a production source require stricter fat-pointer validation before non-null marker work enters the participating numerator. DynamicSigmaHashWorkObservationWindowPolicy adds source-window discipline for that percentage by requiring enough recent observations and enough total observed work before deriving the numerator/denominator pair. DynamicSigmaHeaderObservationWindow lets callers provide headers directly alongside Tenderlink round counters, best-tip transitions, variance telemetry, rollback-risk estimates, and economic exposure inputs, then builds the same telemetry components as the lower-level hash-work observation path. telemetry_components_from_header_observation_window_with_hash_work_policy applies the same recent-window and minimum-work guardrail to header-derived participation, so production callers do not have to bypass the policy when the source signal is PoW headers. DynamicSigmaTimedHeaderObservationWindow derives the variance input from the same header window by comparing adjacent header timestamps against an expected target spacing, rejecting too-short or non-increasing windows and capping the conservative deviation percentage at 100. Its hash-work-policy helper composes that timestamp-derived variance with the same guarded header participation path. target_block_spacing_seconds_from_network_upgrade and target_block_spacing_seconds_for_height derive the expected spacing from Zebra's network-upgrade schedule for production-shaped timed windows. DynamicSigmaEconomicExposurePolicy makes the economic-risk boundary explicit: consensus-critical exposure carries value-at-risk and loss-budget units into proposal evidence, while service-local exposure maps to zero consensus exposure and cannot silently change validator validity. DynamicSigmaBestTipTransition derives the observed reorg-depth input from explicit old-tip, new-tip, and common-ancestor heights, with a helper for taking the maximum rollback depth over a transition window. DynamicSigmaBestTipTransitionRecorder gives live state hooks a small state-machine boundary: seed the first observed best tip, require a common ancestor after that, and reject invalid transition evidence without advancing. rollback_depth_history_from_transition_windows turns a sequence of transition windows into one rollback-depth sample per measurement window, giving live state hooks a pure source target for rollback-risk history. rollback_risk_curve_from_observed_rollback_depths derives a deterministic empirical RollbackRiskCurve by measuring how often observed rollback-depth windows reach each sigma in the ladder, then adding a bounded conservative ppm margin. DynamicSigmaRollbackRiskWindowPolicy wraps that estimator with minimum-history and maximum-recent-window bounds so old rollback events do not silently dominate current sigma selection. telemetry_components_from_observation_window composes these source-shaped hash-work observations, round counters, and best-tip transitions into telemetry components before proposal evidence selection. The branch also includes a BftBlock::try_from_with_confirmation_depth construction hook and tagged payload envelope. The live Tenderlink proposal, validation, and decided-block callbacks now route through a config-aware payload path: default config still emits and accepts the legacy fixed-sigma BftBlock, while dynamic_sigma_prototype emits and validates the tagged dynamic-sigma envelope using shared prototype parameters and proposal-carried evidence. The decoded payload carries its selected confirmation depth into voting-time stale checks, so a prototype dynamic proposal is checked against head - selected_sigma. The prototype proposer now builds telemetry from a fixture timed-header source window, applies the hash-work policy, assembles telemetry components, and then runs the dynamic-sigma controller before selecting sigma; production telemetry sources are still the missing deployment step. Invalid source observation assembly, telemetry assembly, or telemetry-to-evidence selection prevents prototype proposal emission instead of falling back to the base sigma.

CrosslinkDynamicSigmaForkSchedule.qnt composes the dynamic-sigma controller with the derived PoW fork schedule. In this model, dynamic sigma consumes rollback depth computed from best-tip transitions instead of a supplied ObservedReorgDepth map.

CrosslinkDynamicSigmaBranchCompetition.qnt feeds the generated PoW branch-competition model into dynamic sigma. The controller now consumes a rollback-depth signal produced by published-tip work competition, including the adversarial branch release witness.

CrosslinkDynamicSigmaResampling.qnt composes the derived fork signal with the nil-precommit resampling path. It checks that a fork switch can raise sigma before validators advance the abandoned Tenderlink round, and that the resampling path can still decide the fresh stream value. It also carries the hash-power participation floor into the composed model, so low participation by Crosslink-aware miners can raise sigma even when the latest best-tip transition does not add a new fork-switch signal. Its best-tip fixture is backed by generated published-tip work competition.

CrosslinkDynamicSigmaFinality.qnt composes dynamic sigma, nil-precommit resampling, and Crosslink finality. It uses the live dynSigma value as the tail-confirmation depth for finality, so fork-derived and hash-participation sigma increases both delay finalization until the fresh decision is confirmed deeply enough. Its fork signal is now backed by generated published-tip work competition, and finality advances at explicit BFT consensus heights.

Upstream Base

The best current Tendermint Quint base is the Quint repository's Cosmos example:

https://github.com/informalsystems/quint/tree/main/examples/cosmos/tendermint

That example is a Quint port of the CometBFT accountability spec. The older informalsystems/tendermint-spec repository is useful background, but its CSMI library does not typecheck cleanly under Quint 0.31.0.

Toolchain

Quint's command-line package is still a JavaScript CLI, but the simulator uses the Rust backend with --backend=rust. On this machine, Node 26.0.0 exposes a yargs packaging issue in the globally installed Quint CLI, so the working local command is:

QUINT="node /private/tmp/quint-node26-patched-validround/dist/src/cli.js"

If the global CLI works in your shell, use quint instead.

Apalache verification also needs Java on the path. The working local environment is:

export HOME=/private/tmp/quint-home2
export JAVA_HOME=/opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home
export PATH=/opt/homebrew/opt/openjdk/bin:$PATH

Apalache starts a local checker server during quint verify. By default Quint uses port 8822; the local wrapper also accepts APALACHE_PORT_BASE to give each symbolic check a sequential port when a long run would otherwise collide with a lingering checker process.

Checks

Run the baseline Crosslink sweep:

QUINT="$QUINT" spec/quint/check.sh quick-baseline
QUINT="$QUINT" JVM_ARGS=-Xmx8192m spec/quint/check.sh symbolic-baseline

This typechecks the shared sticky Tenderlink model plus the named baseline Crosslink specs, runs the Rust witness tests, runs the Rust safety checks, and then runs the bounded Apalache checks for the baseline-only proof obligations. The CI workflow runs the symbolic baseline gate as two parallel slices: symbolic-baseline-core for the non-accountability finality/PoW checks and symbolic-baseline-accountability for the heavier faulty-evidence checks.

Run the quick local sweep:

QUINT="$QUINT" spec/quint/check.sh quick

Run the bounded Apalache sweep:

QUINT="$QUINT" JVM_ARGS=-Xmx8192m spec/quint/check.sh symbolic

For faster iteration on only the fixed-sigma/sticky baseline variant, use either half of that baseline sweep:

QUINT="$QUINT" spec/quint/check.sh quick-baseline
QUINT="$QUINT" JVM_ARGS=-Xmx8192m spec/quint/check.sh symbolic-baseline
QUINT="$QUINT" JVM_ARGS=-Xmx8192m spec/quint/check.sh symbolic-baseline-core
QUINT="$QUINT" JVM_ARGS=-Xmx8192m spec/quint/check.sh symbolic-baseline-accountability

If a local symbolic run reports an Apalache port collision, rerun with a fresh base port:

QUINT="$QUINT" JVM_ARGS=-Xmx8192m APALACHE_PORT_BASE=8830 \
  spec/quint/check.sh symbolic-baseline

Typecheck:

$QUINT typecheck spec/quint/CrosslinkBaseline.qnt
$QUINT typecheck spec/quint/CrosslinkBaselineTenderlink.qnt
$QUINT typecheck spec/quint/CrosslinkBaselineModels.qnt
$QUINT typecheck spec/quint/CrosslinkBaselineTest.qnt
$QUINT typecheck spec/quint/CrosslinkBaselineAccountability.qnt
$QUINT typecheck spec/quint/CrosslinkBaselineBftHeights.qnt
$QUINT typecheck spec/quint/CrosslinkBaselineFinality.qnt
$QUINT typecheck spec/quint/CrosslinkBaselinePowSampling.qnt
$QUINT typecheck spec/quint/CrosslinkResampling.qnt
$QUINT typecheck spec/quint/CrosslinkForkFinality.qnt
$QUINT typecheck spec/quint/CrosslinkPowForkSchedule.qnt
$QUINT typecheck spec/quint/CrosslinkPowBranchCompetition.qnt
$QUINT typecheck spec/quint/CrosslinkComposed.qnt
$QUINT typecheck spec/quint/CrosslinkBftHeights.qnt
$QUINT typecheck spec/quint/CrosslinkDynamicSigma.qnt
$QUINT typecheck spec/quint/CrosslinkDynamicSigmaCalibration.qnt
$QUINT typecheck spec/quint/CrosslinkDynamicSigmaTelemetry.qnt
$QUINT typecheck spec/quint/CrosslinkDynamicSigmaHysteresis.qnt
$QUINT typecheck spec/quint/CrosslinkDynamicSigmaForkSchedule.qnt
$QUINT typecheck spec/quint/CrosslinkDynamicSigmaBranchCompetition.qnt
$QUINT typecheck spec/quint/CrosslinkDynamicSigmaResampling.qnt
$QUINT typecheck spec/quint/CrosslinkDynamicSigmaFinality.qnt

Witness the named baseline behavior:

$QUINT test spec/quint/CrosslinkBaseline.qnt \
  --main=CrosslinkBaselineStableModel \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaseline.qnt \
  --main=CrosslinkBaselineStreamChangeModel \
  --max-samples=100 \
  --backend=rust

The stable model checks the fixed-sigma/sticky protocol can decide its sampled snapshot when the PoW stream does not move. The stream-change model records the baseline limitation: after a nil-precommit quorum, the current protocol carries the stale sample into the next round and same-round Tendermint state can block a fresh decision.

Witness the upstream-shaped baseline shell:

$QUINT test spec/quint/CrosslinkBaselineTest.qnt \
  --main=CrosslinkBaselineParameterizedShellTest \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselineTest.qnt \
  --main=CrosslinkBaselineN4F1StableTest \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselineTest.qnt \
  --main=CrosslinkBaselineN4F1ForkingTest \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselineTest.qnt \
  --main=CrosslinkBaselineN4F2ForkingTest \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselineTest.qnt \
  --main=CrosslinkBaselineN5F2ForkingTest \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselineTest.qnt \
  --main=CrosslinkBaselineN7F2ForkingTest \
  --max-samples=100 \
  --backend=rust

The stable n4_f1 test checks fixed-sigma sampling, normal decision, no-double-proposal behavior, the normal Tendermint transition from a nil prevote quorum to nil precommits, and the valid-round proposal path. The valid-round witnesses reject a proposal whose validRound has no supporting prevote quorum, then accept the same shape once round 0 really has a 2f+1 prevote quorum for the proposed value. The model also exposes the upstream-shaped split between UponProposalInPropose for nil-valid-round proposals and UponProposalInProposeAndPrevote for proposals justified by an earlier prevote quorum; Next uses that split. CorrectValuePrevotesHaveJustifiedProposal is also part of Safety, so every correct non-nil prevote must have a matching HasPrevoteJustifiedProposal witness. The forking n4_f1 test checks that the shell derives the fresh head - sigma sample after a fork switch while the sticky baseline still carries the old sample into the next round. The parameterized shell also exposes late nil-precommit certificate handling and checks that a real nil-precommit certificate remains disabled as unlock evidence under sticky baseline semantics. The f = 2 tests record that n4_f2 and n5_f2 sit above the live fault boundary for correct-only value commits, while n7_f2 still supports a 2f+1 correct decision path. These three f = 2 safety invariants are also included in symbolic-baseline at max depth 3. The n7_f2 instance is materially more expensive than the smaller boundary instances, but it remains tractable at this depth.

Witness the named baseline accountability behavior:

$QUINT test spec/quint/CrosslinkBaselineAccountability.qnt \
  --main=CrosslinkBaselineAccountabilityModel \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselineAccountability.qnt \
  --main=CrosslinkBaselineFaultyInitTinyModel \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselineAccountability.qnt \
  --main=CrosslinkBaselineFaultyInitForkingModel \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselineAccountability.qnt \
  --main=CrosslinkBaselineBoundedFaultyInitN4F2ForkingModel \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselineAccountability.qnt \
  --main=CrosslinkBaselineSingleFaultyInitN4F2ForkingModel \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselineAccountability.qnt \
  --main=CrosslinkBaselinePairFaultyInitN4F2ForkingModel \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselineAccountability.qnt \
  --main=CrosslinkBaselineTripleFaultyInitN4F2ForkingModel \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselineAccountability.qnt \
  --main=CrosslinkBaselineBoundedFaultyInitN5F2ForkingModel \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselineAccountability.qnt \
  --main=CrosslinkBaselineSingleFaultyInitN5F2ForkingModel \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselineAccountability.qnt \
  --main=CrosslinkBaselinePairFaultyInitN5F2ForkingModel \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselineAccountability.qnt \
  --main=CrosslinkBaselineTripleFaultyInitN5F2ForkingModel \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselineAccountability.qnt \
  --main=CrosslinkBaselineBoundedFaultyInitN7F2ForkingModel \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselineAccountability.qnt \
  --main=CrosslinkBaselineFullFaultyInitForkingModel \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselineAccountability.qnt \
  --main=CrosslinkBaselineFullFaultyInitN5F1ForkingModel \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselineAccountability.qnt \
  --main=CrosslinkBaselineFullFaultyInitN7F2ForkingModel \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselineAccountability.qnt \
  --main=CrosslinkBaselineCounterexampleModel \
  --max-samples=100 \
  --backend=rust

This model checks that baseline nil precommit preserves same-round value-lock state and that conflicting value commits without nil-unlock evidence are accountable through the existing amnesia predicates. It also checks that faulty proposal, prevote, and nil/value precommit evidence feeds the equivocation detector. The parameterized shell test checks that the upstream-shaped baseline shell exposes the full faulty-init evidence surface and transition step through baseline-prefixed aliases before the focused accountability witnesses exercise smaller and larger instances. The tiny faulty-init model checks the upstream-style InitWithFaultyEvidence path with nondeterministic faulty message powersets. The forking faulty-init model checks a larger fixed-sigma/forking baseline instance with bounded faulty proposal, prevote, and precommit powersets. The bounded n4/f2, n5/f2, and n7/f2 faulty-init models check representative f=2 faulty-message domains in the symbolic gate, preserving the same fixed-sigma/forking value rule without attempting the full powerset. The n5/f2 single-, pair-, and triple-faulty models then range over one, two, or three arbitrary faulty proposals, prevotes, and precommits from the complete n5/f2 faulty-message domain while remaining tractable for the symbolic baseline gate. The full forking faulty-init model keeps the complete faulty proposal, prevote, and precommit powerset domain alive as a Rust-backed quick check for the same n4_f1 parameter surface. The n4/f2 full forking faulty-init model applies the same full-domain quick check to the above-live-boundary f=2 surface where correct validators cannot form f+1 catchup evidence or a 2f+1 value quorum. The n5/f1 full forking faulty-init model extends the same full-domain quick check to the intermediate one-fault validator surface. The n5/f2 full forking faulty-init model applies the same full-domain quick check to the above-live-boundary f=2 surface where correct validators can form f+1 catchup evidence but not a 2f+1 value quorum. The n7/f2 full forking faulty-init model applies the full-domain quick check to the proper f=2 BFT-boundary surface while the symbolic gate uses the representative bounded domain. The counterexample model uses .fail() witnesses for false no-conflicting-commit, no-amnesia, no-equivocation, agreement, agreement-or-amnesia, amnesia-implies-equivocation, amnesia-without-equivocation, undecided max-round, and sticky-baseline nil-precommit unlock claims.

Witness baseline BFT-heighted finality:

$QUINT test spec/quint/CrosslinkBaselineBftHeights.qnt \
  --main=CrosslinkBaselineBftHeightsModel \
  --max-samples=100 \
  --backend=rust

This model checks that fixed-sigma baseline finality advances at consecutive BFT heights, rejects skipped BFT heights, and rejects finalizing a fork after a prefix is final. It also includes a .fail() witness for the false claim that the fork-finality attempt is valid after prefix finality.

Witness the named baseline finality behavior:

$QUINT test spec/quint/CrosslinkBaselineFinality.qnt \
  --main=CrosslinkBaselineFinalityStableModel \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselineFinality.qnt \
  --main=CrosslinkBaselineFinalityStreamChangeModel \
  --max-samples=100 \
  --backend=rust

The stable finality model checks the current fixed-sigma/sticky protocol can decide and finalize a tail-confirmed sampled snapshot when the PoW stream is stable. The stream-change finality model records the Crosslink-level limitation: after a nil-precommit quorum, the current protocol can still carry the stale sample and leave finality at the previous prefix instead of reaching the fresh stream value.

Witness fixed-sigma baseline PoW sampling:

$QUINT test spec/quint/CrosslinkBaselinePowSampling.qnt \
  --main=CrosslinkBaselinePowSamplingModel \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselinePowSampling.qnt \
  --main=CrosslinkBaselinePowLongReorgModel \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselinePowSampling.qnt \
  --main=CrosslinkBaselinePowGeneratedScheduleModel \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselinePowSampling.qnt \
  --main=CrosslinkBaselinePowRepeatedGeneratedScheduleModel \
  --max-samples=100 \
  --backend=rust

$QUINT test spec/quint/CrosslinkBaselinePowSampling.qnt \
  --main=CrosslinkBaselinePowStochasticProductionModel \
  --max-samples=100 \
  --backend=rust

This model makes Stream(round) equal to the explicit fixed head - sigma ancestor. Its fork-switch fixture moves the best tip from a4 to b4, derives round samples a3 and b3, and checks that the sticky baseline still proposes the rolled-back a3 sample in round 1. The long-reorg fixture raises sigma to 2, moves the best tip from a5 to c5 with common ancestor a2, derives rollback depth 3, and checks the same sticky stale-sample behavior for a3 versus the fresh c3 sample. The generated-schedule fixture derives best tips from published honest and adversarial work: a3, then a4, then an adversarially released b4. It derives the baseline stream from those tips and checks that the sticky protocol carries round-1 sample a3 into round 2 instead of the fresh fork sample b3. The repeated generated-schedule fixture extends that shape with another adversarial release from b4 to c4, deriving stream samples a2, a3, b3, and c3, and checking sticky carryover across both fork switches. The stochastic-production fixture adds a finite bucketed environment for observed hash-power participation, hidden-work risk, and block variance. It derives low-risk honest extension rounds followed by critical-risk hidden-work releases, then checks the same sticky carryover behavior against the resulting a3, b3, and c3 samples.

Witness the current sticky behavior:

$QUINT test spec/quint/CrosslinkResampling.qnt \
  --main=CrosslinkStickyModel \
  --max-samples=100 \
  --backend=rust

This runs:

  • abandonedRoundProposalTest
  • currentProtocolCarriesStaleSampleTest
  • currentProtocolSameRoundLockBlocksFreshDecisionTest

The second test shows that after a round-0 nil-precommit certificate, the round-1 proposer still proposes s0.

The third test shows the liveness failure mode: if the same-round Tendermint lock is preserved after the stream has changed, the sticky model cannot continue to a fresh s1 decision in round 1.

Witness the proposed resampling behavior:

$QUINT test spec/quint/CrosslinkResampling.qnt \
  --main=CrosslinkNilResamplingModel \
  --max-samples=100 \
  --backend=rust

This runs:

  • abandonedRoundProposalTest
  • nilPrecommitResamplesFreshStreamTest
  • nilPrecommitPreservesOlderTendermintValueLockTest
  • nilPrecommitUnlocksSameRoundTendermintStateTest
  • lateNilPrecommitCertificateUnlocksAbandonedRoundTest
  • nilPrecommitUnlockResamplesAndDecidesFreshValueTest
  • conflictingCommitsExposeInvalidUnlockEvidenceTest
  • conflictWithBogusNilUnlockExposesEquivocationEvidenceTest
  • nilPrecommitCertificateJustifiesSameRoundSwitchTest
  • laterNilCertificateDoesNotUnlockOlderValueLockTest

The second test shows that after the same round-0 nil-precommit certificate, the round-1 proposer proposes s1.

The third and fourth tests are the guardrails for the Tendermint lock rule: a nil precommit certificate preserves older validValue/lockedValue state, but does clear same-round validValue/lockedValue state. The same-round unlock witness is quorum-faithful: the nil certificate is formed by 2f + 1 precommits, while only a minority correct validator keeps a same-round value lock before recovery. The fifth test is the bounded liveness witness: after a same-round nil certificate and stream change, the resampling model reaches a fresh s1 decision.

The late-certificate test covers a real implementation race: a validator may timeout into the next round before it receives the 2f + 1 nil-precommit certificate for the abandoned round. The certificate still clears lock and valid state whose round equals the certificate round, but it does not rewind or re-propose the current round.

The final four tests are the accountability witnesses. They check that:

  • two conflicting value commits across rounds expose Tendermint-style amnesia evidence when there is no nil-precommit unlock certificate for the older lock
  • a bogus nil certificate that coexists with a same-round value commit exposes nil/value equivocation evidence
  • a valid same-round nil certificate justifies switching away from a minority same-round value lock without falsely reporting amnesia
  • a later nil certificate does not justify abandoning an older value lock and still leaves amnesia evidence for the invalid switch

One limitation is intentional: a mixed precommit set with some value precommits and some nil precommits is not treated as unlock evidence unless nil itself has quorum. A mixed set does not rule out a hidden value-commit quorum under Byzantine equivocation, so unlocking on it would be a safety change rather than the nil-certificate liveness improvement modelled here.

Witness Crosslink finality value semantics:

$QUINT test spec/quint/CrosslinkForkFinality.qnt \
  --main=CrosslinkForkFinalityModel \
  --max-samples=100 \
  --backend=rust

This runs:

  • canSkipHeightsOnSamePowBranchTest
  • extendsFinalizedPrefixTest
  • rejectsFinalizingForkAfterFinalBlockTest
  • rejectsUnconfirmedTailTest

Witness derived PoW fork rollback depth:

$QUINT test spec/quint/CrosslinkPowForkSchedule.qnt \
  --main=CrosslinkPowForkScheduleModel \
  --max-samples=100 \
  --backend=rust

This runs:

  • forkSwitchDerivesRollbackDepthTest
  • sameBranchExtensionHasZeroRollbackDepthTest
  • raisedSigmaSurvivesForkSwitchThatBaseSigmaDoesNotTest
  • scheduleDerivesRollbackDepthAcrossRoundsTest

The model has a same-branch extension from a3 to a4, then a fork switch from a4 to b4 whose last common ancestor is at height 2. It derives rollback depth 2 from that best-tip transition and checks that sigma 1 does not survive the switch while sigma 3 does.

Witness generated PoW branch competition:

$QUINT test spec/quint/CrosslinkPowBranchCompetition.qnt \
  --main=CrosslinkPowBranchCompetitionModel \
  --max-samples=100 \
  --backend=rust

This runs:

  • hiddenAdversarialWorkDoesNotWinUntilPublishedTest
  • releasedAdversarialBranchOutworksHonestTipTest
  • generatedCompetitionDerivesRollbackDepthTest
  • raisedSigmaSurvivesGeneratedAdversarialSwitchTest
  • adversarialCatchupProducesForkSwitchTest

The fixture lets an adversarial b4 branch accumulate hidden work while the published best tip remains a4. When b4 is published with higher total work, the generated best tip switches to b4, deriving rollback depth 2 from the a4 -> b4 transition.

Witness the composed nil-precommit-to-finality flow:

$QUINT test spec/quint/CrosslinkComposed.qnt \
  --main=CrosslinkComposedResamplingModel \
  --max-samples=100 \
  --backend=rust

This runs:

  • resamplingNilPrecommitFinalizesFreshCandidateTest

The composed witness forms a round-0 nil-precommit certificate, carries one minority same-round value lock, advances all correct validators to round 1, resamples a2, decides it, and finalizes a2 using a3 as the tail-confirming PoW tip. This also demonstrates height skipping in the composed flow: finality moves from genesis g directly to a2.

Witness BFT-heighted Crosslink finality:

$QUINT test spec/quint/CrosslinkBftHeights.qnt \
  --main=CrosslinkBftHeightsModel \
  --max-samples=100 \
  --backend=rust

This runs:

  • successiveBftDecisionsAdvanceCrosslinkFinalityTest
  • rejectsForkDecisionAfterPrefixFinalityTest
  • rejectsSkippingConsensusHeightTest

The fixture applies scheduled consensus-height-1 decision a2 and consensus-height-2 decision a3, advancing Crosslink finality twice. The negative witnesses reject finalizing a later fork after a2 is final and reject skipping directly from consensus height 0 to height 2.

Witness the dynamic-sigma hash-participation controller:

$QUINT test spec/quint/CrosslinkDynamicSigma.qnt \
  --main=CrosslinkDynamicSigmaHashParticipationModel \
  --max-samples=100 \
  --backend=rust

This runs:

  • highHashParticipationStartsAtBaseSigmaTest
  • roundFailureEscalatesSigmaEvenWithHighHashParticipationTest
  • lowHashParticipationRaisesSigmaFloorWithoutRoundFailureTest
  • criticalHashParticipationForcesMaxSigmaTest
  • adversarialReorgDepthRaisesSigmaFloorTest
  • hashParticipationSigmaFloorIsMonotoneTest
  • calibratedRiskScoreIsMonotoneInParticipationTest
  • combinedStochasticRiskRaisesSigmaWithoutSingleHardSignalTest
  • criticalStochasticRiskForcesMaxSigmaTest
  • blockIntervalVarianceCanRaiseSigmaTest
  • raisedSigmaCanStabilizeMovingPowSampleTest
  • observedReorgRaisesLiveSigmaTest
  • observedLowHashParticipationRaisesLiveSigmaTest
  • observedCriticalHashParticipationForcesLiveMaxSigmaTest

The model separates two signals that should both feed a production controller: round failures tell the protocol that the current sampled stream is not stable enough for Tenderlink to decide, while hash-power participation estimates how much of the global PoW race is actually represented in the Crosslink-visible stream. Lower participation therefore raises the sigma floor even if the current round has not failed. The raisedSigmaCanStabilizeMovingPowSampleTest fixture shows the expected sampling effect directly: head - baseSigma changes across adjacent rounds, but head - raisedSigma remains on the same deeper ancestor. The reorg-depth witnesses add a third input: observed rollback depth forces the controller to move one rung deeper, so an execution that is still healthy by participation can nevertheless raise sigma after a reorg reaches the current floor. The stochastic-risk witnesses add the combined-signal case: moderate coverage risk, recent round failures, and block-interval variance can raise sigma together even when none of the old individual floors would have done so alone; sufficiently high combined risk forces the maximum sigma.

Witness dynamic-sigma calibration over measured windows:

$QUINT test spec/quint/CrosslinkDynamicSigmaCalibration.qnt \
  --main=CrosslinkDynamicSigmaCalibrationModel \
  --max-samples=100 \
  --backend=rust

This runs:

  • healthyMeasuredWindowKeepsBaseSigmaTest
  • marginalHashParticipationRaisesSigmaTest
  • combinedMeasuredRiskRaisesSigmaTest
  • deepReorgMeasuredWindowForcesMaxSigmaTest
  • criticalParticipationMeasuredWindowForcesMaxSigmaTest
  • criticalCombinedRiskForcesMaxSigmaTest
  • calibrationMatchesAllMeasuredWindowsTest

The calibration fixture covers six measured windows: healthy baseline, marginal hash-power participation, combined stochastic risk from coverage plus round-failure plus block-variance signals, deep observed reorgs, critical hash-power participation, and critical combined stochastic risk. The harness checks that the chosen weights and thresholds map each measured window to the expected sigma floor, while keeping each signal monotone and materially weighted.

Witness production-shaped dynamic-sigma telemetry:

$QUINT test spec/quint/CrosslinkDynamicSigmaTelemetry.qnt \
  --main=CrosslinkDynamicSigmaTelemetryModel \
  --max-samples=100 \
  --backend=rust

This runs:

  • healthyTelemetryWindowKeepsBaseSigmaTest
  • sourceHashWorkDerivesTelemetryComponentsTest
  • sourceRoundCountersAreConsistentTest
  • sourceRollbackDepthDerivesTelemetryComponentsTest
  • sourceBlockIntervalVarianceDerivesTelemetryComponentsTest
  • hashWorkParticipationRaisesSigmaTest
  • combinedTelemetryRiskRaisesSigmaTest
  • economicTargetRaisesSigmaAboveSignalFloorTest
  • criticalParticipationForcesMaxSigmaTest
  • economicTargetCanForceMaxSigmaTest
  • unreachableEconomicTargetFallsBackToMaxSigmaTest
  • deepReorgTelemetryWindowForcesMaxSigmaTest
  • expectedLossBudgetRaisesSigmaEvenWithinPpmTargetTest
  • telemetryMatchesAllExpectedWindowsTest

The telemetry fixture covers nine windows: healthy baseline, marginal participating hash work, combined telemetry risk, an economic target that raises sigma above the hard-signal floor, critical participating hash work, an economic target that forces max sigma, an unreachable risk target that falls back to max sigma, a deep reorg, and a high-value-at-risk window where the rollback probability is within the PPM cap but the expected-loss budget still forces a deeper sigma. It checks that conservative telemetry estimates upper-bound raw sampled work and round failures, rollback risk is monotone in sigma, and the selected sigma satisfies the configured rollback-risk and expected-loss targets when the ladder can satisfy them. It also now derives the telemetry component inputs from source-shaped hash-work samples, round counters, best-tip transition heights, and adjacent header timestamps, matching the Rust source observation-window boundary. apply_dynamic_sigma_hysteresis is a pure Rust policy helper for applying those required sigma floors across windows: it raises immediately on worse evidence and only lowers one ladder step after enough stable lower-risk windows. It is also composed into the prototype dynamic-sigma proposal-evidence builder with an explicit initial hysteresis state. Proposal validation still only requires that the carried selected_sigma is at least the telemetry-required floor, so a hysteresis-selected value above the floor remains valid without validators reconstructing private proposer state. The prototype service now owns an in-process hysteresis state and advances it after successfully encoding a dynamic proposal. The hysteresis policy and state now have deterministic Zcash serialization, giving a production source a stable persistence or proposal-carried encoding. The typed Rust selection helper now distinguishes disabled hysteresis from durable-local and proposal-carried state sources: disabled selection emits no next state, while the enabled modes return the next state to persist or carry. Production still needs to choose the durable, consensus-safe or proposal-verifiable source for that state before this becomes a deployed controller rule. The live proposal callback builds a single proposal plan, so the same dynamic-sigma evidence supplies both the BFT block construction depth and the encoded payload instead of running selection twice.

Witness dynamic-sigma hysteresis:

$QUINT test spec/quint/CrosslinkDynamicSigmaHysteresis.qnt \
  --main=CrosslinkDynamicSigmaHysteresisModel \
  --max-samples=100 \
  --backend=rust

This runs:

  • higherRequiredSigmaAppliesImmediatelyTest
  • lowerRequiredSigmaWaitsForStableWindowTest
  • stableLowerRequiredSigmaStepsDownOneLevelTest
  • secondStableDecreaseReturnsToBaseTest
  • hysteresisSafetyHoldsAtWitnessEndTest

The hysteresis fixture treats the required sigma as the output of the telemetry controller. If low Crosslink-participating hash power, fork rollback depth, or combined risk raises that required floor, live sigma follows immediately. If a later window reports healthier participation or lower risk, live sigma waits for stable confirmation windows and then steps down one ladder level instead of jumping directly back to base.

Witness dynamic sigma consuming derived PoW rollback depth:

$QUINT test spec/quint/CrosslinkDynamicSigmaForkSchedule.qnt \
  --main=CrosslinkDynamicSigmaForkScheduleModel \
  --max-samples=100 \
  --backend=rust

This runs:

  • derivedReorgDepthFeedsDynamicSigmaTest
  • forkScheduleDerivedReorgRaisesDynamicSigmaTest
  • derivedRaisedSigmaSurvivesForkSwitchTest

The composed fixture advances from a3 to a4 with rollback depth 0 and keeps base sigma. It then switches from a4 to b4, derives rollback depth 2 from the fork schedule, and raises dynamic sigma from 1 to 3 without relying on a separately supplied observed-reorg map.

Witness dynamic sigma consuming generated PoW branch competition:

$QUINT test spec/quint/CrosslinkDynamicSigmaBranchCompetition.qnt \
  --main=CrosslinkDynamicSigmaBranchCompetitionModel \
  --max-samples=100 \
  --backend=rust

This runs:

  • generatedCompetitionFeedsDynamicSigmaTest
  • generatedCompetitionForkSwitchRaisesDynamicSigmaTest
  • generatedRaisedSigmaSurvivesAdversarialSwitchTest

The composed fixture keeps base sigma while published work extends from a3 to a4, then releases the adversarial b4 branch. The generated best-tip switch derives rollback depth 2 and raises dynamic sigma from 1 to 3.

Witness dynamic sigma composing with nil-precommit resampling:

$QUINT test spec/quint/CrosslinkDynamicSigmaResampling.qnt \
  --main=CrosslinkDynamicSigmaResamplingModel \
  --max-samples=100 \
  --backend=rust

This runs:

  • derivedForkSignalRaisesSigmaBeforeResamplingDecisionTest
  • derivedForkSignalThenNilResamplingDecidesFreshValueTest
  • criticalHashParticipationRaisesSigmaWithoutNewForkSwitchTest
  • generatedCompetitionBacksResamplingForkSignalTest

The witness forms the same-round nil-precommit recovery scenario, derives a rollback-depth signal from the PoW fork fixture, raises sigma from 1 to 3, then advances the validators to round 1 and decides fresh stream value s1. The hash-participation witness then advances over a same-branch transition with rollback depth 0 and still raises sigma to the maximum when participating hash power falls below the configured critical threshold. The generated-competition witness checks that hidden s1 work does not become the best tip until s1 is published, then derives the same rollback-depth signal from that work-backed best-tip switch.

Witness the full dynamic-sigma/resampling/finality composition:

$QUINT test spec/quint/CrosslinkDynamicSigmaFinality.qnt \
  --main=CrosslinkDynamicSigmaFinalityModel \
  --max-samples=100 \
  --backend=rust

This runs:

  • dynamicSigmaResamplingFinalizesTailConfirmedFreshCandidateTest
  • dynamicSigmaRejectsUnderconfirmedFreshCandidateTest
  • dynamicSigmaRejectsSkippedBftHeightFinalityTest
  • hashParticipationSignalRaisesFullCompositionSigmaTest
  • hashParticipationSigmaCanDelayFullFinalityTest
  • generatedCompetitionBacksFullCompositionForkSignalTest

The witness forms a nil-precommit recovery scenario, derives a rollback-depth signal from an a3 -> b4 fork switch, raises dynamic sigma from 1 to 3, resamples and decides fresh b2, then finalizes b2 only with tail-confirming tip b5. The under-confirmed test rejects finalizing the same b2 decision against tip b4, showing that finality uses the raised dynamic sigma rather than the base confirmation depth. The generated-competition witness checks that the fork signal is backed by published work: hidden b4 does not win at round 0, but published b4 becomes the generated best tip at round 1. The skipped BFT-height witness rejects trying to finalize the decided value at consensus height 2 when the current full-composition height is still 0. The hash-participation witnesses then advance to a round with no new fork rollback but only 45% Crosslink-participating hash power; the controller raises sigma to the maximum, and the previously tail-confirmed b2 candidate is rejected against b5 because it is no longer deep enough under the live sigma.

Randomized Rust-backend safety simulation:

$QUINT run spec/quint/CrosslinkResampling.qnt \
  --main=CrosslinkStickyModel \
  --init=Init \
  --step=Next \
  --max-steps=10 \
  --max-samples=1000 \
  --invariant=Safety \
  --backend=rust \
  --verbosity=0

$QUINT run spec/quint/CrosslinkResampling.qnt \
  --main=CrosslinkNilResamplingModel \
  --init=Init \
  --step=Next \
  --max-steps=10 \
  --max-samples=1000 \
  --invariant=Safety \
  --backend=rust \
  --verbosity=0

$QUINT run spec/quint/CrosslinkForkFinality.qnt \
  --main=CrosslinkForkFinalityModel \
  --init=Init \
  --step=Next \
  --max-steps=6 \
  --max-samples=1000 \
  --invariant=Safety \
  --backend=rust \
  --verbosity=0

$QUINT run spec/quint/CrosslinkPowForkSchedule.qnt \
  --main=CrosslinkPowForkScheduleModel \
  --init=Init \
  --step=Next \
  --max-steps=4 \
  --max-samples=1000 \
  --invariant=Safety \
  --backend=rust \
  --verbosity=0

$QUINT run spec/quint/CrosslinkPowBranchCompetition.qnt \
  --main=CrosslinkPowBranchCompetitionModel \
  --init=Init \
  --step=Next \
  --max-steps=4 \
  --max-samples=1000 \
  --invariant=Safety \
  --backend=rust \
  --verbosity=0

$QUINT run spec/quint/CrosslinkResampling.qnt \
  --main=CrosslinkNilResamplingLivenessModel \
  --init=LivenessInit \
  --step=LivenessStep \
  --max-steps=15 \
  --max-samples=1 \
  --invariant=LivenessSafety \
  --backend=rust \
  --verbosity=0

$QUINT run spec/quint/CrosslinkComposed.qnt \
  --main=CrosslinkComposedResamplingModel \
  --init=ComposedInit \
  --step=ComposedNext \
  --max-steps=10 \
  --max-samples=1000 \
  --invariant=ComposedSafety \
  --backend=rust \
  --verbosity=0

$QUINT run spec/quint/CrosslinkComposed.qnt \
  --main=CrosslinkComposedLivenessModel \
  --init=LivenessInit \
  --step=LivenessStep \
  --max-steps=16 \
  --max-samples=1 \
  --invariant=LivenessSafety \
  --backend=rust \
  --verbosity=0

$QUINT run spec/quint/CrosslinkBftHeights.qnt \
  --main=CrosslinkBftHeightsModel \
  --init=Init \
  --step=Next \
  --max-steps=5 \
  --max-samples=1000 \
  --invariant=Safety \
  --backend=rust \
  --verbosity=0

$QUINT run spec/quint/CrosslinkDynamicSigma.qnt \
  --main=CrosslinkDynamicSigmaHashParticipationModel \
  --init=Init \
  --step=Next \
  --max-steps=7 \
  --max-samples=1000 \
  --invariant=Safety \
  --backend=rust \
  --verbosity=0

$QUINT run spec/quint/CrosslinkDynamicSigmaCalibration.qnt \
  --main=CrosslinkDynamicSigmaCalibrationModel \
  --init=Init \
  --step=Next \
  --max-steps=8 \
  --max-samples=1000 \
  --invariant=Safety \
  --backend=rust \
  --verbosity=0

$QUINT run spec/quint/CrosslinkDynamicSigmaTelemetry.qnt \
  --main=CrosslinkDynamicSigmaTelemetryModel \
  --init=Init \
  --step=Next \
  --max-steps=8 \
  --max-samples=1000 \
  --invariant=Safety \
  --backend=rust \
  --verbosity=0

$QUINT run spec/quint/CrosslinkDynamicSigmaHysteresis.qnt \
  --main=CrosslinkDynamicSigmaHysteresisModel \
  --init=Init \
  --step=Next \
  --max-steps=5 \
  --max-samples=1000 \
  --invariant=Safety \
  --backend=rust \
  --verbosity=0

$QUINT run spec/quint/CrosslinkDynamicSigmaForkSchedule.qnt \
  --main=CrosslinkDynamicSigmaForkScheduleModel \
  --init=DerivedInit \
  --step=DerivedNext \
  --max-steps=4 \
  --max-samples=1000 \
  --invariant=DerivedSafety \
  --backend=rust \
  --verbosity=0

$QUINT run spec/quint/CrosslinkDynamicSigmaBranchCompetition.qnt \
  --main=CrosslinkDynamicSigmaBranchCompetitionModel \
  --init=BranchCompetitionDynamicInit \
  --step=BranchCompetitionDynamicNext \
  --max-steps=4 \
  --max-samples=1000 \
  --invariant=BranchCompetitionDynamicSafety \
  --backend=rust \
  --verbosity=0

$QUINT run spec/quint/CrosslinkDynamicSigmaResampling.qnt \
  --main=CrosslinkDynamicSigmaResamplingModel \
  --init=DynamicResamplingInit \
  --step=DynamicResamplingNext \
  --max-steps=8 \
  --max-samples=1000 \
  --invariant=DynamicResamplingSafety \
  --backend=rust \
  --verbosity=0

$QUINT run spec/quint/CrosslinkDynamicSigmaFinality.qnt \
  --main=CrosslinkDynamicSigmaFinalityModel \
  --init=FullComposedInit \
  --step=FullComposedNext \
  --max-steps=10 \
  --max-samples=1000 \
  --invariant=FullComposedSafety \
  --backend=rust \
  --verbosity=0

Bounded Apalache verification:

$QUINT verify spec/quint/CrosslinkResampling.qnt \
  --main=CrosslinkStickyModel \
  --max-steps=3 \
  --init=Init \
  --step=Next \
  --invariant=Safety

$QUINT verify spec/quint/CrosslinkResampling.qnt \
  --main=CrosslinkNilResamplingModel \
  --max-steps=3 \
  --init=Init \
  --step=Next \
  --invariant=Safety

$QUINT verify spec/quint/CrosslinkForkFinality.qnt \
  --main=CrosslinkForkFinalityModel \
  --max-steps=4 \
  --init=Init \
  --step=Next \
  --invariant=Safety

$QUINT verify spec/quint/CrosslinkPowForkSchedule.qnt \
  --main=CrosslinkPowForkScheduleModel \
  --max-steps=4 \
  --init=Init \
  --step=Next \
  --invariant=Safety

$QUINT verify spec/quint/CrosslinkPowBranchCompetition.qnt \
  --main=CrosslinkPowBranchCompetitionModel \
  --max-steps=4 \
  --init=Init \
  --step=Next \
  --invariant=Safety

$QUINT verify spec/quint/CrosslinkResampling.qnt \
  --main=CrosslinkNilResamplingLivenessModel \
  --max-steps=15 \
  --init=LivenessInit \
  --step=LivenessStep \
  --invariant=LivenessSafety

$QUINT verify spec/quint/CrosslinkComposed.qnt \
  --main=CrosslinkComposedResamplingModel \
  --max-steps=5 \
  --init=ComposedInit \
  --step=ComposedNext \
  --invariant=ComposedSafety

$QUINT verify spec/quint/CrosslinkComposed.qnt \
  --main=CrosslinkComposedLivenessModel \
  --max-steps=16 \
  --init=LivenessInit \
  --step=LivenessStep \
  --invariant=LivenessSafety

$QUINT verify spec/quint/CrosslinkBftHeights.qnt \
  --main=CrosslinkBftHeightsModel \
  --max-steps=5 \
  --init=Init \
  --step=Next \
  --invariant=Safety

$QUINT verify spec/quint/CrosslinkDynamicSigma.qnt \
  --main=CrosslinkDynamicSigmaHashParticipationModel \
  --max-steps=7 \
  --init=Init \
  --step=Next \
  --invariant=Safety

$QUINT verify spec/quint/CrosslinkDynamicSigmaCalibration.qnt \
  --main=CrosslinkDynamicSigmaCalibrationModel \
  --max-steps=8 \
  --init=Init \
  --step=Next \
  --invariant=Safety

$QUINT verify spec/quint/CrosslinkDynamicSigmaTelemetry.qnt \
  --main=CrosslinkDynamicSigmaTelemetryModel \
  --max-steps=8 \
  --init=Init \
  --step=Next \
  --invariant=Safety

$QUINT verify spec/quint/CrosslinkDynamicSigmaHysteresis.qnt \
  --main=CrosslinkDynamicSigmaHysteresisModel \
  --max-steps=5 \
  --init=Init \
  --step=Next \
  --invariant=Safety

$QUINT verify spec/quint/CrosslinkDynamicSigmaForkSchedule.qnt \
  --main=CrosslinkDynamicSigmaForkScheduleModel \
  --max-steps=4 \
  --init=DerivedInit \
  --step=DerivedNext \
  --invariant=DerivedSafety

$QUINT verify spec/quint/CrosslinkDynamicSigmaBranchCompetition.qnt \
  --main=CrosslinkDynamicSigmaBranchCompetitionModel \
  --max-steps=4 \
  --init=BranchCompetitionDynamicInit \
  --step=BranchCompetitionDynamicNext \
  --invariant=BranchCompetitionDynamicSafety

$QUINT verify spec/quint/CrosslinkDynamicSigmaResampling.qnt \
  --main=CrosslinkDynamicSigmaResamplingModel \
  --max-steps=8 \
  --init=DynamicResamplingInit \
  --step=DynamicResamplingNext \
  --invariant=DynamicResamplingSafety

JVM_ARGS=-Xmx8192m $QUINT verify spec/quint/CrosslinkDynamicSigmaFinality.qnt \
  --main=CrosslinkDynamicSigmaFinalityModel \
  --max-steps=8 \
  --init=FullComposedInit \
  --step=FullComposedNext \
  --invariant=FullComposedSafety

JVM_ARGS=-Xmx8192m $QUINT verify spec/quint/CrosslinkDynamicSigmaFinality.qnt \
  --main=CrosslinkDynamicSigmaFinalityModel \
  --max-steps=10 \
  --init=FullComposedInit \
  --step=FullComposedNext \
  --invariant=FullProtocolProjectionSafety

JVM_ARGS=-Xmx8192m $QUINT verify spec/quint/CrosslinkDynamicSigmaFinality.qnt \
  --main=CrosslinkDynamicSigmaFinalityModel \
  --max-steps=10 \
  --init=FullComposedInit \
  --step=FullComposedNext \
  --invariant=FullFinalityProjectionSafety

JVM_ARGS=-Xmx8192m $QUINT verify spec/quint/CrosslinkDynamicSigmaFinality.qnt \
  --main=CrosslinkDynamicSigmaFinalityModel \
  --max-steps=10 \
  --init=FullComposedInit \
  --step=FullComposedNext \
  --invariant=FullWorkCompetitionProjectionSafety

The full dynamic-sigma finality composition is substantially heavier under Apalache once the resampling model includes accountability evidence. The checked symbolic bound above passes with an 8G JVM heap. The projection checks split the full composition into protocol/accountability, finalized-prefix, and generated work-competition obligations so each can be pushed to a deeper bound before rerunning the full conjunction.

The bounded resampling checks currently report no violation for Safety, which combines:

  • validity of correct-validator decisions
  • agreement on decided values
  • upstream-style accountability: if agreement fails, at least f + 1 validators are detectable by equivocation or amnesia evidence
  • no correct-validator precommit equivocation
  • no same-round quorum for both nil and a concrete value
  • any retained Tendermint lock keeps its matching valid value across nil-precommit round recovery
  • any retained Tendermint lock is backed by value-precommit evidence
  • a nil certificate leaves at most f correct same-round value locks, so same-round unlock is not discarding a commit-capable value-lock quorum
  • observed proposal, prevote, and precommit messages are covered by transition-carried evidence sets
  • every pair of conflicting value commit quorums has accountability evidence: either correct-validator precommit equivocation, nil/value equivocation in the purported unlock round, or a correct-validator value switch without a valid same-round nil unlock certificate

The bounded baseline-accountability checks report no violation for BaselineAccountabilitySafety, which is the current fixed-sigma/sticky Tenderlink safety invariant. The named witnesses show that a nil-precommit certificate advances the round without clearing same-round validValue or lockedValue, and that conflicting value commits without nil-unlock evidence are attributable through amnesia evidence. The same test module now includes faulty proposal, prevote, and nil/value precommit evidence witnesses that prove observed faulty evidence reaches the equivocation predicates. BaselineFaultyInitSafety additionally checks a tiny instance initialized with nondeterministic faulty proposal, prevote, and precommit powersets. BaselineForkingFaultyInitSafety checks the fixed-sigma/forking baseline parameter surface with a bounded nondeterministic faulty-init domain. BaselineBoundedN4F2ForkingFaultyInitSafety checks a representative bounded n4_f2 faulty-init domain in both the quick gate and the symbolic baseline gate. BaselineSingleN4F2ForkingFaultyInitSafety checks a full-domain single-faulty n4_f2 abstraction in both the quick gate and the symbolic baseline gate, by selecting one arbitrary faulty proposal, prevote, and precommit from the complete n4_f2 faulty-evidence domain. BaselinePairN4F2ForkingFaultyInitSafety checks the next tractable full-domain n4_f2 abstraction in both the quick gate and the symbolic baseline gate, by selecting up to two arbitrary faulty proposals, prevotes, and precommits from the complete n4_f2 faulty-evidence domain. BaselineTripleN4F2ForkingFaultyInitSafety checks the next tractable full-domain n4_f2 abstraction in both the quick gate and the symbolic baseline gate, by selecting up to three arbitrary faulty proposals, prevotes, and precommits from the complete n4_f2 faulty-evidence domain. BaselineBoundedN5F2ForkingFaultyInitSafety checks a representative bounded n5_f2 faulty-init domain in both the quick gate and the symbolic baseline gate. BaselineSingleN5F2ForkingFaultyInitSafety checks a full-domain single-faulty n5_f2 abstraction in both the quick gate and the symbolic baseline gate, by selecting one arbitrary faulty proposal, prevote, and precommit from the complete n5_f2 faulty-evidence domain. BaselinePairN5F2ForkingFaultyInitSafety checks a full-domain pair-faulty n5_f2 abstraction in both the quick gate and the symbolic baseline gate, by selecting up to two arbitrary faulty proposals, prevotes, and precommits from the complete n5_f2 faulty-evidence domain. BaselineTripleN5F2ForkingFaultyInitSafety checks a full-domain triple-faulty n5_f2 abstraction in both the quick gate and the symbolic baseline gate, by selecting up to three arbitrary faulty proposals, prevotes, and precommits from the complete n5_f2 faulty-evidence domain. BaselineBoundedN7F2ForkingFaultyInitSafety checks a representative bounded n7_f2 faulty-init domain in both the quick gate and the symbolic baseline gate. BaselineFullForkingFaultyInitSafety checks the same fixed-sigma/forking baseline parameter surface against the full faulty-init domain in the quick Rust-backed gate. BaselineFullN4F2ForkingFaultyInitSafety checks the above-live-boundary n4_f2 fixed-sigma/forking surface against the full faulty-init domain in the quick Rust-backed gate. BaselineFullN5F1ForkingFaultyInitSafety checks the intermediate n5_f1 fixed-sigma/forking surface against the full faulty-init domain in the quick Rust-backed gate. BaselineFullN5F2ForkingFaultyInitSafety checks the above-live-boundary n5_f2 fixed-sigma/forking surface against the full faulty-init domain in the quick Rust-backed gate. BaselineFullN7F2ForkingFaultyInitSafety checks the proper n7_f2 fixed-sigma/forking boundary surface against the full faulty-init domain in the quick Rust-backed gate. CrosslinkBaselineCounterexampleModel adds negative tests showing that false claims about conflicting commits, amnesia, equivocation, and agreement are rejected by the harness.

The bounded upstream-shaped baseline checks report no violation for the n4_f1 safety witnesses plus the bounded n4_f2, n5_f2, and n7_f2 symbolic safety gates. The stable n4_f1 instance keeps the normal fixed-sigma decision path, no-double-proposal witness, nil-prevote quorum path, and timeout-driven nil vote/round-advance witnesses alive through the parameter shell. It also covers a focused future-round catchup witness from f + 1 future-round messages. The forking n4_f1 instance records that the shell derives the fresh round-1 head - sigma sample while the sticky baseline can still carry the stale round-0 sample. The f = 2 instances document that n4_f2 cannot form even correct-only catchup evidence, n5_f2 can form f+1 catchup evidence but not a 2f+1 correct value quorum, and n7_f2 retains the ordinary 2f+1 correct decision path.

The bounded baseline BFT-height checks report no violation for BaselineBftHeightSafety, which combines bounded consensus-height progression with finalized-prefix safety. The named witnesses show consecutive BFT decisions advancing finality, and reject skipped BFT heights or fork finality after a prefix is final.

The bounded baseline-finality checks report no violation for ComposedSafety, which combines the current fixed-sigma/sticky Tenderlink safety invariant with the finalized-prefix invariant. The stable-stream liveness harness reaches an a2 decision and finality update by phase 9; the stream-change witness records that the sticky baseline can carry a1 into round 1 and leave finality at g instead of finalizing the fresh stream value.

The bounded baseline PoW-sampling checks report no violation for BaselinePowSamplingSafety, BaselinePowLongReorgSafety, and BaselinePowGeneratedScheduleSafety, and BaselinePowRepeatedGeneratedScheduleSafety, and BaselinePowStochasticProductionSafety, which combine the current fixed-sigma/sticky Tenderlink safety invariant with an explicit Stream(round) = head - sigma condition. The fork-switch witness records a rollback from a4 to b4 where the round-0 fixed-sigma sample a3 no longer survives. The long-reorg witness records a deeper rollback from a5 to c5 where rollback depth 3 exceeds sigma 2. The generated-schedule witnesses derive fork switches from published work competition, including repeated releases where b4 outworks a4 and then c4 outworks b4. The stochastic-production witness derives those release windows from finite risk buckets over hash-power participation, hidden-work risk, and observed block variance. In all cases, the sticky baseline still carries the old sample into the next round instead of sampling the fresh fork.

The bounded fork-finality check reports no violation for its Safety, which combines:

  • finalized snapshots are prefix-linear
  • the latest final snapshot extends every previously finalized snapshot
  • the initial final snapshot remains finalized

The bounded PoW fork-schedule check reports no violation for its Safety, which combines:

  • the live best tip matches the configured best-tip schedule
  • the live rollback depth is derived from the previous and current best tips
  • the rollback depth stays within the configured PoW height bound

The bounded PoW branch-competition check reports no violation for its Safety, which combines:

  • the live best tip matches the work-derived published-tip competition
  • the live best-tip work matches honest plus adversarial work
  • the live rollback depth is derived from generated best-tip changes
  • the rollback depth stays within the configured PoW height bound

The bounded liveness harness checks the proposed nil-precommit flow under a post-GST schedule: a same-round nil certificate is formed for s0, one correct validator may hold a minority same-round value lock, all correct validators advance to round 1, the proposer resamples s1, and the model reaches a fresh s1 decision by phase 15 while preserving the safety invariants.

The composed bounded liveness harness checks the Crosslink-level version of the same argument: after a same-round nil certificate and a stream update from a1 to a2, resampling reaches a fresh a2 Tenderlink decision and then a fresh a2 finality update by phase 16 while preserving both the Tenderlink lock safety invariants and the finalized-prefix safety invariants.

The BFT-heighted finality harness checks that consecutive BFT decisions advance Crosslink finality in height order while preserving finalized-prefix safety. It also gives negative witnesses for two invalid transitions: skipping a consensus height and finalizing a fork after a prefix is final.

The dynamic-sigma harness checks a bounded controller invariant: live sigma remains within the configured ladder, never falls below the floor implied by observed hash-power participation, observed reorg depth, or the calibrated stochastic-risk score, tracks the head - sigma snapshot selected for the current round, and uses monotone risk surfaces where lower participation cannot require a lower sigma than higher participation.

The dynamic-sigma calibration harness reports no violation for Safety, which combines:

  • every bounded measurement window maps to its expected sigma floor
  • lower hash-power participation never lowers the participation-derived floor
  • lower hash-power participation never lowers the calibrated risk score
  • round-failure, block-variance, and reorg-depth weights are all material
  • the observation-walk invariant preserves the calibrated label for each window

The production-shaped dynamic-sigma telemetry harness reports no violation for Safety, which combines:

  • source hash-work samples derive the total-work denominator and Crosslink-participating numerator
  • source round counters remain internally consistent
  • source best-tip transition heights derive observed rollback depth
  • source header timestamps derive a conservative block-interval variance input
  • conservative coverage estimates upper-bound the raw gap between total PoW work and Crosslink-participating PoW work
  • conservative round-failure estimates upper-bound raw failed Tenderlink rounds
  • rollback-risk estimates are monotone across the sigma ladder
  • selected sigma satisfies the explicit rollback-risk and expected-loss targets when reachable
  • if the target is unreachable at max sigma, the controller falls back to max sigma and exposes that status
  • sampled hash-work coverage maps to the expected participation floor
  • every telemetry window maps to its expected sigma floor

The dynamic-sigma hysteresis harness reports no violation for Safety, which combines:

  • live sigma remains within the configured ladder
  • live sigma remains at least the required floor for the current window
  • higher required sigma applies immediately
  • lower required sigma waits for stable windows and steps down one ladder level at a time

The dynamic-sigma/fork-schedule composition reports no violation for DerivedSafety, which combines:

  • the PoW fork-schedule safety invariants
  • dynamic sigma stays within the configured ladder
  • dynamic sigma respects the hash-participation floor
  • dynamic sigma respects the rollback-depth floor derived from the fork schedule
  • the controller status matches current hash participation

The dynamic-sigma/branch-competition composition reports no violation for BranchCompetitionDynamicSafety, which combines:

  • the PoW branch-competition safety invariants
  • dynamic sigma stays within the configured ladder
  • dynamic sigma respects the hash-participation floor
  • dynamic sigma respects the rollback-depth floor derived from generated best-tip work competition
  • the controller status matches current hash participation

The dynamic-sigma/resampling composition reports no violation for DynamicResamplingSafety, which combines:

  • the nil-precommit resampling safety invariants
  • dynamic sigma stays within the configured ladder
  • dynamic sigma respects the rollback-depth floor derived from the fork fixture
  • dynamic sigma respects the hash-power participation floor
  • the participation floor is monotone, so lower participation never requires a lower sigma than higher participation
  • the controller status matches current hash-power participation
  • the current dynamic best tip matches generated published-tip work competition

The full dynamic-sigma/resampling/finality composition reports no violation for FullComposedSafety, which combines:

  • the dynamic-sigma/resampling safety invariants
  • the generated best-tip work-competition invariant for the current dynamic round
  • finalized snapshots remain prefix-linear
  • the latest finalized snapshot extends all prior finalized snapshots
  • the initial finalized snapshot remains finalized
  • finality advances exactly one BFT consensus height at a time
  • finality uses the live dynamic sigma as the tail-confirmation depth
  • hash-participation-driven sigma increases compose with the same finality depth rule as fork-derived sigma increases

The split full-composition projection checks report no violation at depth 10. FullProtocolProjectionSafety is the expensive check because it carries the nil-precommit/accountability evidence obligations. FullFinalityProjectionSafety and FullWorkCompetitionProjectionSafety isolate the finalized-prefix and generated-work-competition obligations and run substantially faster.

Next Extensions

This model is intentionally narrow. The next useful extensions are:

  • wire the pure Rust dynamic-sigma controller, proposal-evidence verifier, and selected-sigma BFT block constructor to production telemetry sources and live proposal validation, including a consensus-safe Crosslink hash-participation metric and a validated economic exposure model. The live prototype proposal path now runs the controller over a policy-guarded timed-header fixture, and the pure telemetry assembly boundary fails closed on missing participating-work evidence or inconsistent round counters. The event accumulator now gives live Tenderlink hooks an exact round-counter contract, the hash-work observation accumulator gives source producers an exact participation-numerator contract, the hash-work window policy bounds participation by minimum history, recent window size, and total observed work, the header adapter derives a work-weighted participation share from the current Crosslink fat-pointer marker, custom verifier hooks can replace that prototype marker with stricter production validation, the header observation-window assembler composes those headers with round and fork evidence into telemetry components, the timed header-window adapter derives conservative block-interval variance from adjacent header timestamps and can apply the same hash-work window policy in that timed path, the target-spacing helper derives expected spacing from the active network upgrade, and pure rollback-depth helpers record best-tip transitions, derive the current observed reorg-depth input, and produce one rollback-depth history sample per transition window from explicit best-tip transition evidence. The pure rollback-risk estimator derives a monotone ppm curve from observed rollback-depth windows plus a bounded margin, and the explicit window policy requires enough history while truncating to the most recent bounded sample window. The economic exposure policy now explicitly separates consensus-critical exposure from service-local risk, with proposal-evidence tests rejecting selected sigma below a consensus-critical economic floor. The pure evidence-selection helpers now construct proposal evidence from raw telemetry or assembled components, with optional hysteresis, and the prototype proposal path uses them. The pure hysteresis helper covers bounded sigma decreases for short-window stability, and the prototype evidence builder now applies an explicit hysteresis state before carrying selected_sigma. The proposal callback reuses that planned evidence for both depth selection and payload encoding, then advances the prototype service's in-process hysteresis state after successful encoding. The hysteresis policy/state now have deterministic Zcash serialization for a future durable or proposal-carried state source, and the selection helper now models disabled, durable-local, and proposal-carried source policies explicitly. The remaining work is replacing the fixture with consensus-safe or proposal-verifiable input producers and configuring the production hysteresis state source
  • refine the split projection checks into smaller inductive lemmas if bounds beyond the checked depth-10 projections still need very large JVM/Z3 heaps

Baseline Crosslink Quint Completeness Audit

This audit tracks what remains before the baseline fixed-sigma/sticky Crosslink Quint spec reaches the same completeness target as the upstream Tendermint Quint model.

Objective

The baseline deliverable is not only a collection of witnesses. It should be a reviewable Crosslink baseline specification with:

  • a named fixed-sigma/sticky Tenderlink model
  • explicit head - sigma PoW sampling
  • Crosslink finalized-prefix semantics at explicit BFT consensus heights
  • Tendermint-style safety, validity, agreement, and accountability properties
  • upstream-style small model instances
  • automated quick and symbolic proof gates
  • documented limitations for fork recovery, PoW reorgs, and nil-precommit behavior

Upstream Tendermint Reference

The current upstream reference is:

https://github.com/informalsystems/quint/tree/main/examples/cosmos/tendermint

baseline-upstream-crosswalk.md records the exact upstream commit used for the line-item comparison and maps the current baseline artifacts against each upstream surface.

That directory contains a Quint port of the CometBFT accountability TLA+ spec. The relevant upstream surface is:

  • Tendermint.qnt
    • parameterized process sets: Corr, Faulty, N, T
    • value sets: ValidValues, InvalidValues
    • round/proposer parameters: MaxRound, Proposer
    • quorum constants: THRESHOLD1 = T + 1, THRESHOLD2 = 2 * T + 1
    • consensus state: round, step, decision, lockedValue, lockedRound, validValue, validRound
    • message/evidence state for proposals, prevotes, and precommits
    • nondeterministic faulty proposal, prevote, and precommit injection in Init
    • full transition surface: StartRound, InsertProposal, proposal handlers, prevote quorum handlers, precommit quorum handlers, timeout handlers, and round catchup
    • accountability predicates for equivocation and amnesia
    • properties for agreement, validity, accountability, and false-invariant counterexample exploration
  • TendermintModels.qnt
    • small model instances such as n4_f1, n4_f2, and n5_f2
  • TendermintTest.qnt
    • witness tests for normal decision, no double proposal, and timeout progress

Current Baseline Artifacts

The current branch has these baseline-specific files:

  • CrosslinkBaseline.qnt
    • packages the fixed-sigma/sticky variant with ResampleOnNilPrecommit = false
    • includes stable-stream and stream-change witnesses
  • CrosslinkBaselineTenderlink.qnt
    • adds an upstream-shaped parameter shell for Corr, Faulty, N, T, value sets, rounds, proposers, sigma, best tips, heights, and ancestors
    • derives BaselineStream(round) from the Crosslink fixed-sigma head - sigma rule while keeping sticky nil-precommit semantics
  • CrosslinkBaselineModels.qnt
    • defines small stable and forking baseline model instances over the parameter shell
    • currently covers n4_f1_stable, n4_f1_forking, n5_f1_forking, n4_f2_forking, n5_f2_forking, and n7_f2_forking
  • CrosslinkBaselineTest.qnt
    • adds upstream-style smoke tests for fixed-sigma sampling, normal decision, no double proposal, nil prevote quorum handling, timeout-driven nil votes and round advance, future-round catchup, fork-derived stream change, and sticky stale-sample carryover
    • adds a false-invariant witness for the Crosslink-specific stale fixed-sigma proposal after a stream switch
    • adds f = 2 boundary witnesses distinguishing n4_f2 and n5_f2 above-live-boundary behavior from a proper n7_f2 decision path
  • CrosslinkBaselinePowSampling.qnt
    • derives Stream(round) from an explicit head - sigma ancestor
    • records the fork-switch stale-sample behavior
  • CrosslinkBaselineAccountability.qnt
    • records that baseline nil precommit preserves same-round value locks
    • records conflicting commit accountability through amnesia evidence
    • records that faulty proposal, prevote, and nil/value precommit evidence reaches the equivocation predicates
    • adds a tiny InitWithFaultyEvidence harness with nondeterministically injected faulty proposal, prevote, and precommit powersets
    • adds a fixed-sigma/forking n4_f1 faulty-init harness with a bounded nondeterministic faulty-evidence domain
    • adds representative bounded n4_f2, n5_f2, and n7_f2 faulty-init harnesses to the symbolic gate so f=2 faulty evidence is checked beyond the tiny/one-fault shape
    • adds single-/pair-/triple-faulty n4_f2 symbolic harnesses that select one to three arbitrary faulty proposals, prevotes, and precommits from the full n4_f2 faulty evidence domains
    • adds single-/pair-/triple-faulty n5_f2 symbolic harnesses that select one to three arbitrary faulty proposals, prevotes, and precommits from the larger full n5_f2 faulty evidence domains
    • adds quick-check-only fixed-sigma/forking faulty-init harnesses for n4_f1, n4_f2, n5_f1, n5_f2, and n7_f2 using the full faulty proposal, prevote, and precommit powerset domains
    • adds false-invariant counterexample tests for conflicting commits, amnesia, equivocation, agreement, agreement-or-amnesia, amnesia-implies-equivocation, amnesia-without-equivocation, and undecided max-round behavior
  • CrosslinkBaselineBftHeights.qnt
    • gives baseline finality explicit BFT consensus heights
    • rejects skipped consensus heights and fork finality after a finalized prefix
    • adds a false-invariant witness for a fork-finality attempt after prefix finality
  • CrosslinkBaselineFinality.qnt
    • composes the sticky Tenderlink baseline with finalized-prefix semantics
    • records stable finality and the stream-change finality stall
  • CrosslinkResampling.qnt
    • shared focused Tenderlink model used by both baseline and proposed nil-precommit resampling variants
  • check.sh
    • provides quick-baseline, symbolic-baseline, symbolic-baseline-core, and symbolic-baseline-accountability gates
    • supports APALACHE_PORT_BASE for sequential symbolic checker ports during long local runs
  • .github/workflows/quint-crosslink.yml
    • runs baseline quick checks and parallel core/accountability symbolic checks on the personal fork
  • baseline-upstream-crosswalk.md
    • maps the upstream Tendermint Quint surface to the baseline Crosslink artifacts and names the remaining upstream-quality gaps

Coverage Matrix

Requirement Current evidence Status
Upstream Tendermint crosswalk baseline-upstream-crosswalk.md Covered
Named fixed-sigma/sticky baseline variant CrosslinkBaseline.qnt; ResampleOnNilPrecommit = false Covered
Stable-stream decision path baselineStableStreamDecidesSampledSnapshotTest Covered
Stream-change halt/stale-sample limitation baselineCarriesStaleSampleAfterStreamChangeTest; baselineSameRoundLockBlocksFreshDecisionAfterStreamChangeTest Covered
Explicit fixed head - sigma sampling CrosslinkBaselinePowSampling.qnt Covered
Fork switch rolls back sampled ancestor forkSwitchRollsBackFixedSigmaSampleTest Covered
Sticky baseline carries rolled-back sample stickyBaselineCarriesRolledBackHeadMinusSigmaTest Covered
Crosslink finalized-prefix safety CrosslinkBaselineFinality.qnt; ComposedSafety Covered, bounded
Explicit BFT consensus-height progression CrosslinkBaselineBftHeights.qnt; BaselineBftHeightSafety Covered, bounded
Reject skipped BFT heights baselineRejectsSkippedBftHeightTest Covered
Reject fork finality after prefix finality baselineRejectsForkAfterPrefixFinalityTest Covered
Baseline nil precommit preserves same-round value locks baselineNilPrecommitDoesNotClearSameRoundValueLockTest Covered
Conflicting commits expose accountability evidence baselineConflictingCommitsWithoutUnlockExposeAmnesiaTest Covered as a witness
Automated local baseline quick gate check.sh quick-baseline Covered
Automated local baseline symbolic gate check.sh symbolic-baseline; check.sh symbolic-baseline-core; check.sh symbolic-baseline-accountability Covered
Automated CI baseline gates .github/workflows/quint-crosslink.yml Covered structurally; requires green run evidence per commit
Parameterized Corr/Faulty/N/T validator model CrosslinkBaselineTenderlink.qnt; BaselineInitWithFaultyEvidence; BaselineStartRound; BaselineNext; baseline-prefixed proposal, vote, timeout, nil, round-advance, catchup, and decision aliases; BaselineFaultyInitSafety; CrosslinkBaselineParameterizedShellTest Partial; parameter shell now exposes the full faulty-init evidence surface plus the focused transition surface through baseline-prefixed aliases, while the full upstream-identical transition surface is still not ported
Upstream-style model instances (n4_f1, n4_f2, n5_f2) CrosslinkBaselineModels.qnt; n4_f1_stable, n4_f1_forking, n5_f1_forking, n4_f2_forking, n5_f2_forking, n7_f2_forking Covered as named focused instances with depth-3 symbolic coverage for the f = 2 safety gates
Upstream-style normal decision/no-double-proposal/StartRound/nil-prevote/timeout/catchup/validRound tests CrosslinkBaselineTest.qnt; decisionTest; noProposeTwiceTest; parameterizedShellStartRoundAdvancesToProposeTest; parameterizedShellTransitionAliasesDriveDecisionPathTest; parameterizedShellStreamChangeAliasPrecommitsNilTest; parameterizedShellNilTimeoutAliasesStartNextRoundTest; parameterizedShellLateNilCertificateAliasStaysDisabledTest; parameterizedShellTimeoutPrecommitAndCatchupAliasesTest; parameterizedShellConcreteValidRoundAliasAcceptsPrevoteQuorumTest; nilPrevoteQuorumPrecommitsNilTest; timeoutPrevotePathFormsNilPrecommitCertTest; timeoutPrecommitAdvancesWithoutPrecommitQuorumTest; roundCatchupStartsFutureRoundTest; validRoundProposalWithoutPrevoteQuorumIsRejectedTest; validRoundProposalWithPrevoteQuorumIsAcceptedTest; nilValidRoundProposalHandlerPrevotesTest; validRoundProposalHandlerWithoutPrevoteQuorumIsRejectedTest; validRoundProposalHandlerWithPrevoteQuorumIsAcceptedTest; correctValuePrevotesRequireJustifiedProposalTest Covered for n4_f1_stable plus shell-level StartRound, transition-alias, timeout, catchup, stream-change, late nil-certificate disabled-baseline, and validRound coverage
Upstream-style stream-change/sticky-sample test CrosslinkBaselineTest.qnt; streamChangeDerivesFreshHeadMinusSigmaTest; stickyBaselineCarriesStaleFixedSigmaSampleTest Covered for n4_f1_forking
Fault-boundary behavior for f = 2 CrosslinkBaselineTest.qnt; n4F2DocumentsAboveLiveFaultBoundaryTest; n5F2CatchupEvidenceButNoCorrectValueQuorumTest; n7F2DecisionPathTest; symbolic-baseline depth-3 checks for BaselineN4F2ForkingSafety, BaselineN5F2ForkingSafety, and BaselineN7F2ForkingSafety Covered by Rust-backed witnesses and bounded symbolic gates
Faulty proposal/prevote/precommit evidence reaches equivocation predicates CrosslinkBaselineAccountability.qnt; baselineFaultyProposalEvidenceFeedsEquivocationTest; baselineFaultyPrevoteEvidenceFeedsEquivocationTest; baselineFaultyNilValuePrecommitEvidenceFeedsEquivocationTest Covered as witnesses
Nondeterministic faulty message injection in Init BaselineInitWithFaultyEvidence; BaselineNext; CrosslinkBaselineParameterizedShellTest; InitWithFaultyEvidence; InitWithSingleN4F2FaultyEvidence; InitWithPairN4F2FaultyEvidence; InitWithTripleN4F2FaultyEvidence; InitWithSingleN5F2FaultyEvidence; InitWithPairN5F2FaultyEvidence; InitWithTripleN5F2FaultyEvidence; CrosslinkBaselineFaultyInitTinyModel; CrosslinkBaselineFaultyInitForkingModel; CrosslinkBaselineBoundedFaultyInitN4F2ForkingModel; CrosslinkBaselineSingleFaultyInitN4F2ForkingModel; CrosslinkBaselinePairFaultyInitN4F2ForkingModel; CrosslinkBaselineTripleFaultyInitN4F2ForkingModel; CrosslinkBaselineBoundedFaultyInitN5F2ForkingModel; CrosslinkBaselineSingleFaultyInitN5F2ForkingModel; CrosslinkBaselinePairFaultyInitN5F2ForkingModel; CrosslinkBaselineTripleFaultyInitN5F2ForkingModel; CrosslinkBaselineBoundedFaultyInitN7F2ForkingModel; CrosslinkBaselineFullFaultyInitForkingModel; CrosslinkBaselineFullFaultyInitN4F2ForkingModel; CrosslinkBaselineFullFaultyInitN5F1ForkingModel; CrosslinkBaselineFullFaultyInitN5F2ForkingModel; CrosslinkBaselineFullFaultyInitN7F2ForkingModel; BaselineFaultyInitSafety; BaselineForkingFaultyInitSafety; BaselineBoundedN4F2ForkingFaultyInitSafety; BaselineSingleN4F2ForkingFaultyInitSafety; BaselinePairN4F2ForkingFaultyInitSafety; BaselineTripleN4F2ForkingFaultyInitSafety; BaselineBoundedN5F2ForkingFaultyInitSafety; BaselineSingleN5F2ForkingFaultyInitSafety; BaselinePairN5F2ForkingFaultyInitSafety; BaselineTripleN5F2ForkingFaultyInitSafety; BaselineBoundedN7F2ForkingFaultyInitSafety; BaselineFullForkingFaultyInitSafety; BaselineFullN4F2ForkingFaultyInitSafety; BaselineFullN5F1ForkingFaultyInitSafety; BaselineFullN5F2ForkingFaultyInitSafety; BaselineFullN7F2ForkingFaultyInitSafety Partial; exposed through the parameterized shell and covered by a tiny full-powerset instance, bounded symbolic fixed-sigma/forking instances for n4_f1, representative n4_f2, n5_f2, and n7_f2, single-/pair-/triple-faulty full-domain n4_f2 abstractions, single-/pair-/triple-faulty full-domain n5_f2 abstractions, and quick-check full-powerset fixed-sigma/forking n4_f1, n4_f2, n5_f1, n5_f2, and n7_f2 instances; not yet lifted into symbolic gates for the full-powerset larger instances
Full Tendermint transition surface Current model covers StartRound-style round initialization, value prevote quorum, nil prevote quorum, split nil-valid-round and concrete-valid-round proposal handlers, validRound proposal justification, correct-value-prevote proposal provenance, propose/prevote/precommit timeout paths, stream-change nil precommit, late nil-precommit certificate handling as a disabled sticky-baseline alias, round advance after precommit quorum, timeout round advance, future-round catchup, and decision Covered for the focused baseline shell; still not a full upstream port
Full agreement/validity/accountability invariant suite over arbitrary evidence Safety includes Agreement, Validity, and Accountability in the focused symbolic gates; current suite also has bounded faulty-init gates plus focused accountability witnesses Partial; larger full-powerset faulty-evidence surfaces are still quick-check-only rather than symbolic
False-invariant/counterexample harnesses for amnesia/equivocation/agreement CrosslinkBaselineCounterexampleModel; falseNoConflictingCommitsInvariantFailsTest; falseNoAmnesiaEvidenceInvariantFailsTest; falseNoEquivocationEvidenceInvariantFailsTest; falseAgreementInvariantFailsTest; falseAgreementOrAmnesiaInvariantFailsTest; falseAmnesiaImpliesEquivocationInvariantFailsTest; falseShowMeAmnesiaWithoutEquivocationInvariantFailsTest; falseNeverUndecidedInMaxRoundInvariantFailsTest; falseNilPrecommitClearsSameRoundLockInvariantFailsTest Covered as seeded Rust witnesses; not yet an arbitrary-evidence symbolic suite
Crosslink-specific false-invariant witnesses falseNoStaleFixedSigmaProposalInvariantFailsTest; falseForkFinalityAttemptIsValidTest; falseNilPrecommitClearsSameRoundLockInvariantFailsTest Covered as seeded Rust witnesses
Generated/adversarial PoW schedule for baseline long reorgs CrosslinkBaselinePowSampling.qnt; CrosslinkBaselinePowSamplingModel; CrosslinkBaselinePowLongReorgModel; CrosslinkBaselinePowGeneratedScheduleModel; CrosslinkBaselinePowRepeatedGeneratedScheduleModel; BaselinePowSamplingSafety; BaselinePowLongReorgSafety; BaselinePowGeneratedScheduleSafety; BaselinePowRepeatedGeneratedScheduleSafety Covered, bounded; fork-switch, long-reorg, generated adversarial work-competition, and repeated generated stream-change fixtures are covered
Stochastic PoW block-production model CrosslinkBaselinePowStochasticProductionModel; BaselinePowStochasticProductionSafety Covered, bounded; finite hash-participation, hidden-work-risk, and block-variance buckets derive honest extension and hidden-work release windows
Inductive or deeper multi-height finality argument Current BFT-height model is bounded Partial

Remaining Work

To finish a focused baseline artifact, the remaining work is:

  1. Keep quick-baseline, symbolic-baseline-core, and symbolic-baseline-accountability green locally and in CI for the current commit.
  2. Keep the baseline limitation explicit: stream changes between prevote and precommit can leave the sticky baseline carrying a stale fixed-sigma sample and can halt fresh finality.
  3. Keep the upstream-shaped shell documented as a shell, not as a complete port of the upstream Tendermint transition system.

To finish an upstream-quality baseline spec, the remaining work is larger:

Use baseline-upstream-crosswalk.md as the controlling checklist for the remaining upstream-quality gaps.

  1. Extend the parameterized baseline shell into a full transition model.
    • Keep the current upstream-shaped constants and assumptions: Corr, Faulty, N, T, value sets, MaxRound, Proposer, sigma, best tips, heights, and ancestors.
    • Continue to keep Crosslink value selection as Stream(round) = ancestor(bestTip(round), height(bestTip(round)) - sigma) rather than an arbitrary Tendermint value.
  2. Add the remaining baseline model instances.
    • The branch now includes focused n4_f2, n5_f2, and n7_f2 instances without violating the focused shell's size(Faulty) <= T assumption silently.
    • The n4_f2 and n5_f2 witnesses document why those smaller f = 2 layouts are above the live fault boundary for correct-only value commits; n7_f2 records the corresponding 2f+1 correct decision path.
    • The f = 2 safety invariants are now symbolic-baseline gates at max depth 3; deeper bounds remain a tractability question.
  3. Add upstream-style faulty message injection.
    • The parameterized shell now exposes BaselineInitWithFaultyEvidence, BaselineNext, BaselineFaultyInitDomainWellFormed, and BaselineFaultyInitSafety for nondeterministically seeded faulty proposals, prevotes, and precommits in Init.
    • Ensure proposal, prevote, and precommit evidence is carried into Crosslink accountability predicates.
    • The full faulty-init powerset is now exercised for the fixed-sigma/forking n4_f1, n4_f2, n5_f1, n5_f2, and n7_f2 instances in quick-baseline.
    • The single-/pair-/triple-faulty n4_f2 harnesses symbolically range over one to three arbitrary faulty proposals, prevotes, and precommits from the full n4_f2 domain at max depth 2, and single-/pair-/triple-faulty n5_f2 harnesses range over the larger full n5_f2 domain at max depth 2; remaining work is broader symbolic coverage that stays tractable. A local Apalache probe of CrosslinkBaselineFullFaultyInitN4F2ForkingModel at max depth 1 exhausted the default 4GB JVM heap, so the current quick-only boundary for larger full-powerset instances is a real tractability limit rather than an omitted proof gate.
  4. Port the full transition surface.
    • Include proposer selection, proposal insertion, proposal handling, prevote quorum handling, precommit quorum handling, timeouts, nil prevotes, and round catchup. The current baseline model now covers StartRound-style round initialization plus the timeout and catchup paths for the focused shell.
    • Preserve the baseline sticky rule: nil precommit does not clear same-round valid or locked state.
  5. Strengthen properties.
    • The focused and larger-instance Safety gates already include agreement, validity, and accountability; remaining work is to broaden the arbitrary-evidence/accountability shape that can be checked symbolically.
    • Keep Crosslink finalized-prefix safety separate from Tenderlink agreement so failures are easier to diagnose.
  6. Add deeper Crosslink-specific false-invariant/counterexample modules.
    • The upstream-shaped negative checks for amnesia, equivocation, agreement, agreement-or-amnesia, amnesia-implies-equivocation, amnesia-without-equivocation, and undecided max-round behavior now have seeded witnesses.
    • Seeded witnesses now also cover the false claims that sticky baseline proposals always match the current fixed-sigma sample and that fork-finality attempts remain valid after prefix finality.
    • Remaining work is to broaden those Crosslink-specific false invariants beyond hand-authored fixtures.
  7. Expand the PoW environment.
    • The baseline now has a generated bounded PoW schedule where published work selects a3, then a4, then an adversarially released b4.
    • The baseline now has a repeated generated bounded PoW schedule where published work selects a3, then a4, then adversarially released b4, then adversarially released c4.
    • A long-reorg fixture now covers rollback depth 3 with sigma 2, where the sticky baseline carries the stale head - sigma sample across the fork switch.
    • A finite stochastic-production fixture now buckets hash-power participation, hidden-work risk, and block-time variance, then derives honest extension and hidden-work release windows from those buckets.
  8. Push finality beyond a bounded fixture.
    • Either add deeper symbolic projection checks or split the model into smaller lemmas that make the multi-height finalized-prefix argument more obviously inductive.

Recommended Order

  1. Broaden upstream-style faulty message injection beyond the current shell aliases and focused harnesses.
    • Existing focused witnesses prove that manually seeded faulty proposal, prevote, and precommit evidence reaches equivocation predicates.
    • A tiny proof-gated InitWithFaultyEvidence harness now covers nondeterministic faulty proposal, prevote, and precommit powersets.
    • A fixed-sigma/forking n4_f1 harness now covers the same init shape with a bounded faulty-evidence domain.
    • Representative bounded n4_f2, n5_f2, and n7_f2 harnesses now carry the same idea into f=2 symbolic checks without expanding the full powerset.
    • Single-/pair-/triple-faulty n4_f2 harnesses now symbolically range over the full faulty-evidence domain while bounding the selected evidence set size.
    • Single-/pair-/triple-faulty n5_f2 harnesses now range over the larger full faulty-evidence domain at max depth 2.
    • The parameterized shell now exposes the full faulty-init surface and transition step through baseline-prefixed aliases.
    • Quick-check-only fixed-sigma/forking n4_f1, n4_f2, n5_f1, n5_f2, and n7_f2 harnesses now cover the full faulty-evidence domain. The missing piece is finding tractable symbolic abstractions beyond the representative bounded and selected-evidence domains.
  2. Continue porting the full Tendermint transition surface into the parameterized shell while preserving the baseline sticky nil-precommit rule. The shell now exposes baseline-prefixed aliases for the focused transition surface, but it is still not an upstream-identical transition system.
  3. Broaden the full-powerset faulty-evidence shapes that can be checked symbolically, without dropping agreement, validity, or accountability from the checked invariant.
  4. Keep watching CI runtime as the symbolic frontier expands. The workflow now splits baseline symbolic checks into core and accountability matrix jobs, so additional work should preserve that parallel structure or add further slices before the 20-minute timeout becomes tight again.

Completion Standard

The baseline should be treated as complete only when:

  • the focused baseline witnesses still pass
  • the upstream-shaped parameterized baseline model typechecks
  • every baseline model instance has quick witness coverage
  • bounded symbolic checks pass for agreement, validity, accountability, fixed-sigma sampling, and finalized-prefix safety
  • the counterexample harnesses produce the expected failures
  • CI runs the quick and symbolic baseline gates on the personal fork
  • the issue and gist link to the current spec folder and this audit

Baseline Crosslink / Upstream Tendermint Crosswalk

This crosswalk maps the fixed-sigma/sticky baseline Crosslink spec against the current upstream Tendermint Quint example. It is meant to keep the baseline work honest about which parts are already modeled, which parts are intentionally Crosslink-specific, and which parts are still incomplete.

Upstream Snapshot

The upstream reference checked for this crosswalk was:

https://github.com/informalsystems/quint/tree/main/examples/cosmos/tendermint
commit db6f80b2120a3cda86ae577c6f99e322f70d6a9d
checked 2026-05-19

The reference directory contains:

  • Tendermint.qnt
  • TendermintModels.qnt
  • TendermintTest.qnt
  • README.md
  • the source TLA+ accountability spec under tla/

The README describes Tendermint.qnt as a Quint version of the CometBFT accountability TLA+ specification.

Crosslink Baseline Rule

The baseline is not a verbatim Tendermint value model. It preserves the current Crosslink/Tenderlink behavior:

BaselineStream(round) =
  ancestor(bestTip(round), height(bestTip(round)) - sigma)

Consensus values are fixed-sigma PoW snapshots. The sticky baseline keeps ResampleOnNilPrecommit = false, so a nil-precommit quorum advances the round without clearing same-round value or proposal-cache state.

Surface Crosswalk

Upstream Tendermint surface Baseline Crosslink artifact Status Notes
Parameter sets Corr, Faulty, N, T CrosslinkBaselineTenderlink.qnt exposes BaselineCorr, BaselineFaulty, BaselineN, BaselineT; CrosslinkResampling.qnt imports them as Corr, Faulty, N, T Covered The focused model assumes size(Faulty) <= T and quorum intersection explicitly.
Value sets ValidValues, InvalidValues BaselineValidSnapshots, BaselineInvalidSnapshots, Snapshots, NilSnapshot Covered with Crosslink specialization Values are PoW snapshots, not arbitrary application values.
Round/proposer parameters MaxRound, Proposer BaselineMaxRound, BaselineProposer, Rounds, Proposer Covered Small instances use the upstream-style proposer schedule.
Crosslink value selection BaselineSigma, BaselineBestTip, BaselineHeight, BaselineAncestorAt, BaselineHeadMinusSigma, BaselineStream Crosslink-specific addition This is the main baseline deviation from upstream Tendermint.
Consensus state round, step, decision, lockedValue, lockedRound, validValue, validRound Same state in CrosslinkResampling.qnt Covered Baseline also adds cachedProposal and cachedProposalRound for sticky Crosslink proposal carryover.
Message/evidence state for proposals, prevotes, precommits msgsPropose, msgsPrevote, msgsPrecommit, evidencePropose, evidencePrevote, evidencePrecommit Covered Evidence is used by equivocation, amnesia, and Crosslink-specific accountability witnesses.
Faulty proposal/prevote/precommit domains FaultyProposals, FaultyPrevotes, FaultyPrecommits, AllFaulty* Covered structurally The full domains exist in the shared model.
Nondeterministic faulty message injection in Init BaselineInitWithFaultyEvidence, BaselineNext, BaselineFaultyInitSafety, CrosslinkBaselineParameterizedShellTest, InitWithFaultyEvidence, InitWithSingleN4F2FaultyEvidence, InitWithPairN4F2FaultyEvidence, InitWithTripleN4F2FaultyEvidence, InitWithSingleN5F2FaultyEvidence, InitWithPairN5F2FaultyEvidence, InitWithTripleN5F2FaultyEvidence, CrosslinkBaselineFaultyInitTinyModel, CrosslinkBaselineFaultyInitForkingModel, CrosslinkBaselineBoundedFaultyInitN4F2ForkingModel, CrosslinkBaselineSingleFaultyInitN4F2ForkingModel, CrosslinkBaselinePairFaultyInitN4F2ForkingModel, CrosslinkBaselineTripleFaultyInitN4F2ForkingModel, CrosslinkBaselineBoundedFaultyInitN5F2ForkingModel, CrosslinkBaselineSingleFaultyInitN5F2ForkingModel, CrosslinkBaselinePairFaultyInitN5F2ForkingModel, CrosslinkBaselineTripleFaultyInitN5F2ForkingModel, CrosslinkBaselineBoundedFaultyInitN7F2ForkingModel, CrosslinkBaselineFullFaultyInitForkingModel, CrosslinkBaselineFullFaultyInitN4F2ForkingModel, CrosslinkBaselineFullFaultyInitN5F1ForkingModel, CrosslinkBaselineFullFaultyInitN5F2ForkingModel, CrosslinkBaselineFullFaultyInitN7F2ForkingModel Partial Exposed through the parameterized baseline shell and covered in a tiny full-powerset harness, bounded symbolic forking harnesses for n4_f1 plus representative n4_f2, n5_f2, and n7_f2, single-/pair-/triple-faulty full-domain n4_f2 symbolic abstractions, single-/pair-/triple-faulty full-domain n5_f2 symbolic abstractions, and full-powerset quick-check forking n4_f1, n4_f2, n5_f1, n5_f2, and n7_f2 harnesses; not lifted into symbolic checking for every full-powerset larger instance.
StartRound StartRound; BaselineStartRound; parameterizedShellStartRoundAdvancesToProposeTest; StartNextRoundAfterPrecommitQuorum, TimeoutPrecommitStartNextRound, CatchUpToRound Covered with Crosslink specialization Baseline now exposes a named upstream-shaped round-initialization helper through the shell. The round-advance transitions still wrap Crosslink-specific nil-certificate and catchup preconditions.
BroadcastProposal, BroadcastPrevote, BroadcastPrecommit Same named broadcast actions Covered Message evidence is updated alongside observed messages.
InsertProposal(p, v) InsertProposal(p) using StickyOrStreamProposal(p) Intentional Crosslink deviation A correct Crosslink proposer samples Stream(round) or reuses sticky cached/valid state; it does not choose arbitrary v.
Proposal handling in propose step UponProposalInPropose; UponProposalInProposeAndPrevote; UponProposalPrevote; BaselineUponProposalInPropose; BaselineUponProposalInProposeAndPrevote; BaselineUponProposalPrevote; HasPrevoteJustifiedProposal; CorrectValuePrevotesHaveJustifiedProposal Covered with Crosslink specialization Covers the upstream-shaped split between nil-valid-round proposals and proposals justified by an earlier prevote quorum, while preserving Crosslink freshness and lock checks.
Upstream valid-round proposal path validValue, validRound, StickyOrStreamProposal, HasNilValidRoundProposal, HasConcreteValidRoundProposal, HasPrevoteJustifiedProposal, CorrectValuePrevotesHaveJustifiedProposal, BaselineValidValueOf, BaselineValidRoundOf, BaselineLockedValueOf, BaselineLockedRoundOf, validRoundProposalWithoutPrevoteQuorumIsRejectedTest, validRoundProposalWithPrevoteQuorumIsAcceptedTest, nilValidRoundProposalHandlerPrevotesTest, validRoundProposalHandlerWithoutPrevoteQuorumIsRejectedTest, validRoundProposalHandlerWithPrevoteQuorumIsAcceptedTest, correctValuePrevotesRequireJustifiedProposalTest, parameterizedShellConcreteValidRoundAliasAcceptsPrevoteQuorumTest Covered with Crosslink specialization Baseline preserves lock/valid state and requires concrete validRound proposals to be backed by an earlier prevote quorum; the Crosslink-equivalent safety lemma is part of Safety, and Next now uses the split proposal handlers.
Any prevote quorum handling UponValuePrevoteQuorum, UponNilPrevoteQuorum, TimeoutPrevotePrecommitNil, BaselineUponValuePrevoteQuorum, BaselineUponNilPrevoteQuorum, BaselineTimeoutPrevotePrecommitNil Covered for focused baseline shell Value prevote quorums lock and precommit; nil prevote quorums precommit nil.
Any precommit quorum handling StartNextRoundAfterPrecommitQuorum, Decide, ApplyLateNilPrecommitCertificate, BaselineStartNextRoundAfterPrecommitQuorum, BaselineApplyLateNilPrecommitCertificate, BaselineDecide, parameterizedShellLateNilCertificateAliasStaysDisabledTest Partial Baseline distinguishes value commit decision from nil/any-precommit round advance, and explicitly exposes late nil-precommit certificate handling as disabled under sticky baseline semantics, but does not take arbitrary evidence-set parameters like upstream.
Timeout propose TimeoutProposePrevoteNil, BaselineTimeoutProposePrevoteNil Covered Correct processes can prevote nil when the proposal step times out.
Timeout precommit TimeoutPrecommitStartNextRound, BaselineTimeoutPrecommitStartNextRound Covered Correct processes can advance rounds without a precommit quorum.
Nil prevote quorum UponNilPrevoteQuorum, BaselineUponNilPrevoteQuorum Covered This is one of the explicit baseline witnesses.
Round catchup RoundCatchupEvidence, CatchUpToRound, BaselineRoundCatchupEvidence, BaselineCatchUpToRound Covered for focused shell Catchup requires T + 1 observed activity in the target round.
System transition Next CrosslinkResampling.qnt Next; CrosslinkBaselineTenderlink.qnt BaselineNext; baseline-prefixed focused transition aliases Partial Includes focused Crosslink proposal, vote, timeout, nil, stream-change, disabled late-nil-certificate, catchup, and decision paths behind baseline-prefixed shell aliases; still not an upstream-identical transition surface.
Agreement Agreement, BaselineAgreement, symbolic BaselineSafety/ComposedSafety gates Covered, bounded Agreement is checked directly in the focused and composed baseline gates.
Validity Validity, BaselineValidity, Safety Covered, bounded Valid decisions must be modeled snapshots.
Accountability EquivocationBy, AmnesiaBy, DetectableFaults, Accountability, ConflictingCommitsAccountable Partial Baseline adapts accountability to Crosslink nil certificates; broader arbitrary-evidence checking remains incomplete.
False invariant examples CrosslinkBaselineCounterexampleModel Covered as seeded witnesses Covers false no-conflicting-commit, no-amnesia, no-equivocation, agreement, agreement-or-amnesia, amnesia-implies-equivocation, amnesia-without-equivocation, undecided max-round, and sticky-baseline nil-precommit unlock witnesses.
Small n4_f1, n4_f2, n5_f2 model handles CrosslinkBaselineModels.qnt has n4_f1_stable, n4_f1_forking, n5_f1_forking, n4_f2_forking, n5_f2_forking, n7_f2_forking Covered with Crosslink variants n4_f2 and n5_f2 are above the live BFT boundary for correct-only value commits under T = 2; n7_f2 records the corresponding decision path.
Normal decision test decisionTest, baselineStableStreamDecidesSampledSnapshotTest, n7F2DecisionPathTest Covered Baseline decision values are stream snapshots.
No double proposal test noProposeTwiceTest Covered Checks a correct proposer cannot insert two proposals for the same round.
Timeout progress test timeoutPrevotePathFormsNilPrecommitCertTest, timeoutPrecommitAdvancesWithoutPrecommitQuorumTest Covered The baseline splits upstream timeout behavior into prevote-nil and precommit-timeout paths.
Crosslink-specific stale-sample negative witness falseNoStaleFixedSigmaProposalInvariantFailsTest Covered as seeded witness Shows the sticky baseline violates the false claim that proposals always equal the current head - sigma sample.
Crosslink-specific fork-finality negative witness falseForkFinalityAttemptIsValidTest Covered as seeded witness Shows a fork-finality attempt is invalid once the prefix has finalized on the other branch.

Proof Gate Crosswalk

Upstream-quality expectation Current baseline gate Status
Typecheck the parameterized model check.sh quick-baseline typechecks all baseline files Covered
Run witness tests check.sh quick-baseline runs baseline, accountability, BFT-height, finality, PoW-sampling, and f = 2 witness modules Covered
Bounded agreement/validity/accountability checks check.sh symbolic-baseline verifies focused baseline safety invariants, and CI splits the same gate through symbolic-baseline-core plus symbolic-baseline-accountability; Safety includes Agreement, Validity, and Accountability Covered, bounded
Faulty init symbolic checking Tiny full-powerset faulty init plus bounded forking faulty init for n4_f1, representative n4_f2/n5_f2/n7_f2, single-/pair-/triple-faulty full-domain n4_f2 abstractions, and single-/pair-/triple-faulty full-domain n5_f2 abstractions; full forking faulty init remains quick-check-only for n4_f1, n4_f2, n5_f1, n5_f2, and n7_f2 Partial
f = 2 symbolic checking symbolic-baseline-core verifies n4_f2, n5_f2, and n7_f2 safety invariants at depth 3; symbolic-baseline-accountability verifies the f=2 faulty-evidence abstractions Covered, bounded
Full arbitrary-evidence accountability checking Focused witnesses, upstream-shaped negative witnesses, and bounded faulty-init gates Partial
Full PoW environment checking Fixed fork switch, long-reorg, generated adversarial work-competition, repeated generated stream-change, finite stochastic-production, and fixed-sigma sampling fixtures Partial; bounded fixtures rather than an unbounded PoW environment
Stochastic or adversarial block production CrosslinkBaselinePowStochasticProductionModel; generated and repeated generated work-competition fixtures Covered, bounded
Inductive multi-height finality proof Bounded BFT-height and composed-finality fixtures Partial

Intentional Deviations

These differences should remain in the Crosslink baseline instead of being "fixed" toward vanilla Tendermint:

  1. Correct proposals are selected from head - sigma, not from an arbitrary valid-value set.
  2. Decision freshness checks require the decided snapshot to match the sampled stream for the decision round.
  3. The sticky baseline keeps same-round locks, valid values, and cached proposals across nil-precommit round advance.
  4. A nil-precommit certificate for the abandoned round is valid unlock evidence for the proposed resampling variant, but not for the sticky baseline.
  5. Crosslink finality has a separate finalized-prefix model at BFT consensus heights; it is not only Tendermint single-height agreement.

Remaining Upstream-Quality Gaps

The crosswalk leaves these concrete gaps:

  1. Lift the focused baseline shell into a fuller parameterized transition model without losing the Crosslink head - sigma value rule.
  2. Broaden the tractable symbolic shape for faulty proposal, prevote, and precommit injection beyond the parameterized shell quick witness, tiny, bounded n4_f1, representative n4_f2/n5_f2/n7_f2, single-/pair-/triple-faulty full-domain n4_f2 harnesses, and single-/pair-/triple-faulty full-domain n5_f2 harnesses. The fixed-sigma/forking n4_f1, n4_f2, n5_f1, n5_f2, and n7_f2 surfaces now have full-powerset quick coverage. A local depth-1 Apalache probe of the full-powerset n4_f2 surface exhausted the default 4GB JVM heap, so the next symbolic step likely needs a smaller abstraction rather than simply adding the full-powerset larger instances to CI.
  3. Broaden arbitrary-evidence symbolic accountability coverage. The current focused and larger-instance Safety gates already include agreement, validity, and accountability, but the larger full-powerset faulty-evidence surfaces are still quick-check-only.
  4. Decide whether any f = 2 symbolic gates should be deepened beyond max depth 3.
  5. Broaden Crosslink-specific false-invariant witnesses beyond the current seeded stale-sample, fork-finality, and sticky nil-precommit fixtures.
  6. Strengthen finalized-prefix reasoning beyond bounded fixtures, either with deeper symbolic projections or smaller lemmas.
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Usage: spec/quint/check.sh [quick|symbolic|all|quick-baseline|symbolic-baseline|symbolic-baseline-core|symbolic-baseline-accountability]
Modes:
quick typecheck all specs, run witness tests, and run Rust safety checks
symbolic run bounded Apalache checks from README.md
all run quick and symbolic
quick-baseline run only the baseline Crosslink quick checks
symbolic-baseline run only the baseline Crosslink bounded Apalache checks
symbolic-baseline-core
run baseline non-accountability bounded Apalache checks
symbolic-baseline-accountability
run baseline accountability bounded Apalache checks
Set QUINT to override the command, for example:
QUINT="node /private/tmp/quint-node26-patched-validround/dist/src/cli.js" spec/quint/check.sh quick
Set APALACHE_PORT_BASE to give each symbolic check a sequential local checker
port, for example:
APALACHE_PORT_BASE=8830 spec/quint/check.sh symbolic-baseline
USAGE
}
mode="${1:-quick}"
if [[
"${mode}" != "quick" &&
"${mode}" != "symbolic" &&
"${mode}" != "all" &&
"${mode}" != "quick-baseline" &&
"${mode}" != "symbolic-baseline" &&
"${mode}" != "symbolic-baseline-core" &&
"${mode}" != "symbolic-baseline-accountability"
]]; then
usage
exit 2
fi
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "${root}"
if [[ -n "${QUINT:-}" ]]; then
read -r -a quint_cmd <<< "${QUINT}"
else
quint_cmd=(quint)
fi
run_quint() {
printf '+'
printf ' %q' "${quint_cmd[@]}" "$@"
printf '\n'
"${quint_cmd[@]}" "$@"
}
typecheck_all() {
local specs=(
spec/quint/CrosslinkBaseline.qnt
spec/quint/CrosslinkBaselineTenderlink.qnt
spec/quint/CrosslinkBaselineModels.qnt
spec/quint/CrosslinkBaselineTest.qnt
spec/quint/CrosslinkBaselineAccountability.qnt
spec/quint/CrosslinkBaselineBftHeights.qnt
spec/quint/CrosslinkBaselineFinality.qnt
spec/quint/CrosslinkBaselinePowSampling.qnt
spec/quint/CrosslinkResampling.qnt
spec/quint/CrosslinkForkFinality.qnt
spec/quint/CrosslinkPowForkSchedule.qnt
spec/quint/CrosslinkPowBranchCompetition.qnt
spec/quint/CrosslinkComposed.qnt
spec/quint/CrosslinkBftHeights.qnt
spec/quint/CrosslinkDynamicSigma.qnt
spec/quint/CrosslinkDynamicSigmaCalibration.qnt
spec/quint/CrosslinkDynamicSigmaTelemetry.qnt
spec/quint/CrosslinkDynamicSigmaHysteresis.qnt
spec/quint/CrosslinkDynamicSigmaForkSchedule.qnt
spec/quint/CrosslinkDynamicSigmaBranchCompetition.qnt
spec/quint/CrosslinkDynamicSigmaResampling.qnt
spec/quint/CrosslinkDynamicSigmaFinality.qnt
)
for spec in "${specs[@]}"; do
run_quint typecheck "${spec}"
done
}
typecheck_baseline() {
local specs=(
spec/quint/CrosslinkResampling.qnt
spec/quint/CrosslinkBaseline.qnt
spec/quint/CrosslinkBaselineTenderlink.qnt
spec/quint/CrosslinkBaselineModels.qnt
spec/quint/CrosslinkBaselineTest.qnt
spec/quint/CrosslinkBaselineAccountability.qnt
spec/quint/CrosslinkBaselineBftHeights.qnt
spec/quint/CrosslinkBaselineFinality.qnt
spec/quint/CrosslinkBaselinePowSampling.qnt
)
for spec in "${specs[@]}"; do
run_quint typecheck "${spec}"
done
}
test_model() {
local spec="$1"
local main="$2"
run_quint test "${spec}" --main="${main}" --max-samples=100 --backend=rust
}
run_model() {
local spec="$1"
local main="$2"
local init="$3"
local step="$4"
local max_steps="$5"
local max_samples="$6"
local invariant="$7"
run_quint run "${spec}" \
--main="${main}" \
--init="${init}" \
--step="${step}" \
--max-steps="${max_steps}" \
--max-samples="${max_samples}" \
--invariant="${invariant}" \
--backend=rust \
--verbosity=0
}
verify_count=0
verify_model() {
local spec="$1"
local main="$2"
local max_steps="$3"
local init="$4"
local step="$5"
local invariant="$6"
local args=(
verify "${spec}"
--main="${main}" \
--max-steps="${max_steps}" \
--init="${init}" \
--step="${step}" \
--invariant="${invariant}"
)
if [[ -n "${APALACHE_PORT_BASE:-}" ]]; then
local port=$((APALACHE_PORT_BASE + verify_count))
verify_count=$((verify_count + 1))
args+=(--server-endpoint="localhost:${port}")
fi
run_quint "${args[@]}"
}
quick_checks() {
typecheck_all
test_model spec/quint/CrosslinkResampling.qnt CrosslinkStickyModel
test_model spec/quint/CrosslinkBaseline.qnt CrosslinkBaselineStableModel
test_model spec/quint/CrosslinkBaseline.qnt CrosslinkBaselineStreamChangeModel
test_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineParameterizedShellTest
test_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN4F1StableTest
test_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN4F1ForkingTest
test_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN4F2ForkingTest
test_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN5F2ForkingTest
test_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN7F2ForkingTest
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineAccountabilityModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFaultyInitTinyModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFaultyInitForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineBoundedFaultyInitN4F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineSingleFaultyInitN4F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselinePairFaultyInitN4F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineTripleFaultyInitN4F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineBoundedFaultyInitN5F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineSingleFaultyInitN5F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselinePairFaultyInitN5F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineTripleFaultyInitN5F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineBoundedFaultyInitN7F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFullFaultyInitForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFullFaultyInitN4F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFullFaultyInitN5F1ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFullFaultyInitN5F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFullFaultyInitN7F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineCounterexampleModel
test_model spec/quint/CrosslinkBaselineBftHeights.qnt CrosslinkBaselineBftHeightsModel
test_model spec/quint/CrosslinkBaselineFinality.qnt CrosslinkBaselineFinalityStableModel
test_model spec/quint/CrosslinkBaselineFinality.qnt CrosslinkBaselineFinalityStreamChangeModel
test_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowSamplingModel
test_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowLongReorgModel
test_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowGeneratedScheduleModel
test_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowRepeatedGeneratedScheduleModel
test_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowStochasticProductionModel
test_model spec/quint/CrosslinkResampling.qnt CrosslinkNilResamplingModel
test_model spec/quint/CrosslinkForkFinality.qnt CrosslinkForkFinalityModel
test_model spec/quint/CrosslinkPowForkSchedule.qnt CrosslinkPowForkScheduleModel
test_model spec/quint/CrosslinkPowBranchCompetition.qnt CrosslinkPowBranchCompetitionModel
test_model spec/quint/CrosslinkComposed.qnt CrosslinkComposedResamplingModel
test_model spec/quint/CrosslinkBftHeights.qnt CrosslinkBftHeightsModel
test_model spec/quint/CrosslinkDynamicSigma.qnt CrosslinkDynamicSigmaHashParticipationModel
test_model spec/quint/CrosslinkDynamicSigmaCalibration.qnt CrosslinkDynamicSigmaCalibrationModel
test_model spec/quint/CrosslinkDynamicSigmaTelemetry.qnt CrosslinkDynamicSigmaTelemetryModel
test_model spec/quint/CrosslinkDynamicSigmaHysteresis.qnt CrosslinkDynamicSigmaHysteresisModel
test_model spec/quint/CrosslinkDynamicSigmaForkSchedule.qnt CrosslinkDynamicSigmaForkScheduleModel
test_model spec/quint/CrosslinkDynamicSigmaBranchCompetition.qnt CrosslinkDynamicSigmaBranchCompetitionModel
test_model spec/quint/CrosslinkDynamicSigmaResampling.qnt CrosslinkDynamicSigmaResamplingModel
test_model spec/quint/CrosslinkDynamicSigmaFinality.qnt CrosslinkDynamicSigmaFinalityModel
run_model spec/quint/CrosslinkResampling.qnt CrosslinkStickyModel Init Next 10 1000 Safety
run_model spec/quint/CrosslinkBaseline.qnt CrosslinkBaselineStableModel Init Next 10 1000 BaselineSafety
run_model spec/quint/CrosslinkBaseline.qnt CrosslinkBaselineStreamChangeModel Init Next 10 1000 BaselineSafety
run_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineParameterizedShellTest BaselineInitWithFaultyEvidence BaselineNext 2 100 BaselineFaultyInitSafety
run_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN4F1StableTest Init Next 10 1000 BaselineN4F1StableSafety
run_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN4F1ForkingTest Init Next 10 1000 BaselineN4F1ForkingSafety
run_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN4F2ForkingTest Init Next 10 1000 BaselineN4F2ForkingSafety
run_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN5F2ForkingTest Init Next 10 1000 BaselineN5F2ForkingSafety
run_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN7F2ForkingTest Init Next 10 1000 BaselineN7F2ForkingSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineAccountabilityModel Init Next 10 1000 BaselineAccountabilitySafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFaultyInitTinyModel InitWithFaultyEvidence Next 2 1000 BaselineFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFaultyInitForkingModel InitWithBoundedForkingFaultyEvidence Next 2 1000 BaselineForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineBoundedFaultyInitN4F2ForkingModel InitWithRepresentativeN4F2FaultyEvidence Next 2 1000 BaselineBoundedN4F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineSingleFaultyInitN4F2ForkingModel InitWithSingleN4F2FaultyEvidence Next 2 1000 BaselineSingleN4F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselinePairFaultyInitN4F2ForkingModel InitWithPairN4F2FaultyEvidence Next 2 1000 BaselinePairN4F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineTripleFaultyInitN4F2ForkingModel InitWithTripleN4F2FaultyEvidence Next 2 1000 BaselineTripleN4F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineBoundedFaultyInitN5F2ForkingModel InitWithRepresentativeN5F2FaultyEvidence Next 2 1000 BaselineBoundedN5F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineSingleFaultyInitN5F2ForkingModel InitWithSingleN5F2FaultyEvidence Next 2 1000 BaselineSingleN5F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselinePairFaultyInitN5F2ForkingModel InitWithPairN5F2FaultyEvidence Next 2 1000 BaselinePairN5F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineTripleFaultyInitN5F2ForkingModel InitWithTripleN5F2FaultyEvidence Next 2 1000 BaselineTripleN5F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineBoundedFaultyInitN7F2ForkingModel InitWithRepresentativeN7F2FaultyEvidence Next 2 1000 BaselineBoundedN7F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFullFaultyInitForkingModel InitWithFaultyEvidence Next 2 100 BaselineFullForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFullFaultyInitN4F2ForkingModel InitWithFaultyEvidence Next 2 100 BaselineFullN4F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFullFaultyInitN5F1ForkingModel InitWithFaultyEvidence Next 2 100 BaselineFullN5F1ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFullFaultyInitN5F2ForkingModel InitWithFaultyEvidence Next 2 100 BaselineFullN5F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFullFaultyInitN7F2ForkingModel InitWithFaultyEvidence Next 2 100 BaselineFullN7F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineBftHeights.qnt CrosslinkBaselineBftHeightsModel Init Next 5 1000 BaselineBftHeightSafety
run_model spec/quint/CrosslinkBaselineFinality.qnt CrosslinkBaselineFinalityStableModel ComposedInit ComposedNext 10 1000 ComposedSafety
run_model spec/quint/CrosslinkBaselineFinality.qnt CrosslinkBaselineFinalityStreamChangeModel ComposedInit ComposedNext 10 1000 ComposedSafety
run_model spec/quint/CrosslinkBaselineFinality.qnt CrosslinkBaselineFinalityLivenessModel LivenessInit LivenessStep 9 1 LivenessSafety
run_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowSamplingModel Init Next 10 1000 BaselinePowSamplingSafety
run_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowLongReorgModel Init Next 10 1000 BaselinePowLongReorgSafety
run_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowGeneratedScheduleModel Init Next 10 1000 BaselinePowGeneratedScheduleSafety
run_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowRepeatedGeneratedScheduleModel Init Next 10 1000 BaselinePowRepeatedGeneratedScheduleSafety
run_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowStochasticProductionModel Init Next 10 1000 BaselinePowStochasticProductionSafety
run_model spec/quint/CrosslinkResampling.qnt CrosslinkNilResamplingModel Init Next 10 1000 Safety
run_model spec/quint/CrosslinkForkFinality.qnt CrosslinkForkFinalityModel Init Next 6 1000 Safety
run_model spec/quint/CrosslinkPowForkSchedule.qnt CrosslinkPowForkScheduleModel Init Next 4 1000 Safety
run_model spec/quint/CrosslinkPowBranchCompetition.qnt CrosslinkPowBranchCompetitionModel Init Next 4 1000 Safety
run_model spec/quint/CrosslinkResampling.qnt CrosslinkNilResamplingLivenessModel LivenessInit LivenessStep 15 1 LivenessSafety
run_model spec/quint/CrosslinkComposed.qnt CrosslinkComposedResamplingModel ComposedInit ComposedNext 10 1000 ComposedSafety
run_model spec/quint/CrosslinkComposed.qnt CrosslinkComposedLivenessModel LivenessInit LivenessStep 16 1 LivenessSafety
run_model spec/quint/CrosslinkBftHeights.qnt CrosslinkBftHeightsModel Init Next 5 1000 Safety
run_model spec/quint/CrosslinkDynamicSigma.qnt CrosslinkDynamicSigmaHashParticipationModel Init Next 7 1000 Safety
run_model spec/quint/CrosslinkDynamicSigmaCalibration.qnt CrosslinkDynamicSigmaCalibrationModel Init Next 8 1000 Safety
run_model spec/quint/CrosslinkDynamicSigmaTelemetry.qnt CrosslinkDynamicSigmaTelemetryModel Init Next 8 1000 Safety
run_model spec/quint/CrosslinkDynamicSigmaHysteresis.qnt CrosslinkDynamicSigmaHysteresisModel Init Next 5 1000 Safety
run_model spec/quint/CrosslinkDynamicSigmaForkSchedule.qnt CrosslinkDynamicSigmaForkScheduleModel DerivedInit DerivedNext 4 1000 DerivedSafety
run_model spec/quint/CrosslinkDynamicSigmaBranchCompetition.qnt CrosslinkDynamicSigmaBranchCompetitionModel BranchCompetitionDynamicInit BranchCompetitionDynamicNext 4 1000 BranchCompetitionDynamicSafety
run_model spec/quint/CrosslinkDynamicSigmaResampling.qnt CrosslinkDynamicSigmaResamplingModel DynamicResamplingInit DynamicResamplingNext 8 1000 DynamicResamplingSafety
run_model spec/quint/CrosslinkDynamicSigmaFinality.qnt CrosslinkDynamicSigmaFinalityModel FullComposedInit FullComposedNext 10 1000 FullComposedSafety
}
baseline_quick_checks() {
typecheck_baseline
test_model spec/quint/CrosslinkResampling.qnt CrosslinkStickyModel
test_model spec/quint/CrosslinkBaseline.qnt CrosslinkBaselineStableModel
test_model spec/quint/CrosslinkBaseline.qnt CrosslinkBaselineStreamChangeModel
test_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineParameterizedShellTest
test_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN4F1StableTest
test_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN4F1ForkingTest
test_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN4F2ForkingTest
test_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN5F2ForkingTest
test_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN7F2ForkingTest
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineAccountabilityModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFaultyInitTinyModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFaultyInitForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineBoundedFaultyInitN4F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineSingleFaultyInitN4F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselinePairFaultyInitN4F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineTripleFaultyInitN4F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineBoundedFaultyInitN5F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineSingleFaultyInitN5F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselinePairFaultyInitN5F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineTripleFaultyInitN5F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineBoundedFaultyInitN7F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFullFaultyInitForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFullFaultyInitN4F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFullFaultyInitN5F1ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFullFaultyInitN5F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFullFaultyInitN7F2ForkingModel
test_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineCounterexampleModel
test_model spec/quint/CrosslinkBaselineBftHeights.qnt CrosslinkBaselineBftHeightsModel
test_model spec/quint/CrosslinkBaselineFinality.qnt CrosslinkBaselineFinalityStableModel
test_model spec/quint/CrosslinkBaselineFinality.qnt CrosslinkBaselineFinalityStreamChangeModel
test_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowSamplingModel
test_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowLongReorgModel
test_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowGeneratedScheduleModel
test_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowRepeatedGeneratedScheduleModel
test_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowStochasticProductionModel
run_model spec/quint/CrosslinkResampling.qnt CrosslinkStickyModel Init Next 10 1000 Safety
run_model spec/quint/CrosslinkBaseline.qnt CrosslinkBaselineStableModel Init Next 10 1000 BaselineSafety
run_model spec/quint/CrosslinkBaseline.qnt CrosslinkBaselineStreamChangeModel Init Next 10 1000 BaselineSafety
run_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineParameterizedShellTest BaselineInitWithFaultyEvidence BaselineNext 2 100 BaselineFaultyInitSafety
run_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN4F1StableTest Init Next 10 1000 BaselineN4F1StableSafety
run_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN4F1ForkingTest Init Next 10 1000 BaselineN4F1ForkingSafety
run_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN4F2ForkingTest Init Next 10 1000 BaselineN4F2ForkingSafety
run_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN5F2ForkingTest Init Next 10 1000 BaselineN5F2ForkingSafety
run_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN7F2ForkingTest Init Next 10 1000 BaselineN7F2ForkingSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineAccountabilityModel Init Next 10 1000 BaselineAccountabilitySafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFaultyInitTinyModel InitWithFaultyEvidence Next 2 1000 BaselineFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFaultyInitForkingModel InitWithBoundedForkingFaultyEvidence Next 2 1000 BaselineForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineBoundedFaultyInitN4F2ForkingModel InitWithRepresentativeN4F2FaultyEvidence Next 2 1000 BaselineBoundedN4F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineSingleFaultyInitN4F2ForkingModel InitWithSingleN4F2FaultyEvidence Next 2 1000 BaselineSingleN4F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselinePairFaultyInitN4F2ForkingModel InitWithPairN4F2FaultyEvidence Next 2 1000 BaselinePairN4F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineTripleFaultyInitN4F2ForkingModel InitWithTripleN4F2FaultyEvidence Next 2 1000 BaselineTripleN4F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineBoundedFaultyInitN5F2ForkingModel InitWithRepresentativeN5F2FaultyEvidence Next 2 1000 BaselineBoundedN5F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineSingleFaultyInitN5F2ForkingModel InitWithSingleN5F2FaultyEvidence Next 2 1000 BaselineSingleN5F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselinePairFaultyInitN5F2ForkingModel InitWithPairN5F2FaultyEvidence Next 2 1000 BaselinePairN5F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineTripleFaultyInitN5F2ForkingModel InitWithTripleN5F2FaultyEvidence Next 2 1000 BaselineTripleN5F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineBoundedFaultyInitN7F2ForkingModel InitWithRepresentativeN7F2FaultyEvidence Next 2 1000 BaselineBoundedN7F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFullFaultyInitForkingModel InitWithFaultyEvidence Next 2 100 BaselineFullForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFullFaultyInitN4F2ForkingModel InitWithFaultyEvidence Next 2 100 BaselineFullN4F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFullFaultyInitN5F1ForkingModel InitWithFaultyEvidence Next 2 100 BaselineFullN5F1ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFullFaultyInitN5F2ForkingModel InitWithFaultyEvidence Next 2 100 BaselineFullN5F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFullFaultyInitN7F2ForkingModel InitWithFaultyEvidence Next 2 100 BaselineFullN7F2ForkingFaultyInitSafety
run_model spec/quint/CrosslinkBaselineBftHeights.qnt CrosslinkBaselineBftHeightsModel Init Next 5 1000 BaselineBftHeightSafety
run_model spec/quint/CrosslinkBaselineFinality.qnt CrosslinkBaselineFinalityStableModel ComposedInit ComposedNext 10 1000 ComposedSafety
run_model spec/quint/CrosslinkBaselineFinality.qnt CrosslinkBaselineFinalityStreamChangeModel ComposedInit ComposedNext 10 1000 ComposedSafety
run_model spec/quint/CrosslinkBaselineFinality.qnt CrosslinkBaselineFinalityLivenessModel LivenessInit LivenessStep 9 1 LivenessSafety
run_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowSamplingModel Init Next 10 1000 BaselinePowSamplingSafety
run_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowLongReorgModel Init Next 10 1000 BaselinePowLongReorgSafety
run_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowGeneratedScheduleModel Init Next 10 1000 BaselinePowGeneratedScheduleSafety
run_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowRepeatedGeneratedScheduleModel Init Next 10 1000 BaselinePowRepeatedGeneratedScheduleSafety
run_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowStochasticProductionModel Init Next 10 1000 BaselinePowStochasticProductionSafety
}
symbolic_checks() {
verify_model spec/quint/CrosslinkResampling.qnt CrosslinkStickyModel 3 Init Next Safety
verify_model spec/quint/CrosslinkBaseline.qnt CrosslinkBaselineStableModel 3 Init Next BaselineSafety
verify_model spec/quint/CrosslinkBaseline.qnt CrosslinkBaselineStreamChangeModel 3 Init Next BaselineSafety
verify_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN4F1StableTest 3 Init Next BaselineN4F1StableSafety
verify_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN4F1ForkingTest 3 Init Next BaselineN4F1ForkingSafety
verify_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN4F2ForkingTest 3 Init Next BaselineN4F2ForkingSafety
verify_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN5F2ForkingTest 3 Init Next BaselineN5F2ForkingSafety
verify_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN7F2ForkingTest 3 Init Next BaselineN7F2ForkingSafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineAccountabilityModel 3 Init Next BaselineAccountabilitySafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFaultyInitTinyModel 2 InitWithFaultyEvidence Next BaselineFaultyInitSafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFaultyInitForkingModel 2 InitWithBoundedForkingFaultyEvidence Next BaselineForkingFaultyInitSafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineBoundedFaultyInitN4F2ForkingModel 2 InitWithRepresentativeN4F2FaultyEvidence Next BaselineBoundedN4F2ForkingFaultyInitSafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineSingleFaultyInitN4F2ForkingModel 2 InitWithSingleN4F2FaultyEvidence Next BaselineSingleN4F2ForkingFaultyInitSafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselinePairFaultyInitN4F2ForkingModel 2 InitWithPairN4F2FaultyEvidence Next BaselinePairN4F2ForkingFaultyInitSafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineTripleFaultyInitN4F2ForkingModel 2 InitWithTripleN4F2FaultyEvidence Next BaselineTripleN4F2ForkingFaultyInitSafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineBoundedFaultyInitN5F2ForkingModel 2 InitWithRepresentativeN5F2FaultyEvidence Next BaselineBoundedN5F2ForkingFaultyInitSafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineSingleFaultyInitN5F2ForkingModel 2 InitWithSingleN5F2FaultyEvidence Next BaselineSingleN5F2ForkingFaultyInitSafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselinePairFaultyInitN5F2ForkingModel 2 InitWithPairN5F2FaultyEvidence Next BaselinePairN5F2ForkingFaultyInitSafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineTripleFaultyInitN5F2ForkingModel 2 InitWithTripleN5F2FaultyEvidence Next BaselineTripleN5F2ForkingFaultyInitSafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineBoundedFaultyInitN7F2ForkingModel 2 InitWithRepresentativeN7F2FaultyEvidence Next BaselineBoundedN7F2ForkingFaultyInitSafety
verify_model spec/quint/CrosslinkBaselineBftHeights.qnt CrosslinkBaselineBftHeightsModel 5 Init Next BaselineBftHeightSafety
verify_model spec/quint/CrosslinkBaselineFinality.qnt CrosslinkBaselineFinalityStableModel 5 ComposedInit ComposedNext ComposedSafety
verify_model spec/quint/CrosslinkBaselineFinality.qnt CrosslinkBaselineFinalityStreamChangeModel 5 ComposedInit ComposedNext ComposedSafety
verify_model spec/quint/CrosslinkBaselineFinality.qnt CrosslinkBaselineFinalityLivenessModel 9 LivenessInit LivenessStep LivenessSafety
verify_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowSamplingModel 3 Init Next BaselinePowSamplingSafety
verify_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowLongReorgModel 3 Init Next BaselinePowLongReorgSafety
verify_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowGeneratedScheduleModel 3 Init Next BaselinePowGeneratedScheduleSafety
verify_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowRepeatedGeneratedScheduleModel 3 Init Next BaselinePowRepeatedGeneratedScheduleSafety
verify_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowStochasticProductionModel 3 Init Next BaselinePowStochasticProductionSafety
verify_model spec/quint/CrosslinkResampling.qnt CrosslinkNilResamplingModel 3 Init Next Safety
verify_model spec/quint/CrosslinkForkFinality.qnt CrosslinkForkFinalityModel 4 Init Next Safety
verify_model spec/quint/CrosslinkPowForkSchedule.qnt CrosslinkPowForkScheduleModel 4 Init Next Safety
verify_model spec/quint/CrosslinkPowBranchCompetition.qnt CrosslinkPowBranchCompetitionModel 4 Init Next Safety
verify_model spec/quint/CrosslinkResampling.qnt CrosslinkNilResamplingLivenessModel 15 LivenessInit LivenessStep LivenessSafety
verify_model spec/quint/CrosslinkComposed.qnt CrosslinkComposedResamplingModel 5 ComposedInit ComposedNext ComposedSafety
verify_model spec/quint/CrosslinkComposed.qnt CrosslinkComposedLivenessModel 16 LivenessInit LivenessStep LivenessSafety
verify_model spec/quint/CrosslinkBftHeights.qnt CrosslinkBftHeightsModel 5 Init Next Safety
verify_model spec/quint/CrosslinkDynamicSigma.qnt CrosslinkDynamicSigmaHashParticipationModel 7 Init Next Safety
verify_model spec/quint/CrosslinkDynamicSigmaCalibration.qnt CrosslinkDynamicSigmaCalibrationModel 8 Init Next Safety
verify_model spec/quint/CrosslinkDynamicSigmaTelemetry.qnt CrosslinkDynamicSigmaTelemetryModel 8 Init Next Safety
verify_model spec/quint/CrosslinkDynamicSigmaHysteresis.qnt CrosslinkDynamicSigmaHysteresisModel 5 Init Next Safety
verify_model spec/quint/CrosslinkDynamicSigmaForkSchedule.qnt CrosslinkDynamicSigmaForkScheduleModel 4 DerivedInit DerivedNext DerivedSafety
verify_model spec/quint/CrosslinkDynamicSigmaBranchCompetition.qnt CrosslinkDynamicSigmaBranchCompetitionModel 4 BranchCompetitionDynamicInit BranchCompetitionDynamicNext BranchCompetitionDynamicSafety
verify_model spec/quint/CrosslinkDynamicSigmaResampling.qnt CrosslinkDynamicSigmaResamplingModel 8 DynamicResamplingInit DynamicResamplingNext DynamicResamplingSafety
verify_model spec/quint/CrosslinkDynamicSigmaFinality.qnt CrosslinkDynamicSigmaFinalityModel 8 FullComposedInit FullComposedNext FullComposedSafety
verify_model spec/quint/CrosslinkDynamicSigmaFinality.qnt CrosslinkDynamicSigmaFinalityModel 10 FullComposedInit FullComposedNext FullProtocolProjectionSafety
verify_model spec/quint/CrosslinkDynamicSigmaFinality.qnt CrosslinkDynamicSigmaFinalityModel 10 FullComposedInit FullComposedNext FullFinalityProjectionSafety
verify_model spec/quint/CrosslinkDynamicSigmaFinality.qnt CrosslinkDynamicSigmaFinalityModel 10 FullComposedInit FullComposedNext FullWorkCompetitionProjectionSafety
}
baseline_symbolic_core_checks() {
verify_model spec/quint/CrosslinkResampling.qnt CrosslinkStickyModel 3 Init Next Safety
verify_model spec/quint/CrosslinkBaseline.qnt CrosslinkBaselineStableModel 3 Init Next BaselineSafety
verify_model spec/quint/CrosslinkBaseline.qnt CrosslinkBaselineStreamChangeModel 3 Init Next BaselineSafety
verify_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN4F1StableTest 3 Init Next BaselineN4F1StableSafety
verify_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN4F1ForkingTest 3 Init Next BaselineN4F1ForkingSafety
verify_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN4F2ForkingTest 3 Init Next BaselineN4F2ForkingSafety
verify_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN5F2ForkingTest 3 Init Next BaselineN5F2ForkingSafety
verify_model spec/quint/CrosslinkBaselineTest.qnt CrosslinkBaselineN7F2ForkingTest 3 Init Next BaselineN7F2ForkingSafety
verify_model spec/quint/CrosslinkBaselineBftHeights.qnt CrosslinkBaselineBftHeightsModel 5 Init Next BaselineBftHeightSafety
verify_model spec/quint/CrosslinkBaselineFinality.qnt CrosslinkBaselineFinalityStableModel 5 ComposedInit ComposedNext ComposedSafety
verify_model spec/quint/CrosslinkBaselineFinality.qnt CrosslinkBaselineFinalityStreamChangeModel 5 ComposedInit ComposedNext ComposedSafety
verify_model spec/quint/CrosslinkBaselineFinality.qnt CrosslinkBaselineFinalityLivenessModel 9 LivenessInit LivenessStep LivenessSafety
verify_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowSamplingModel 3 Init Next BaselinePowSamplingSafety
verify_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowLongReorgModel 3 Init Next BaselinePowLongReorgSafety
verify_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowGeneratedScheduleModel 3 Init Next BaselinePowGeneratedScheduleSafety
verify_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowRepeatedGeneratedScheduleModel 3 Init Next BaselinePowRepeatedGeneratedScheduleSafety
verify_model spec/quint/CrosslinkBaselinePowSampling.qnt CrosslinkBaselinePowStochasticProductionModel 3 Init Next BaselinePowStochasticProductionSafety
}
baseline_symbolic_accountability_checks() {
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineAccountabilityModel 3 Init Next BaselineAccountabilitySafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFaultyInitTinyModel 2 InitWithFaultyEvidence Next BaselineFaultyInitSafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineFaultyInitForkingModel 2 InitWithBoundedForkingFaultyEvidence Next BaselineForkingFaultyInitSafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineBoundedFaultyInitN4F2ForkingModel 2 InitWithRepresentativeN4F2FaultyEvidence Next BaselineBoundedN4F2ForkingFaultyInitSafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineSingleFaultyInitN4F2ForkingModel 2 InitWithSingleN4F2FaultyEvidence Next BaselineSingleN4F2ForkingFaultyInitSafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselinePairFaultyInitN4F2ForkingModel 2 InitWithPairN4F2FaultyEvidence Next BaselinePairN4F2ForkingFaultyInitSafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineTripleFaultyInitN4F2ForkingModel 2 InitWithTripleN4F2FaultyEvidence Next BaselineTripleN4F2ForkingFaultyInitSafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineBoundedFaultyInitN5F2ForkingModel 2 InitWithRepresentativeN5F2FaultyEvidence Next BaselineBoundedN5F2ForkingFaultyInitSafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineSingleFaultyInitN5F2ForkingModel 2 InitWithSingleN5F2FaultyEvidence Next BaselineSingleN5F2ForkingFaultyInitSafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselinePairFaultyInitN5F2ForkingModel 2 InitWithPairN5F2FaultyEvidence Next BaselinePairN5F2ForkingFaultyInitSafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineTripleFaultyInitN5F2ForkingModel 2 InitWithTripleN5F2FaultyEvidence Next BaselineTripleN5F2ForkingFaultyInitSafety
verify_model spec/quint/CrosslinkBaselineAccountability.qnt CrosslinkBaselineBoundedFaultyInitN7F2ForkingModel 2 InitWithRepresentativeN7F2FaultyEvidence Next BaselineBoundedN7F2ForkingFaultyInitSafety
}
baseline_symbolic_checks() {
baseline_symbolic_core_checks
baseline_symbolic_accountability_checks
}
case "${mode}" in
quick)
quick_checks
;;
quick-baseline)
baseline_quick_checks
;;
symbolic)
symbolic_checks
;;
symbolic-baseline)
baseline_symbolic_checks
;;
symbolic-baseline-core)
baseline_symbolic_core_checks
;;
symbolic-baseline-accountability)
baseline_symbolic_accountability_checks
;;
all)
quick_checks
symbolic_checks
;;
esac
// -*- mode: Bluespec; -*-
module CrosslinkBaselineStableModel {
/*
Baseline Crosslink fixes the current/sticky Tenderlink behavior:
- sigma is fixed outside this focused model;
- proposals sample the current PoW stream;
- a nil-precommit quorum advances the round but does not resample or unlock
same-round Tendermint state.
This module gives the positive baseline witness: when the sampled PoW stream
is stable long enough, the normal three-step Tendermint flow decides the
sampled value.
*/
import CrosslinkResamplingTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 2,
Proposer = Map(0 -> "p1", 1 -> "p2", 2 -> "p3"),
Stream = Map(0 -> "s0", 1 -> "s0", 2 -> "s0"),
Snapshots = Set("s0"),
ResampleOnNilPrecommit = false,
ExpectedRound1Proposal = "s0",
).* from "./CrosslinkResampling"
val BaselineSafety =
Safety
run baselineStableStreamDecidesSampledSnapshotTest = {
Init
.then(InsertProposal("p1"))
.then(UponProposalPrevote("p1", "s0"))
.then(UponProposalPrevote("p2", "s0"))
.then(UponProposalPrevote("p3", "s0"))
.then(UponValuePrevoteQuorum("p1", "s0"))
.then(UponValuePrevoteQuorum("p2", "s0"))
.then(UponValuePrevoteQuorum("p3", "s0"))
.then(Decide("p1", 0, "s0"))
.then(all {
assert(decision.get("p1") == "s0"),
assert(BaselineSafety),
unchangedAll,
})
}
}
module CrosslinkBaselineStreamChangeModel {
/*
This module gives the negative baseline witnesses that motivate
nil-precommit resampling: after a stream change, the current/sticky
behavior can keep carrying the stale sample and same-round lock state.
*/
import CrosslinkResamplingTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 2,
Proposer = Map(0 -> "p1", 1 -> "p2", 2 -> "p3"),
Stream = Map(0 -> "s0", 1 -> "s1", 2 -> "s2"),
Snapshots = Set("s0", "s1", "s2"),
ResampleOnNilPrecommit = false,
ExpectedRound1Proposal = "s0",
).* from "./CrosslinkResampling"
val BaselineSafety =
Safety
run baselineCarriesStaleSampleAfterStreamChangeTest = {
Init
.then(SeedAbandonedRoundState("p1"))
.then(SeedAbandonedRoundState("p2"))
.then(SeedAbandonedRoundState("p3"))
.then(StartNextRoundAfterPrecommitQuorum("p2"))
.then(InsertProposal("p2"))
.then(all {
assert(msgsPropose.get(1).exists(m =>
m.src == "p2" and m.round == 1 and m.proposal == "s0"
)),
assert(BaselineSafety),
unchangedAll,
})
}
run baselineSameRoundLockBlocksFreshDecisionAfterStreamChangeTest = {
Init
.then(SeedValidNilPrecommitState("p1"))
.then(SeedValidNilPrecommitState("p2"))
.then(SeedFaultyNilPrecommit("p4"))
.then(SeedSameRoundValueLock("p3"))
.then(StartNextRoundAfterPrecommitQuorum("p1"))
.then(StartNextRoundAfterPrecommitQuorum("p2"))
.then(StartNextRoundAfterPrecommitQuorum("p3"))
.then(InsertProposal("p2"))
.then(UponProposalPrevote("p1", "s1"))
.fail()
}
}
// -*- mode: Bluespec; -*-
module CrosslinkBaselineAccountabilityModel {
/*
Baseline Crosslink keeps the current fixed-sigma/sticky Tenderlink lock
behavior. In this variant, a nil-precommit certificate advances the round but
does not clear same-round `validValue` or `lockedValue` state.
These witnesses make the baseline accountability projection explicit: actual
baseline transitions retain same-round value locks after nil precommit, and
conflicting value commits without unlock evidence expose Tendermint-style
amnesia evidence.
*/
import CrosslinkResamplingTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 2,
Proposer = Map(0 -> "p1", 1 -> "p2", 2 -> "p3"),
Stream = Map(0 -> "s0", 1 -> "s1", 2 -> "s2"),
Snapshots = Set("s0", "s1", "s2"),
ResampleOnNilPrecommit = false,
ExpectedRound1Proposal = "s0",
).* from "./CrosslinkResampling"
val BaselineAccountabilitySafety =
Safety
run baselineNilPrecommitDoesNotClearSameRoundValueLockTest = {
Init
.then(SeedValidNilPrecommitState("p1"))
.then(SeedValidNilPrecommitState("p2"))
.then(SeedFaultyNilPrecommit("p4"))
.then(SeedSameRoundValueLock("p3"))
.then(StartNextRoundAfterPrecommitQuorum("p3"))
.then(all {
assert(NilPrecommitCert(0)),
assert(lockedValue.get("p3") == "s0"),
assert(lockedRound.get("p3") == 0),
assert(validValue.get("p3") == "s0"),
assert(validRound.get("p3") == 0),
assert(size(CorrectValueLocks(0, "s0")) == 1),
assert(NilCertLeavesAtMostFCurrentRoundLocks),
assert(CorrectValueLocksHavePrecommitEvidence),
assert(BaselineAccountabilitySafety),
unchangedAll,
})
}
run baselineConflictingCommitsWithoutUnlockExposeAmnesiaTest = {
Init
.then(SeedCorrectPrecommitEvidence("p1", 0, "s0"))
.then(SeedCorrectPrecommitEvidence("p2", 0, "s0"))
.then(SeedFaultyPrecommitEvidence("p4", 0, "s0"))
.then(SeedCorrectPrecommitEvidence("p2", 1, "s1"))
.then(SeedCorrectPrecommitEvidence("p3", 1, "s1"))
.then(SeedFaultyPrecommitEvidence("p4", 1, "s1"))
.then(all {
assert(PrecommitQuorum(0, "s0")),
assert(PrecommitQuorum(1, "s1")),
assert(not(NilPrecommitCert(0))),
assert(CorrectValueSwitchWithoutUnlock("p2", 0, "s0", 1, "s1")),
assert(AmnesiaBy("p2")),
assert(AmnesiaBy("p4")),
assert(size(DetectableFaults) >= THRESHOLD1),
assert(Accountability),
assert(ConflictHasAccountabilityEvidence(0, "s0", 1, "s1")),
assert(ConflictingCommitsAccountable),
unchangedAll,
})
}
run baselineFaultyProposalEvidenceFeedsEquivocationTest = {
Init
.then(SeedFaultyProposalEvidence("p4", 0, "s0", -1))
.then(SeedFaultyProposalEvidence("p4", 0, "s1", -1))
.then(all {
assert(ProposalEquivocationIn("p4", EvidencePropose)),
assert(EquivocationBy("p4")),
assert(DetectableFaultBy("p4")),
assert(EvidenceCoversObservedMessages),
assert(BaselineAccountabilitySafety),
unchangedAll,
})
}
run baselineFaultyPrevoteEvidenceFeedsEquivocationTest = {
Init
.then(SeedFaultyPrevoteEvidence("p4", 0, "s0"))
.then(SeedFaultyPrevoteEvidence("p4", 0, "s1"))
.then(all {
assert(VoteEquivocationIn("p4", EvidencePrevote)),
assert(EquivocationBy("p4")),
assert(DetectableFaultBy("p4")),
assert(EvidenceCoversObservedMessages),
assert(BaselineAccountabilitySafety),
unchangedAll,
})
}
run baselineFaultyNilValuePrecommitEvidenceFeedsEquivocationTest = {
Init
.then(SeedFaultyNilPrecommitEvidence("p4", 0))
.then(SeedFaultyPrecommitEvidence("p4", 0, "s0"))
.then(all {
assert(VoteEquivocationIn("p4", EvidencePrecommit)),
assert(EquivocationBy("p4")),
assert(DetectableFaultBy("p4")),
assert(EvidenceCoversObservedMessages),
assert(BaselineAccountabilitySafety),
unchangedAll,
})
}
}
module CrosslinkBaselineFaultyInitTinyModel {
/*
A tiny upstream-style faulty Init harness. It keeps the nondeterministic
faulty-message powersets small enough for the baseline proof gate while
checking that initially injected faulty evidence is covered by the same
baseline accountability invariant.
*/
import CrosslinkResamplingTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 1,
Proposer = Map(0 -> "p1", 1 -> "p2"),
Stream = Map(0 -> "s0", 1 -> "s0"),
Snapshots = Set("s0"),
ResampleOnNilPrecommit = false,
ExpectedRound1Proposal = "s0",
).* from "./CrosslinkResampling"
val BaselineFaultyInitSafety =
Safety
run baselineFaultyInitCoversInjectedEvidenceTest = {
InitWithFaultyEvidence.then(all {
assert(EvidenceCoversObservedMessages),
assert(BaselineFaultyInitSafety),
unchangedAll,
})
}
}
module CrosslinkBaselineFaultyInitForkingModel {
/*
A larger fixed-sigma/forking baseline faulty-init harness. The baseline
protocol parameters match the upstream-shaped N4/F1 forking shell, while the
injected faulty evidence is bounded to a small domain so the symbolic proof
gate remains tractable.
*/
import CrosslinkBaselineModels.* from "./CrosslinkBaselineModels"
pure val TestRounds = 0.to(4)
pure def TestHeadMinusSigma(r: int): str =
AncestorByHeight
.get(ForkingBestTip.get(r))
.get(HeightBySnapshot.get(ForkingBestTip.get(r)) - 1)
pure val TestStream: int -> str =
TestRounds.mapBy(r => TestHeadMinusSigma(r))
pure val TestStreamMatchesFixedSigma =
TestRounds.forall(r => TestStream.get(r) == TestHeadMinusSigma(r))
import CrosslinkResamplingTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 4,
Proposer = ProposerSchedule,
Stream = TestStream,
Snapshots = ValidSnapshots,
ResampleOnNilPrecommit = false,
ExpectedRound1Proposal = TestStream.get(0),
).* from "./CrosslinkResampling"
pure val FaultyInitRounds =
Set(0)
pure val FaultyInitSnapshots =
Set(TestHeadMinusSigma(0), TestHeadMinusSigma(1))
pure val FaultyInitSnapshotsOrNil =
Set(NilSnapshot).union(FaultyInitSnapshots)
pure val FaultyInitValidRounds =
Set(NilRound, 0)
pure val FaultyInitDomainWellFormed =
FaultyInitRounds.subseteq(Rounds) and
FaultyInitSnapshots.subseteq(Snapshots) and
FaultyInitValidRounds.subseteq(RoundsOrNil)
def BoundedFaultyInitProposals(r: Round_t): Set[Propose_t] =
tuples(Faulty, FaultyInitSnapshots, FaultyInitValidRounds)
.map(((p, v, vr)) => {
src: p,
round: r,
proposal: v,
validRound: vr,
})
val AllBoundedFaultyInitProposals =
FaultyInitRounds.map(r => BoundedFaultyInitProposals(r)).flatten()
def BoundedFaultyInitPrevotes(r: Round_t): Set[Vote_t] =
tuples(Faulty, FaultyInitSnapshotsOrNil)
.map(((p, v)) => {
src: p,
round: r,
id: v,
})
val AllBoundedFaultyInitPrevotes =
FaultyInitRounds.map(r => BoundedFaultyInitPrevotes(r)).flatten()
def BoundedFaultyInitPrecommits(r: Round_t): Set[Vote_t] =
tuples(Faulty, FaultyInitSnapshotsOrNil)
.map(((p, v)) => {
src: p,
round: r,
id: v,
})
val AllBoundedFaultyInitPrecommits =
FaultyInitRounds.map(r => BoundedFaultyInitPrecommits(r)).flatten()
action InitWithBoundedForkingFaultyEvidence =
nondet faultyProposals = AllBoundedFaultyInitProposals.powerset().oneOf()
nondet faultyPrevotes = AllBoundedFaultyInitPrevotes.powerset().oneOf()
nondet faultyPrecommits = AllBoundedFaultyInitPrecommits.powerset().oneOf()
all {
round' = Corr.mapBy(_ => 0),
step' = Corr.mapBy(_ => "propose"),
decision' = Corr.mapBy(_ => NilSnapshot),
lockedValue' = Corr.mapBy(_ => NilSnapshot),
lockedRound' = Corr.mapBy(_ => NilRound),
validValue' = Corr.mapBy(_ => NilSnapshot),
validRound' = Corr.mapBy(_ => NilRound),
cachedProposal' = Corr.mapBy(_ => NilSnapshot),
cachedProposalRound' = Corr.mapBy(_ => NilRound),
msgsPropose' = Rounds.mapBy(r =>
faultyProposals.filter(m => m.round == r)
),
msgsPrevote' = Rounds.mapBy(r =>
faultyPrevotes.filter(m => m.round == r)
),
msgsPrecommit' = Rounds.mapBy(r =>
faultyPrecommits.filter(m => m.round == r)
),
evidencePropose' = faultyProposals,
evidencePrevote' = faultyPrevotes,
evidencePrecommit' = faultyPrecommits,
firedAction' = "InitWithBoundedForkingFaultyEvidence",
}
val BaselineForkingFaultyInitSafety =
Safety and TestStreamMatchesFixedSigma and FaultyInitDomainWellFormed
run baselineForkingFaultyInitCoversEvidenceTest = {
InitWithBoundedForkingFaultyEvidence.then(all {
assert(TestHeadMinusSigma(0) == "a1"),
assert(TestHeadMinusSigma(1) == "b1"),
assert(EvidenceCoversObservedMessages),
assert(BaselineForkingFaultyInitSafety),
unchangedAll,
})
}
run baselineForkingFaultyInitAllowsCorrectProposalTest = {
InitWithBoundedForkingFaultyEvidence
.then(InsertProposal("p1"))
.then(all {
assert(HasProposal(0, "a1")),
assert(EvidenceCoversObservedMessages),
assert(BaselineForkingFaultyInitSafety),
unchangedAll,
})
}
}
module CrosslinkBaselineBoundedFaultyInitN4F2ForkingModel {
/*
A representative n4/f2 faulty-init symbolic harness. This keeps the
above-live-boundary f=2 shape in the symbolic gate without expanding the full
faulty-message powerset.
*/
import CrosslinkBaselineModels.* from "./CrosslinkBaselineModels"
pure val TestRounds = 0.to(4)
pure def TestHeadMinusSigma(r: int): str =
AncestorByHeight
.get(ForkingBestTip.get(r))
.get(HeightBySnapshot.get(ForkingBestTip.get(r)) - 1)
pure val TestStream: int -> str =
TestRounds.mapBy(r => TestHeadMinusSigma(r))
pure val TestStreamMatchesFixedSigma =
TestRounds.forall(r => TestStream.get(r) == TestHeadMinusSigma(r))
import CrosslinkResampling(
Corr = Set("p1", "p2"),
Faulty = Set("p3", "p4"),
N = 4,
T = 2,
MaxRound = 4,
Proposer = ProposerSchedule,
Stream = TestStream,
Snapshots = ValidSnapshots,
ResampleOnNilPrecommit = false,
).* from "./CrosslinkResampling"
action unchangedAll = all {
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
firedAction' = firedAction,
}
pure val RepresentativeN4F2FaultyInitProposals =
Set(
{ src: "p3", round: 0, proposal: TestHeadMinusSigma(0), validRound: NilRound },
{ src: "p4", round: 0, proposal: TestHeadMinusSigma(1), validRound: 0 },
)
pure val RepresentativeN4F2FaultyInitPrevotes =
Set(
{ src: "p3", round: 0, id: TestHeadMinusSigma(0) },
{ src: "p4", round: 0, id: TestHeadMinusSigma(1) },
{ src: "p4", round: 0, id: NilSnapshot },
)
pure val RepresentativeN4F2FaultyInitPrecommits =
Set(
{ src: "p3", round: 0, id: TestHeadMinusSigma(0) },
{ src: "p4", round: 0, id: TestHeadMinusSigma(1) },
{ src: "p4", round: 0, id: NilSnapshot },
)
pure val RepresentativeN4F2FaultyInitDomainWellFormed =
RepresentativeN4F2FaultyInitProposals.forall(m =>
m.src.in(Faulty) and
m.round.in(Rounds) and
m.proposal.in(Snapshots) and
m.validRound.in(RoundsOrNil)
) and
RepresentativeN4F2FaultyInitPrevotes.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
) and
RepresentativeN4F2FaultyInitPrecommits.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
)
pure val RepresentativeN4F2FaultyInitUsesBothFaultyValidators =
RepresentativeN4F2FaultyInitProposals.exists(m =>
m.src == "p3" and m.proposal == "a1" and m.validRound == NilRound
) and
RepresentativeN4F2FaultyInitProposals.exists(m =>
m.src == "p4" and m.proposal == "b1" and m.validRound == 0
) and
RepresentativeN4F2FaultyInitPrevotes.exists(m =>
m.src == "p4" and m.id == NilSnapshot
) and
RepresentativeN4F2FaultyInitPrecommits.exists(m =>
m.src == "p4" and m.id == NilSnapshot
)
action InitWithRepresentativeN4F2FaultyEvidence =
nondet faultyProposals = RepresentativeN4F2FaultyInitProposals.powerset().oneOf()
nondet faultyPrevotes = RepresentativeN4F2FaultyInitPrevotes.powerset().oneOf()
nondet faultyPrecommits = RepresentativeN4F2FaultyInitPrecommits.powerset().oneOf()
all {
round' = Corr.mapBy(_ => 0),
step' = Corr.mapBy(_ => "propose"),
decision' = Corr.mapBy(_ => NilSnapshot),
lockedValue' = Corr.mapBy(_ => NilSnapshot),
lockedRound' = Corr.mapBy(_ => NilRound),
validValue' = Corr.mapBy(_ => NilSnapshot),
validRound' = Corr.mapBy(_ => NilRound),
cachedProposal' = Corr.mapBy(_ => NilSnapshot),
cachedProposalRound' = Corr.mapBy(_ => NilRound),
msgsPropose' = Rounds.mapBy(r =>
faultyProposals.filter(m => m.round == r)
),
msgsPrevote' = Rounds.mapBy(r =>
faultyPrevotes.filter(m => m.round == r)
),
msgsPrecommit' = Rounds.mapBy(r =>
faultyPrecommits.filter(m => m.round == r)
),
evidencePropose' = faultyProposals,
evidencePrevote' = faultyPrevotes,
evidencePrecommit' = faultyPrecommits,
firedAction' = "InitWithRepresentativeN4F2FaultyEvidence",
}
val BaselineBoundedN4F2ForkingFaultyInitSafety =
Safety and
TestStreamMatchesFixedSigma and
RepresentativeN4F2FaultyInitDomainWellFormed
run baselineBoundedN4F2ForkingFaultyInitCoversEvidenceTest = {
InitWithRepresentativeN4F2FaultyEvidence.then(all {
assert(THRESHOLD1 == 3),
assert(THRESHOLD2 == 5),
assert(not(size(Corr) >= THRESHOLD1)),
assert(not(size(Corr) >= THRESHOLD2)),
assert(TestHeadMinusSigma(0) == "a1"),
assert(TestHeadMinusSigma(1) == "b1"),
assert(RepresentativeN4F2FaultyInitUsesBothFaultyValidators),
assert(EvidenceCoversObservedMessages),
assert(BaselineBoundedN4F2ForkingFaultyInitSafety),
unchangedAll,
})
}
run baselineBoundedN4F2ForkingFaultyInitAllowsCorrectProposalTest = {
InitWithRepresentativeN4F2FaultyEvidence
.then(InsertProposal("p1"))
.then(all {
assert(HasProposal(0, "a1")),
assert(EvidenceCoversObservedMessages),
assert(BaselineBoundedN4F2ForkingFaultyInitSafety),
unchangedAll,
})
}
}
module CrosslinkBaselineSingleFaultyInitN4F2ForkingModel {
/*
A tractability probe between representative f=2 faulty evidence and the full
faulty-message powerset. It picks one arbitrary faulty proposal, prevote,
and precommit from the full N4/F2 domain, then checks the same baseline
safety shape symbolically.
*/
import CrosslinkBaselineModels.* from "./CrosslinkBaselineModels"
pure val TestRounds = 0.to(4)
pure def TestHeadMinusSigma(r: int): str =
AncestorByHeight
.get(ForkingBestTip.get(r))
.get(HeightBySnapshot.get(ForkingBestTip.get(r)) - 1)
pure val TestStream: int -> str =
TestRounds.mapBy(r => TestHeadMinusSigma(r))
pure val TestStreamMatchesFixedSigma =
TestRounds.forall(r => TestStream.get(r) == TestHeadMinusSigma(r))
import CrosslinkResampling(
Corr = Set("p1", "p2"),
Faulty = Set("p3", "p4"),
N = 4,
T = 2,
MaxRound = 4,
Proposer = ProposerSchedule,
Stream = TestStream,
Snapshots = ValidSnapshots,
ResampleOnNilPrecommit = false,
).* from "./CrosslinkResampling"
action unchangedAll = all {
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
firedAction' = firedAction,
}
pure val SingleN4F2FaultyInitDomainWellFormed =
AllFaultyProposals.forall(m =>
m.src.in(Faulty) and
m.round.in(Rounds) and
m.proposal.in(Snapshots) and
m.validRound.in(RoundsOrNil)
) and
AllFaultyPrevotes.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
) and
AllFaultyPrecommits.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
)
action InitWithSingleN4F2FaultyEvidence =
nondet faultyProposal = AllFaultyProposals.oneOf()
nondet faultyPrevote = AllFaultyPrevotes.oneOf()
nondet faultyPrecommit = AllFaultyPrecommits.oneOf()
all {
round' = Corr.mapBy(_ => 0),
step' = Corr.mapBy(_ => "propose"),
decision' = Corr.mapBy(_ => NilSnapshot),
lockedValue' = Corr.mapBy(_ => NilSnapshot),
lockedRound' = Corr.mapBy(_ => NilRound),
validValue' = Corr.mapBy(_ => NilSnapshot),
validRound' = Corr.mapBy(_ => NilRound),
cachedProposal' = Corr.mapBy(_ => NilSnapshot),
cachedProposalRound' = Corr.mapBy(_ => NilRound),
msgsPropose' = Rounds.mapBy(r =>
Set(faultyProposal).filter(m => m.round == r)
),
msgsPrevote' = Rounds.mapBy(r =>
Set(faultyPrevote).filter(m => m.round == r)
),
msgsPrecommit' = Rounds.mapBy(r =>
Set(faultyPrecommit).filter(m => m.round == r)
),
evidencePropose' = Set(faultyProposal),
evidencePrevote' = Set(faultyPrevote),
evidencePrecommit' = Set(faultyPrecommit),
firedAction' = "InitWithSingleN4F2FaultyEvidence",
}
val BaselineSingleN4F2ForkingFaultyInitSafety =
Safety and
TestStreamMatchesFixedSigma and
SingleN4F2FaultyInitDomainWellFormed
run baselineSingleN4F2FaultyInitCoversEvidenceTest = {
InitWithSingleN4F2FaultyEvidence.then(all {
assert(size(EvidencePropose) == 1),
assert(size(EvidencePrevote) == 1),
assert(size(EvidencePrecommit) == 1),
assert(EvidenceCoversObservedMessages),
assert(BaselineSingleN4F2ForkingFaultyInitSafety),
unchangedAll,
})
}
}
module CrosslinkBaselinePairFaultyInitN4F2ForkingModel {
/*
A tractability step beyond the single-message abstraction. It chooses two
arbitrary faulty proposals, prevotes, and precommits from the full N4/F2
domain, allowing symbolic checks over paired faulty evidence without
expanding the full powerset.
*/
import CrosslinkBaselineModels.* from "./CrosslinkBaselineModels"
pure val TestRounds = 0.to(4)
pure def TestHeadMinusSigma(r: int): str =
AncestorByHeight
.get(ForkingBestTip.get(r))
.get(HeightBySnapshot.get(ForkingBestTip.get(r)) - 1)
pure val TestStream: int -> str =
TestRounds.mapBy(r => TestHeadMinusSigma(r))
pure val TestStreamMatchesFixedSigma =
TestRounds.forall(r => TestStream.get(r) == TestHeadMinusSigma(r))
import CrosslinkResampling(
Corr = Set("p1", "p2"),
Faulty = Set("p3", "p4"),
N = 4,
T = 2,
MaxRound = 4,
Proposer = ProposerSchedule,
Stream = TestStream,
Snapshots = ValidSnapshots,
ResampleOnNilPrecommit = false,
).* from "./CrosslinkResampling"
action unchangedAll = all {
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
firedAction' = firedAction,
}
pure val PairN4F2FaultyInitDomainWellFormed =
AllFaultyProposals.forall(m =>
m.src.in(Faulty) and
m.round.in(Rounds) and
m.proposal.in(Snapshots) and
m.validRound.in(RoundsOrNil)
) and
AllFaultyPrevotes.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
) and
AllFaultyPrecommits.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
)
action InitWithPairN4F2FaultyEvidence =
nondet faultyProposalA = AllFaultyProposals.oneOf()
nondet faultyProposalB = AllFaultyProposals.oneOf()
nondet faultyPrevoteA = AllFaultyPrevotes.oneOf()
nondet faultyPrevoteB = AllFaultyPrevotes.oneOf()
nondet faultyPrecommitA = AllFaultyPrecommits.oneOf()
nondet faultyPrecommitB = AllFaultyPrecommits.oneOf()
val faultyProposals = Set(faultyProposalA, faultyProposalB)
val faultyPrevotes = Set(faultyPrevoteA, faultyPrevoteB)
val faultyPrecommits = Set(faultyPrecommitA, faultyPrecommitB)
all {
round' = Corr.mapBy(_ => 0),
step' = Corr.mapBy(_ => "propose"),
decision' = Corr.mapBy(_ => NilSnapshot),
lockedValue' = Corr.mapBy(_ => NilSnapshot),
lockedRound' = Corr.mapBy(_ => NilRound),
validValue' = Corr.mapBy(_ => NilSnapshot),
validRound' = Corr.mapBy(_ => NilRound),
cachedProposal' = Corr.mapBy(_ => NilSnapshot),
cachedProposalRound' = Corr.mapBy(_ => NilRound),
msgsPropose' = Rounds.mapBy(r =>
faultyProposals.filter(m => m.round == r)
),
msgsPrevote' = Rounds.mapBy(r =>
faultyPrevotes.filter(m => m.round == r)
),
msgsPrecommit' = Rounds.mapBy(r =>
faultyPrecommits.filter(m => m.round == r)
),
evidencePropose' = faultyProposals,
evidencePrevote' = faultyPrevotes,
evidencePrecommit' = faultyPrecommits,
firedAction' = "InitWithPairN4F2FaultyEvidence",
}
val BaselinePairN4F2ForkingFaultyInitSafety =
Safety and
TestStreamMatchesFixedSigma and
PairN4F2FaultyInitDomainWellFormed
run baselinePairN4F2FaultyInitCoversEvidenceTest = {
InitWithPairN4F2FaultyEvidence.then(all {
assert(size(EvidencePropose) >= 1),
assert(size(EvidencePropose) <= 2),
assert(size(EvidencePrevote) >= 1),
assert(size(EvidencePrevote) <= 2),
assert(size(EvidencePrecommit) >= 1),
assert(size(EvidencePrecommit) <= 2),
assert(EvidencePropose.subseteq(AllFaultyProposals)),
assert(EvidencePrevote.subseteq(AllFaultyPrevotes)),
assert(EvidencePrecommit.subseteq(AllFaultyPrecommits)),
assert(EvidenceCoversObservedMessages),
assert(BaselinePairN4F2ForkingFaultyInitSafety),
unchangedAll,
})
}
}
module CrosslinkBaselineTripleFaultyInitN4F2ForkingModel {
/*
A tractability step beyond the pair-message abstraction. It chooses three
arbitrary faulty proposals, prevotes, and precommits from the full N4/F2
domain, covering richer faulty-evidence shapes without expanding the full
powerset.
*/
import CrosslinkBaselineModels.* from "./CrosslinkBaselineModels"
pure val TestRounds = 0.to(4)
pure def TestHeadMinusSigma(r: int): str =
AncestorByHeight
.get(ForkingBestTip.get(r))
.get(HeightBySnapshot.get(ForkingBestTip.get(r)) - 1)
pure val TestStream: int -> str =
TestRounds.mapBy(r => TestHeadMinusSigma(r))
pure val TestStreamMatchesFixedSigma =
TestRounds.forall(r => TestStream.get(r) == TestHeadMinusSigma(r))
import CrosslinkResampling(
Corr = Set("p1", "p2"),
Faulty = Set("p3", "p4"),
N = 4,
T = 2,
MaxRound = 4,
Proposer = ProposerSchedule,
Stream = TestStream,
Snapshots = ValidSnapshots,
ResampleOnNilPrecommit = false,
).* from "./CrosslinkResampling"
action unchangedAll = all {
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
firedAction' = firedAction,
}
pure val TripleN4F2FaultyInitDomainWellFormed =
AllFaultyProposals.forall(m =>
m.src.in(Faulty) and
m.round.in(Rounds) and
m.proposal.in(Snapshots) and
m.validRound.in(RoundsOrNil)
) and
AllFaultyPrevotes.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
) and
AllFaultyPrecommits.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
)
action InitWithTripleN4F2FaultyEvidence =
nondet faultyProposalA = AllFaultyProposals.oneOf()
nondet faultyProposalB = AllFaultyProposals.oneOf()
nondet faultyProposalC = AllFaultyProposals.oneOf()
nondet faultyPrevoteA = AllFaultyPrevotes.oneOf()
nondet faultyPrevoteB = AllFaultyPrevotes.oneOf()
nondet faultyPrevoteC = AllFaultyPrevotes.oneOf()
nondet faultyPrecommitA = AllFaultyPrecommits.oneOf()
nondet faultyPrecommitB = AllFaultyPrecommits.oneOf()
nondet faultyPrecommitC = AllFaultyPrecommits.oneOf()
val faultyProposals = Set(faultyProposalA, faultyProposalB, faultyProposalC)
val faultyPrevotes = Set(faultyPrevoteA, faultyPrevoteB, faultyPrevoteC)
val faultyPrecommits = Set(faultyPrecommitA, faultyPrecommitB, faultyPrecommitC)
all {
round' = Corr.mapBy(_ => 0),
step' = Corr.mapBy(_ => "propose"),
decision' = Corr.mapBy(_ => NilSnapshot),
lockedValue' = Corr.mapBy(_ => NilSnapshot),
lockedRound' = Corr.mapBy(_ => NilRound),
validValue' = Corr.mapBy(_ => NilSnapshot),
validRound' = Corr.mapBy(_ => NilRound),
cachedProposal' = Corr.mapBy(_ => NilSnapshot),
cachedProposalRound' = Corr.mapBy(_ => NilRound),
msgsPropose' = Rounds.mapBy(r =>
faultyProposals.filter(m => m.round == r)
),
msgsPrevote' = Rounds.mapBy(r =>
faultyPrevotes.filter(m => m.round == r)
),
msgsPrecommit' = Rounds.mapBy(r =>
faultyPrecommits.filter(m => m.round == r)
),
evidencePropose' = faultyProposals,
evidencePrevote' = faultyPrevotes,
evidencePrecommit' = faultyPrecommits,
firedAction' = "InitWithTripleN4F2FaultyEvidence",
}
val BaselineTripleN4F2ForkingFaultyInitSafety =
Safety and
TestStreamMatchesFixedSigma and
TripleN4F2FaultyInitDomainWellFormed
run baselineTripleN4F2FaultyInitCoversEvidenceTest = {
InitWithTripleN4F2FaultyEvidence.then(all {
assert(size(EvidencePropose) >= 1),
assert(size(EvidencePropose) <= 3),
assert(size(EvidencePrevote) >= 1),
assert(size(EvidencePrevote) <= 3),
assert(size(EvidencePrecommit) >= 1),
assert(size(EvidencePrecommit) <= 3),
assert(EvidencePropose.subseteq(AllFaultyProposals)),
assert(EvidencePrevote.subseteq(AllFaultyPrevotes)),
assert(EvidencePrecommit.subseteq(AllFaultyPrecommits)),
assert(EvidenceCoversObservedMessages),
assert(BaselineTripleN4F2ForkingFaultyInitSafety),
unchangedAll,
})
}
}
module CrosslinkBaselineBoundedFaultyInitN5F2ForkingModel {
/*
A representative f=2 faulty-init symbolic harness. It uses the n5/f2
fixed-sigma/forking parameters, but keeps the nondeterministic faulty domain
deliberately tiny so Apalache can check the larger validator shape without
absorbing the full faulty-message powerset.
*/
import CrosslinkBaselineModels.* from "./CrosslinkBaselineModels"
pure val TestRounds = 0.to(4)
pure def TestHeadMinusSigma(r: int): str =
AncestorByHeight
.get(ForkingBestTip.get(r))
.get(HeightBySnapshot.get(ForkingBestTip.get(r)) - 1)
pure val TestStream: int -> str =
TestRounds.mapBy(r => TestHeadMinusSigma(r))
pure val TestStreamMatchesFixedSigma =
TestRounds.forall(r => TestStream.get(r) == TestHeadMinusSigma(r))
import CrosslinkResampling(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4", "p5"),
N = 5,
T = 2,
MaxRound = 4,
Proposer = ProposerSchedule,
Stream = TestStream,
Snapshots = ValidSnapshots,
ResampleOnNilPrecommit = false,
).* from "./CrosslinkResampling"
action unchangedAll = all {
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
firedAction' = firedAction,
}
pure val RepresentativeN5F2FaultyInitProposals =
Set(
{ src: "p4", round: 0, proposal: TestHeadMinusSigma(0), validRound: NilRound },
{ src: "p5", round: 0, proposal: TestHeadMinusSigma(1), validRound: 0 },
)
pure val RepresentativeN5F2FaultyInitPrevotes =
Set(
{ src: "p4", round: 0, id: TestHeadMinusSigma(0) },
{ src: "p5", round: 0, id: TestHeadMinusSigma(1) },
{ src: "p5", round: 0, id: NilSnapshot },
)
pure val RepresentativeN5F2FaultyInitPrecommits =
Set(
{ src: "p4", round: 0, id: TestHeadMinusSigma(0) },
{ src: "p5", round: 0, id: TestHeadMinusSigma(1) },
{ src: "p5", round: 0, id: NilSnapshot },
)
pure val RepresentativeN5F2FaultyInitDomainWellFormed =
RepresentativeN5F2FaultyInitProposals.forall(m =>
m.src.in(Faulty) and
m.round.in(Rounds) and
m.proposal.in(Snapshots) and
m.validRound.in(RoundsOrNil)
) and
RepresentativeN5F2FaultyInitPrevotes.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
) and
RepresentativeN5F2FaultyInitPrecommits.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
)
pure val RepresentativeN5F2FaultyInitUsesBothFaultyValidators =
RepresentativeN5F2FaultyInitProposals.exists(m =>
m.src == "p4" and m.proposal == "a1" and m.validRound == NilRound
) and
RepresentativeN5F2FaultyInitProposals.exists(m =>
m.src == "p5" and m.proposal == "b1" and m.validRound == 0
) and
RepresentativeN5F2FaultyInitPrevotes.exists(m =>
m.src == "p5" and m.id == NilSnapshot
) and
RepresentativeN5F2FaultyInitPrecommits.exists(m =>
m.src == "p5" and m.id == NilSnapshot
)
action InitWithRepresentativeN5F2FaultyEvidence =
nondet faultyProposals = RepresentativeN5F2FaultyInitProposals.powerset().oneOf()
nondet faultyPrevotes = RepresentativeN5F2FaultyInitPrevotes.powerset().oneOf()
nondet faultyPrecommits = RepresentativeN5F2FaultyInitPrecommits.powerset().oneOf()
all {
round' = Corr.mapBy(_ => 0),
step' = Corr.mapBy(_ => "propose"),
decision' = Corr.mapBy(_ => NilSnapshot),
lockedValue' = Corr.mapBy(_ => NilSnapshot),
lockedRound' = Corr.mapBy(_ => NilRound),
validValue' = Corr.mapBy(_ => NilSnapshot),
validRound' = Corr.mapBy(_ => NilRound),
cachedProposal' = Corr.mapBy(_ => NilSnapshot),
cachedProposalRound' = Corr.mapBy(_ => NilRound),
msgsPropose' = Rounds.mapBy(r =>
faultyProposals.filter(m => m.round == r)
),
msgsPrevote' = Rounds.mapBy(r =>
faultyPrevotes.filter(m => m.round == r)
),
msgsPrecommit' = Rounds.mapBy(r =>
faultyPrecommits.filter(m => m.round == r)
),
evidencePropose' = faultyProposals,
evidencePrevote' = faultyPrevotes,
evidencePrecommit' = faultyPrecommits,
firedAction' = "InitWithRepresentativeN5F2FaultyEvidence",
}
val BaselineBoundedN5F2ForkingFaultyInitSafety =
Safety and
TestStreamMatchesFixedSigma and
RepresentativeN5F2FaultyInitDomainWellFormed
run baselineBoundedN5F2ForkingFaultyInitCoversEvidenceTest = {
InitWithRepresentativeN5F2FaultyEvidence.then(all {
assert(THRESHOLD1 == 3),
assert(THRESHOLD2 == 5),
assert(size(Corr) >= THRESHOLD1),
assert(not(size(Corr) >= THRESHOLD2)),
assert(TestHeadMinusSigma(0) == "a1"),
assert(TestHeadMinusSigma(1) == "b1"),
assert(RepresentativeN5F2FaultyInitUsesBothFaultyValidators),
assert(EvidenceCoversObservedMessages),
assert(BaselineBoundedN5F2ForkingFaultyInitSafety),
unchangedAll,
})
}
run baselineBoundedN5F2ForkingFaultyInitAllowsCorrectProposalTest = {
InitWithRepresentativeN5F2FaultyEvidence
.then(InsertProposal("p1"))
.then(all {
assert(HasProposal(0, "a1")),
assert(EvidenceCoversObservedMessages),
assert(BaselineBoundedN5F2ForkingFaultyInitSafety),
unchangedAll,
})
}
}
module CrosslinkBaselineSingleFaultyInitN5F2ForkingModel {
/*
A full-domain single-message faulty-init harness for the n5/f2
fixed-sigma/forking surface. This carries the tractable symbolic abstraction
beyond n4/f2 while preserving the larger above-live-boundary process set.
*/
import CrosslinkBaselineModels.* from "./CrosslinkBaselineModels"
pure val TestRounds = 0.to(4)
pure def TestHeadMinusSigma(r: int): str =
AncestorByHeight
.get(ForkingBestTip.get(r))
.get(HeightBySnapshot.get(ForkingBestTip.get(r)) - 1)
pure val TestStream: int -> str =
TestRounds.mapBy(r => TestHeadMinusSigma(r))
pure val TestStreamMatchesFixedSigma =
TestRounds.forall(r => TestStream.get(r) == TestHeadMinusSigma(r))
import CrosslinkResampling(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4", "p5"),
N = 5,
T = 2,
MaxRound = 4,
Proposer = ProposerSchedule,
Stream = TestStream,
Snapshots = ValidSnapshots,
ResampleOnNilPrecommit = false,
).* from "./CrosslinkResampling"
action unchangedAll = all {
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
firedAction' = firedAction,
}
pure val SingleN5F2FaultyInitDomainWellFormed =
AllFaultyProposals.forall(m =>
m.src.in(Faulty) and
m.round.in(Rounds) and
m.proposal.in(Snapshots) and
m.validRound.in(RoundsOrNil)
) and
AllFaultyPrevotes.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
) and
AllFaultyPrecommits.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
)
action InitWithSingleN5F2FaultyEvidence =
nondet faultyProposal = AllFaultyProposals.oneOf()
nondet faultyPrevote = AllFaultyPrevotes.oneOf()
nondet faultyPrecommit = AllFaultyPrecommits.oneOf()
all {
round' = Corr.mapBy(_ => 0),
step' = Corr.mapBy(_ => "propose"),
decision' = Corr.mapBy(_ => NilSnapshot),
lockedValue' = Corr.mapBy(_ => NilSnapshot),
lockedRound' = Corr.mapBy(_ => NilRound),
validValue' = Corr.mapBy(_ => NilSnapshot),
validRound' = Corr.mapBy(_ => NilRound),
cachedProposal' = Corr.mapBy(_ => NilSnapshot),
cachedProposalRound' = Corr.mapBy(_ => NilRound),
msgsPropose' = Rounds.mapBy(r =>
Set(faultyProposal).filter(m => m.round == r)
),
msgsPrevote' = Rounds.mapBy(r =>
Set(faultyPrevote).filter(m => m.round == r)
),
msgsPrecommit' = Rounds.mapBy(r =>
Set(faultyPrecommit).filter(m => m.round == r)
),
evidencePropose' = Set(faultyProposal),
evidencePrevote' = Set(faultyPrevote),
evidencePrecommit' = Set(faultyPrecommit),
firedAction' = "InitWithSingleN5F2FaultyEvidence",
}
val BaselineSingleN5F2ForkingFaultyInitSafety =
Safety and
TestStreamMatchesFixedSigma and
SingleN5F2FaultyInitDomainWellFormed
run baselineSingleN5F2FaultyInitCoversEvidenceTest = {
InitWithSingleN5F2FaultyEvidence.then(all {
assert(THRESHOLD1 == 3),
assert(THRESHOLD2 == 5),
assert(size(Corr) >= THRESHOLD1),
assert(not(size(Corr) >= THRESHOLD2)),
assert(size(EvidencePropose) == 1),
assert(size(EvidencePrevote) == 1),
assert(size(EvidencePrecommit) == 1),
assert(EvidencePropose.subseteq(AllFaultyProposals)),
assert(EvidencePrevote.subseteq(AllFaultyPrevotes)),
assert(EvidencePrecommit.subseteq(AllFaultyPrecommits)),
assert(EvidenceCoversObservedMessages),
assert(BaselineSingleN5F2ForkingFaultyInitSafety),
unchangedAll,
})
}
}
module CrosslinkBaselinePairFaultyInitN5F2ForkingModel {
/*
A pair-message full-domain faulty-init harness for the n5/f2
fixed-sigma/forking surface. This is the next tractable frontier after the
single-message n5/f2 abstraction: it ranges over two arbitrary faulty
proposals, prevotes, and precommits without expanding the full powerset.
*/
import CrosslinkBaselineModels.* from "./CrosslinkBaselineModels"
pure val TestRounds = 0.to(4)
pure def TestHeadMinusSigma(r: int): str =
AncestorByHeight
.get(ForkingBestTip.get(r))
.get(HeightBySnapshot.get(ForkingBestTip.get(r)) - 1)
pure val TestStream: int -> str =
TestRounds.mapBy(r => TestHeadMinusSigma(r))
pure val TestStreamMatchesFixedSigma =
TestRounds.forall(r => TestStream.get(r) == TestHeadMinusSigma(r))
import CrosslinkResampling(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4", "p5"),
N = 5,
T = 2,
MaxRound = 4,
Proposer = ProposerSchedule,
Stream = TestStream,
Snapshots = ValidSnapshots,
ResampleOnNilPrecommit = false,
).* from "./CrosslinkResampling"
action unchangedAll = all {
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
firedAction' = firedAction,
}
pure val PairN5F2FaultyInitDomainWellFormed =
AllFaultyProposals.forall(m =>
m.src.in(Faulty) and
m.round.in(Rounds) and
m.proposal.in(Snapshots) and
m.validRound.in(RoundsOrNil)
) and
AllFaultyPrevotes.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
) and
AllFaultyPrecommits.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
)
action InitWithPairN5F2FaultyEvidence =
nondet faultyProposalA = AllFaultyProposals.oneOf()
nondet faultyProposalB = AllFaultyProposals.oneOf()
nondet faultyPrevoteA = AllFaultyPrevotes.oneOf()
nondet faultyPrevoteB = AllFaultyPrevotes.oneOf()
nondet faultyPrecommitA = AllFaultyPrecommits.oneOf()
nondet faultyPrecommitB = AllFaultyPrecommits.oneOf()
val faultyProposals = Set(faultyProposalA, faultyProposalB)
val faultyPrevotes = Set(faultyPrevoteA, faultyPrevoteB)
val faultyPrecommits = Set(faultyPrecommitA, faultyPrecommitB)
all {
round' = Corr.mapBy(_ => 0),
step' = Corr.mapBy(_ => "propose"),
decision' = Corr.mapBy(_ => NilSnapshot),
lockedValue' = Corr.mapBy(_ => NilSnapshot),
lockedRound' = Corr.mapBy(_ => NilRound),
validValue' = Corr.mapBy(_ => NilSnapshot),
validRound' = Corr.mapBy(_ => NilRound),
cachedProposal' = Corr.mapBy(_ => NilSnapshot),
cachedProposalRound' = Corr.mapBy(_ => NilRound),
msgsPropose' = Rounds.mapBy(r =>
faultyProposals.filter(m => m.round == r)
),
msgsPrevote' = Rounds.mapBy(r =>
faultyPrevotes.filter(m => m.round == r)
),
msgsPrecommit' = Rounds.mapBy(r =>
faultyPrecommits.filter(m => m.round == r)
),
evidencePropose' = faultyProposals,
evidencePrevote' = faultyPrevotes,
evidencePrecommit' = faultyPrecommits,
firedAction' = "InitWithPairN5F2FaultyEvidence",
}
val BaselinePairN5F2ForkingFaultyInitSafety =
Safety and
TestStreamMatchesFixedSigma and
PairN5F2FaultyInitDomainWellFormed
run baselinePairN5F2FaultyInitCoversEvidenceTest = {
InitWithPairN5F2FaultyEvidence.then(all {
assert(THRESHOLD1 == 3),
assert(THRESHOLD2 == 5),
assert(size(Corr) >= THRESHOLD1),
assert(not(size(Corr) >= THRESHOLD2)),
assert(size(EvidencePropose) >= 1),
assert(size(EvidencePropose) <= 2),
assert(size(EvidencePrevote) >= 1),
assert(size(EvidencePrevote) <= 2),
assert(size(EvidencePrecommit) >= 1),
assert(size(EvidencePrecommit) <= 2),
assert(EvidencePropose.subseteq(AllFaultyProposals)),
assert(EvidencePrevote.subseteq(AllFaultyPrevotes)),
assert(EvidencePrecommit.subseteq(AllFaultyPrecommits)),
assert(EvidenceCoversObservedMessages),
assert(BaselinePairN5F2ForkingFaultyInitSafety),
unchangedAll,
})
}
}
module CrosslinkBaselineTripleFaultyInitN5F2ForkingModel {
/*
A triple-message full-domain faulty-init harness for the n5/f2
fixed-sigma/forking surface. It extends the n5/f2 symbolic frontier to three
arbitrary faulty proposals, prevotes, and precommits while still avoiding the
full powerset expansion that is too large for Apalache.
*/
import CrosslinkBaselineModels.* from "./CrosslinkBaselineModels"
pure val TestRounds = 0.to(4)
pure def TestHeadMinusSigma(r: int): str =
AncestorByHeight
.get(ForkingBestTip.get(r))
.get(HeightBySnapshot.get(ForkingBestTip.get(r)) - 1)
pure val TestStream: int -> str =
TestRounds.mapBy(r => TestHeadMinusSigma(r))
pure val TestStreamMatchesFixedSigma =
TestRounds.forall(r => TestStream.get(r) == TestHeadMinusSigma(r))
import CrosslinkResampling(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4", "p5"),
N = 5,
T = 2,
MaxRound = 4,
Proposer = ProposerSchedule,
Stream = TestStream,
Snapshots = ValidSnapshots,
ResampleOnNilPrecommit = false,
).* from "./CrosslinkResampling"
action unchangedAll = all {
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
firedAction' = firedAction,
}
pure val TripleN5F2FaultyInitDomainWellFormed =
AllFaultyProposals.forall(m =>
m.src.in(Faulty) and
m.round.in(Rounds) and
m.proposal.in(Snapshots) and
m.validRound.in(RoundsOrNil)
) and
AllFaultyPrevotes.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
) and
AllFaultyPrecommits.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
)
action InitWithTripleN5F2FaultyEvidence =
nondet faultyProposalA = AllFaultyProposals.oneOf()
nondet faultyProposalB = AllFaultyProposals.oneOf()
nondet faultyProposalC = AllFaultyProposals.oneOf()
nondet faultyPrevoteA = AllFaultyPrevotes.oneOf()
nondet faultyPrevoteB = AllFaultyPrevotes.oneOf()
nondet faultyPrevoteC = AllFaultyPrevotes.oneOf()
nondet faultyPrecommitA = AllFaultyPrecommits.oneOf()
nondet faultyPrecommitB = AllFaultyPrecommits.oneOf()
nondet faultyPrecommitC = AllFaultyPrecommits.oneOf()
val faultyProposals = Set(faultyProposalA, faultyProposalB, faultyProposalC)
val faultyPrevotes = Set(faultyPrevoteA, faultyPrevoteB, faultyPrevoteC)
val faultyPrecommits =
Set(faultyPrecommitA, faultyPrecommitB, faultyPrecommitC)
all {
round' = Corr.mapBy(_ => 0),
step' = Corr.mapBy(_ => "propose"),
decision' = Corr.mapBy(_ => NilSnapshot),
lockedValue' = Corr.mapBy(_ => NilSnapshot),
lockedRound' = Corr.mapBy(_ => NilRound),
validValue' = Corr.mapBy(_ => NilSnapshot),
validRound' = Corr.mapBy(_ => NilRound),
cachedProposal' = Corr.mapBy(_ => NilSnapshot),
cachedProposalRound' = Corr.mapBy(_ => NilRound),
msgsPropose' = Rounds.mapBy(r =>
faultyProposals.filter(m => m.round == r)
),
msgsPrevote' = Rounds.mapBy(r =>
faultyPrevotes.filter(m => m.round == r)
),
msgsPrecommit' = Rounds.mapBy(r =>
faultyPrecommits.filter(m => m.round == r)
),
evidencePropose' = faultyProposals,
evidencePrevote' = faultyPrevotes,
evidencePrecommit' = faultyPrecommits,
firedAction' = "InitWithTripleN5F2FaultyEvidence",
}
val BaselineTripleN5F2ForkingFaultyInitSafety =
Safety and
TestStreamMatchesFixedSigma and
TripleN5F2FaultyInitDomainWellFormed
run baselineTripleN5F2FaultyInitCoversEvidenceTest = {
InitWithTripleN5F2FaultyEvidence.then(all {
assert(THRESHOLD1 == 3),
assert(THRESHOLD2 == 5),
assert(size(Corr) >= THRESHOLD1),
assert(not(size(Corr) >= THRESHOLD2)),
assert(size(EvidencePropose) >= 1),
assert(size(EvidencePropose) <= 3),
assert(size(EvidencePrevote) >= 1),
assert(size(EvidencePrevote) <= 3),
assert(size(EvidencePrecommit) >= 1),
assert(size(EvidencePrecommit) <= 3),
assert(EvidencePropose.subseteq(AllFaultyProposals)),
assert(EvidencePrevote.subseteq(AllFaultyPrevotes)),
assert(EvidencePrecommit.subseteq(AllFaultyPrecommits)),
assert(EvidenceCoversObservedMessages),
assert(BaselineTripleN5F2ForkingFaultyInitSafety),
unchangedAll,
})
}
}
module CrosslinkBaselineFullFaultyInitForkingModel {
/*
A full-domain fixed-sigma/forking faulty-init harness for the upstream-shaped
n4/f1 baseline instance. This keeps the larger parameter surface usable in
quick checks while leaving the bounded faulty-init harness as the symbolic
gate.
*/
import CrosslinkBaselineModels.* from "./CrosslinkBaselineModels"
pure val TestRounds = 0.to(4)
pure def TestHeadMinusSigma(r: int): str =
AncestorByHeight
.get(ForkingBestTip.get(r))
.get(HeightBySnapshot.get(ForkingBestTip.get(r)) - 1)
pure val TestStream: int -> str =
TestRounds.mapBy(r => TestHeadMinusSigma(r))
pure val TestStreamMatchesFixedSigma =
TestRounds.forall(r => TestStream.get(r) == TestHeadMinusSigma(r))
import CrosslinkResamplingTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 4,
Proposer = ProposerSchedule,
Stream = TestStream,
Snapshots = ValidSnapshots,
ResampleOnNilPrecommit = false,
ExpectedRound1Proposal = TestStream.get(0),
).* from "./CrosslinkResampling"
pure val FullFaultyInitDomainWellFormed =
AllFaultyProposals.forall(m =>
m.src.in(Faulty) and
m.round.in(Rounds) and
m.proposal.in(Snapshots) and
m.validRound.in(RoundsOrNil)
) and
AllFaultyPrevotes.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
) and
AllFaultyPrecommits.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
)
pure val FullFaultyInitUsesWholeForkingDomain =
AllFaultyProposals.exists(m =>
m.src == "p4" and
m.round == 4 and
m.proposal == "b2" and
m.validRound == 4
) and
AllFaultyPrevotes.exists(m =>
m.src == "p4" and m.round == 4 and m.id == NilSnapshot
) and
AllFaultyPrecommits.exists(m =>
m.src == "p4" and m.round == 4 and m.id == "a1"
)
val BaselineFullForkingFaultyInitSafety =
Safety and
TestStreamMatchesFixedSigma and
FullFaultyInitDomainWellFormed
run baselineFullForkingFaultyInitCoversEvidenceTest = {
InitWithFaultyEvidence.then(all {
assert(TestHeadMinusSigma(0) == "a1"),
assert(TestHeadMinusSigma(1) == "b1"),
assert(FullFaultyInitUsesWholeForkingDomain),
assert(EvidenceCoversObservedMessages),
assert(BaselineFullForkingFaultyInitSafety),
unchangedAll,
})
}
run baselineFullForkingFaultyInitAllowsCorrectProposalTest = {
InitWithFaultyEvidence
.then(InsertProposal("p1"))
.then(all {
assert(HasProposal(0, "a1")),
assert(EvidenceCoversObservedMessages),
assert(BaselineFullForkingFaultyInitSafety),
unchangedAll,
})
}
}
module CrosslinkBaselineFullFaultyInitN5F1ForkingModel {
/*
A full-domain fixed-sigma/forking faulty-init harness for the n5/f1 baseline
instance. This fills the intermediate one-fault validator surface between
the upstream-shaped n4/f1 quick harness and the proper n7/f2 boundary.
*/
import CrosslinkBaselineModels.* from "./CrosslinkBaselineModels"
pure val TestRounds = 0.to(4)
pure def TestHeadMinusSigma(r: int): str =
AncestorByHeight
.get(ForkingBestTip.get(r))
.get(HeightBySnapshot.get(ForkingBestTip.get(r)) - 1)
pure val TestStream: int -> str =
TestRounds.mapBy(r => TestHeadMinusSigma(r))
pure val TestStreamMatchesFixedSigma =
TestRounds.forall(r => TestStream.get(r) == TestHeadMinusSigma(r))
import CrosslinkResamplingTest(
Corr = Set("p1", "p2", "p3", "p4"),
Faulty = Set("p5"),
N = 5,
T = 1,
MaxRound = 4,
Proposer = ProposerSchedule,
Stream = TestStream,
Snapshots = ValidSnapshots,
ResampleOnNilPrecommit = false,
ExpectedRound1Proposal = TestStream.get(0),
).* from "./CrosslinkResampling"
pure val FullN5F1FaultyInitDomainWellFormed =
AllFaultyProposals.forall(m =>
m.src.in(Faulty) and
m.round.in(Rounds) and
m.proposal.in(Snapshots) and
m.validRound.in(RoundsOrNil)
) and
AllFaultyPrevotes.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
) and
AllFaultyPrecommits.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
)
pure val FullN5F1FaultyInitUsesWholeForkingDomain =
AllFaultyProposals.exists(m =>
m.src == "p5" and
m.round == 4 and
m.proposal == "b2" and
m.validRound == 4
) and
AllFaultyPrevotes.exists(m =>
m.src == "p5" and m.round == 4 and m.id == NilSnapshot
) and
AllFaultyPrecommits.exists(m =>
m.src == "p5" and m.round == 4 and m.id == "a1"
)
val BaselineFullN5F1ForkingFaultyInitSafety =
Safety and
TestStreamMatchesFixedSigma and
FullN5F1FaultyInitDomainWellFormed
run baselineFullN5F1ForkingFaultyInitCoversEvidenceTest = {
InitWithFaultyEvidence.then(all {
assert(THRESHOLD1 == 2),
assert(THRESHOLD2 == 3),
assert(TestHeadMinusSigma(0) == "a1"),
assert(TestHeadMinusSigma(1) == "b1"),
assert(FullN5F1FaultyInitUsesWholeForkingDomain),
assert(EvidenceCoversObservedMessages),
assert(BaselineFullN5F1ForkingFaultyInitSafety),
unchangedAll,
})
}
run baselineFullN5F1ForkingFaultyInitAllowsCorrectProposalTest = {
InitWithFaultyEvidence
.then(InsertProposal("p1"))
.then(all {
assert(HasProposal(0, "a1")),
assert(EvidenceCoversObservedMessages),
assert(BaselineFullN5F1ForkingFaultyInitSafety),
unchangedAll,
})
}
}
module CrosslinkBaselineFullFaultyInitN4F2ForkingModel {
/*
A full-domain faulty-init harness for the n4/f2 fixed-sigma/forking surface.
This is an above-live-boundary instance: the two correct validators cannot
form f+1 catchup evidence or a 2f+1 value commit, but the shared model should
still expose the full arbitrary faulty-message domain in the quick gate.
*/
import CrosslinkBaselineModels.* from "./CrosslinkBaselineModels"
pure val TestRounds = 0.to(4)
pure def TestHeadMinusSigma(r: int): str =
AncestorByHeight
.get(ForkingBestTip.get(r))
.get(HeightBySnapshot.get(ForkingBestTip.get(r)) - 1)
pure val TestStream: int -> str =
TestRounds.mapBy(r => TestHeadMinusSigma(r))
pure val TestStreamMatchesFixedSigma =
TestRounds.forall(r => TestStream.get(r) == TestHeadMinusSigma(r))
import CrosslinkResampling(
Corr = Set("p1", "p2"),
Faulty = Set("p3", "p4"),
N = 4,
T = 2,
MaxRound = 4,
Proposer = ProposerSchedule,
Stream = TestStream,
Snapshots = ValidSnapshots,
ResampleOnNilPrecommit = false,
).* from "./CrosslinkResampling"
action unchangedAll = all {
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
firedAction' = firedAction,
}
pure val FullN4F2FaultyInitDomainWellFormed =
AllFaultyProposals.forall(m =>
m.src.in(Faulty) and
m.round.in(Rounds) and
m.proposal.in(Snapshots) and
m.validRound.in(RoundsOrNil)
) and
AllFaultyPrevotes.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
) and
AllFaultyPrecommits.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
)
pure val FullN4F2FaultyInitUsesWholeForkingDomain =
AllFaultyProposals.exists(m =>
m.src == "p4" and
m.round == 4 and
m.proposal == "b2" and
m.validRound == 4
) and
AllFaultyPrevotes.exists(m =>
m.src == "p3" and m.round == 4 and m.id == NilSnapshot
) and
AllFaultyPrecommits.exists(m =>
m.src == "p4" and m.round == 4 and m.id == "a1"
)
val BaselineFullN4F2ForkingFaultyInitSafety =
Safety and
TestStreamMatchesFixedSigma and
FullN4F2FaultyInitDomainWellFormed
run baselineFullN4F2ForkingFaultyInitCoversEvidenceTest = {
InitWithFaultyEvidence.then(all {
assert(THRESHOLD1 == 3),
assert(THRESHOLD2 == 5),
assert(not(size(Corr) >= THRESHOLD1)),
assert(not(size(Corr) >= THRESHOLD2)),
assert(TestHeadMinusSigma(0) == "a1"),
assert(TestHeadMinusSigma(1) == "b1"),
assert(FullN4F2FaultyInitUsesWholeForkingDomain),
assert(EvidenceCoversObservedMessages),
assert(BaselineFullN4F2ForkingFaultyInitSafety),
unchangedAll,
})
}
run baselineFullN4F2ForkingFaultyInitAllowsCorrectProposalTest = {
InitWithFaultyEvidence
.then(InsertProposal("p1"))
.then(all {
assert(HasProposal(0, "a1")),
assert(EvidenceCoversObservedMessages),
assert(BaselineFullN4F2ForkingFaultyInitSafety),
unchangedAll,
})
}
}
module CrosslinkBaselineFullFaultyInitN5F2ForkingModel {
/*
A full-domain faulty-init harness for the n5/f2 fixed-sigma/forking surface.
This above-live-boundary instance has enough correct validators for f+1
catchup evidence, but not enough correct validators for a 2f+1 value commit,
so arbitrary faulty evidence has to stay visible in the baseline quick gate.
*/
import CrosslinkBaselineModels.* from "./CrosslinkBaselineModels"
pure val TestRounds = 0.to(4)
pure def TestHeadMinusSigma(r: int): str =
AncestorByHeight
.get(ForkingBestTip.get(r))
.get(HeightBySnapshot.get(ForkingBestTip.get(r)) - 1)
pure val TestStream: int -> str =
TestRounds.mapBy(r => TestHeadMinusSigma(r))
pure val TestStreamMatchesFixedSigma =
TestRounds.forall(r => TestStream.get(r) == TestHeadMinusSigma(r))
import CrosslinkResampling(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4", "p5"),
N = 5,
T = 2,
MaxRound = 4,
Proposer = ProposerSchedule,
Stream = TestStream,
Snapshots = ValidSnapshots,
ResampleOnNilPrecommit = false,
).* from "./CrosslinkResampling"
action unchangedAll = all {
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
firedAction' = firedAction,
}
pure val FullN5F2FaultyInitDomainWellFormed =
AllFaultyProposals.forall(m =>
m.src.in(Faulty) and
m.round.in(Rounds) and
m.proposal.in(Snapshots) and
m.validRound.in(RoundsOrNil)
) and
AllFaultyPrevotes.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
) and
AllFaultyPrecommits.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
)
pure val FullN5F2FaultyInitUsesWholeForkingDomain =
AllFaultyProposals.exists(m =>
m.src == "p5" and
m.round == 4 and
m.proposal == "b2" and
m.validRound == 4
) and
AllFaultyPrevotes.exists(m =>
m.src == "p4" and m.round == 4 and m.id == NilSnapshot
) and
AllFaultyPrecommits.exists(m =>
m.src == "p5" and m.round == 4 and m.id == "a1"
)
val BaselineFullN5F2ForkingFaultyInitSafety =
Safety and
TestStreamMatchesFixedSigma and
FullN5F2FaultyInitDomainWellFormed
run baselineFullN5F2ForkingFaultyInitCoversEvidenceTest = {
InitWithFaultyEvidence.then(all {
assert(THRESHOLD1 == 3),
assert(THRESHOLD2 == 5),
assert(size(Corr) >= THRESHOLD1),
assert(not(size(Corr) >= THRESHOLD2)),
assert(TestHeadMinusSigma(0) == "a1"),
assert(TestHeadMinusSigma(1) == "b1"),
assert(FullN5F2FaultyInitUsesWholeForkingDomain),
assert(EvidenceCoversObservedMessages),
assert(BaselineFullN5F2ForkingFaultyInitSafety),
unchangedAll,
})
}
run baselineFullN5F2ForkingFaultyInitAllowsCorrectProposalTest = {
InitWithFaultyEvidence
.then(InsertProposal("p1"))
.then(all {
assert(HasProposal(0, "a1")),
assert(EvidenceCoversObservedMessages),
assert(BaselineFullN5F2ForkingFaultyInitSafety),
unchangedAll,
})
}
}
module CrosslinkBaselineFullFaultyInitN7F2ForkingModel {
/*
A full-domain faulty-init harness for the proper f=2 boundary instance. This
keeps arbitrary faulty proposal, prevote, and precommit injection alive for
the n7/f2 fixed-sigma/forking surface in the Rust-backed quick gate.
*/
import CrosslinkBaselineModels.* from "./CrosslinkBaselineModels"
pure val TestRounds = 0.to(4)
pure def TestHeadMinusSigma(r: int): str =
AncestorByHeight
.get(ForkingBestTip.get(r))
.get(HeightBySnapshot.get(ForkingBestTip.get(r)) - 1)
pure val TestStream: int -> str =
TestRounds.mapBy(r => TestHeadMinusSigma(r))
pure val TestStreamMatchesFixedSigma =
TestRounds.forall(r => TestStream.get(r) == TestHeadMinusSigma(r))
import CrosslinkResampling(
Corr = Set("p1", "p2", "p3", "p4", "p5"),
Faulty = Set("p6", "p7"),
N = 7,
T = 2,
MaxRound = 4,
Proposer = ProposerSchedule,
Stream = TestStream,
Snapshots = ValidSnapshots,
ResampleOnNilPrecommit = false,
).* from "./CrosslinkResampling"
action unchangedAll = all {
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
firedAction' = firedAction,
}
pure val FullN7F2FaultyInitDomainWellFormed =
AllFaultyProposals.forall(m =>
m.src.in(Faulty) and
m.round.in(Rounds) and
m.proposal.in(Snapshots) and
m.validRound.in(RoundsOrNil)
) and
AllFaultyPrevotes.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
) and
AllFaultyPrecommits.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
)
pure val FullN7F2FaultyInitUsesWholeForkingDomain =
AllFaultyProposals.exists(m =>
m.src == "p7" and
m.round == 4 and
m.proposal == "b2" and
m.validRound == 4
) and
AllFaultyPrevotes.exists(m =>
m.src == "p6" and m.round == 4 and m.id == NilSnapshot
) and
AllFaultyPrecommits.exists(m =>
m.src == "p7" and m.round == 4 and m.id == "a1"
)
val BaselineFullN7F2ForkingFaultyInitSafety =
Safety and
TestStreamMatchesFixedSigma and
FullN7F2FaultyInitDomainWellFormed
run baselineFullN7F2ForkingFaultyInitCoversEvidenceTest = {
InitWithFaultyEvidence.then(all {
assert(THRESHOLD1 == 3),
assert(THRESHOLD2 == 5),
assert(TestHeadMinusSigma(0) == "a1"),
assert(TestHeadMinusSigma(1) == "b1"),
assert(FullN7F2FaultyInitUsesWholeForkingDomain),
assert(EvidenceCoversObservedMessages),
assert(BaselineFullN7F2ForkingFaultyInitSafety),
unchangedAll,
})
}
run baselineFullN7F2ForkingFaultyInitAllowsCorrectProposalTest = {
InitWithFaultyEvidence
.then(InsertProposal("p1"))
.then(all {
assert(HasProposal(0, "a1")),
assert(EvidenceCoversObservedMessages),
assert(BaselineFullN7F2ForkingFaultyInitSafety),
unchangedAll,
})
}
}
module CrosslinkBaselineBoundedFaultyInitN7F2ForkingModel {
/*
A representative faulty-init symbolic harness for the proper f=2 boundary
instance. This checks the n7/f2 validator shape without expanding the full
faulty-message powerset.
*/
import CrosslinkBaselineModels.* from "./CrosslinkBaselineModels"
pure val TestRounds = 0.to(4)
pure def TestHeadMinusSigma(r: int): str =
AncestorByHeight
.get(ForkingBestTip.get(r))
.get(HeightBySnapshot.get(ForkingBestTip.get(r)) - 1)
pure val TestStream: int -> str =
TestRounds.mapBy(r => TestHeadMinusSigma(r))
pure val TestStreamMatchesFixedSigma =
TestRounds.forall(r => TestStream.get(r) == TestHeadMinusSigma(r))
import CrosslinkResampling(
Corr = Set("p1", "p2", "p3", "p4", "p5"),
Faulty = Set("p6", "p7"),
N = 7,
T = 2,
MaxRound = 4,
Proposer = ProposerSchedule,
Stream = TestStream,
Snapshots = ValidSnapshots,
ResampleOnNilPrecommit = false,
).* from "./CrosslinkResampling"
action unchangedAll = all {
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
firedAction' = firedAction,
}
pure val RepresentativeN7F2FaultyInitProposals =
Set(
{ src: "p6", round: 0, proposal: TestHeadMinusSigma(0), validRound: NilRound },
{ src: "p7", round: 0, proposal: TestHeadMinusSigma(1), validRound: 0 },
)
pure val RepresentativeN7F2FaultyInitPrevotes =
Set(
{ src: "p6", round: 0, id: TestHeadMinusSigma(0) },
{ src: "p7", round: 0, id: TestHeadMinusSigma(1) },
{ src: "p7", round: 0, id: NilSnapshot },
)
pure val RepresentativeN7F2FaultyInitPrecommits =
Set(
{ src: "p6", round: 0, id: TestHeadMinusSigma(0) },
{ src: "p7", round: 0, id: TestHeadMinusSigma(1) },
{ src: "p7", round: 0, id: NilSnapshot },
)
pure val RepresentativeN7F2FaultyInitDomainWellFormed =
RepresentativeN7F2FaultyInitProposals.forall(m =>
m.src.in(Faulty) and
m.round.in(Rounds) and
m.proposal.in(Snapshots) and
m.validRound.in(RoundsOrNil)
) and
RepresentativeN7F2FaultyInitPrevotes.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
) and
RepresentativeN7F2FaultyInitPrecommits.forall(m =>
m.src.in(Faulty) and m.round.in(Rounds) and m.id.in(SnapshotsOrNil)
)
pure val RepresentativeN7F2FaultyInitUsesBothFaultyValidators =
RepresentativeN7F2FaultyInitProposals.exists(m =>
m.src == "p6" and m.proposal == "a1" and m.validRound == NilRound
) and
RepresentativeN7F2FaultyInitProposals.exists(m =>
m.src == "p7" and m.proposal == "b1" and m.validRound == 0
) and
RepresentativeN7F2FaultyInitPrevotes.exists(m =>
m.src == "p7" and m.id == NilSnapshot
) and
RepresentativeN7F2FaultyInitPrecommits.exists(m =>
m.src == "p7" and m.id == NilSnapshot
)
action InitWithRepresentativeN7F2FaultyEvidence =
nondet faultyProposals = RepresentativeN7F2FaultyInitProposals.powerset().oneOf()
nondet faultyPrevotes = RepresentativeN7F2FaultyInitPrevotes.powerset().oneOf()
nondet faultyPrecommits = RepresentativeN7F2FaultyInitPrecommits.powerset().oneOf()
all {
round' = Corr.mapBy(_ => 0),
step' = Corr.mapBy(_ => "propose"),
decision' = Corr.mapBy(_ => NilSnapshot),
lockedValue' = Corr.mapBy(_ => NilSnapshot),
lockedRound' = Corr.mapBy(_ => NilRound),
validValue' = Corr.mapBy(_ => NilSnapshot),
validRound' = Corr.mapBy(_ => NilRound),
cachedProposal' = Corr.mapBy(_ => NilSnapshot),
cachedProposalRound' = Corr.mapBy(_ => NilRound),
msgsPropose' = Rounds.mapBy(r =>
faultyProposals.filter(m => m.round == r)
),
msgsPrevote' = Rounds.mapBy(r =>
faultyPrevotes.filter(m => m.round == r)
),
msgsPrecommit' = Rounds.mapBy(r =>
faultyPrecommits.filter(m => m.round == r)
),
evidencePropose' = faultyProposals,
evidencePrevote' = faultyPrevotes,
evidencePrecommit' = faultyPrecommits,
firedAction' = "InitWithRepresentativeN7F2FaultyEvidence",
}
val BaselineBoundedN7F2ForkingFaultyInitSafety =
Safety and
TestStreamMatchesFixedSigma and
RepresentativeN7F2FaultyInitDomainWellFormed
run baselineBoundedN7F2ForkingFaultyInitCoversEvidenceTest = {
InitWithRepresentativeN7F2FaultyEvidence.then(all {
assert(THRESHOLD1 == 3),
assert(THRESHOLD2 == 5),
assert(size(Corr) >= THRESHOLD1),
assert(size(Corr) >= THRESHOLD2),
assert(TestHeadMinusSigma(0) == "a1"),
assert(TestHeadMinusSigma(1) == "b1"),
assert(RepresentativeN7F2FaultyInitUsesBothFaultyValidators),
assert(EvidenceCoversObservedMessages),
assert(BaselineBoundedN7F2ForkingFaultyInitSafety),
unchangedAll,
})
}
run baselineBoundedN7F2ForkingFaultyInitAllowsCorrectProposalTest = {
InitWithRepresentativeN7F2FaultyEvidence
.then(InsertProposal("p1"))
.then(all {
assert(HasProposal(0, "a1")),
assert(EvidenceCoversObservedMessages),
assert(BaselineBoundedN7F2ForkingFaultyInitSafety),
unchangedAll,
})
}
}
module CrosslinkBaselineCounterexampleModel {
/*
False-invariant harnesses. These are intentionally bad claims that should
fail under seeded evidence, proving the accountability predicates are not
vacuous in the bounded baseline model.
*/
import CrosslinkResamplingTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 2,
Proposer = Map(0 -> "p1", 1 -> "p2", 2 -> "p3"),
Stream = Map(0 -> "s0", 1 -> "s1", 2 -> "s2"),
Snapshots = Set("s0", "s1", "s2"),
ResampleOnNilPrecommit = false,
ExpectedRound1Proposal = "s0",
).* from "./CrosslinkResampling"
val NoConflictingValueCommits =
Rounds.forall(r1 =>
Rounds.forall(r2 =>
Snapshots.forall(v1 =>
Snapshots.forall(v2 =>
not(ConflictingValueCommits(r1, v1, r2, v2))
)
)
)
)
val AgreementOrAmnesia =
Agreement or Faulty.forall(p => AmnesiaBy(p))
val ShowMeAmnesiaWithoutEquivocation =
(not(Agreement) and Faulty.exists(p => not(EquivocationBy(p)))) implies
Faulty.forall(p => not(AmnesiaBy(p)))
val AmnesiaImpliesEquivocation =
Faulty.exists(p => AmnesiaBy(p)) implies Faulty.exists(q => EquivocationBy(q))
val NeverUndecidedInMaxRound =
Corr.forall(p => round.get(p) == MaxRound) implies
Corr.forall(p => decision.get(p) != NilSnapshot)
val NilPrecommitClearsSameRoundTendermintState =
Corr.forall(p =>
Rounds.forall(r =>
(round.get(p) > r and NilPrecommitCert(r)) implies all {
not(validRound.get(p) == r and validValue.get(p) != NilSnapshot),
not(lockedRound.get(p) == r and lockedValue.get(p) != NilSnapshot),
}
)
)
action SeedConflictingDecisions: bool =
all {
decision' = decision.set("p1", "s0").set("p2", "s1"),
firedAction' = "SeedConflictingDecisions",
round' = round,
step' = step,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
}
action SeedAllCorrectAtMaxRoundWithoutDecision: bool =
all {
round' = Corr.mapBy(_ => MaxRound),
step' = Corr.mapBy(_ => "propose"),
decision' = Corr.mapBy(_ => NilSnapshot),
firedAction' = "SeedAllCorrectAtMaxRoundWithoutDecision",
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
}
run falseNoConflictingCommitsInvariantFailsTest = {
Init
.then(SeedCorrectPrecommitEvidence("p1", 0, "s0"))
.then(SeedCorrectPrecommitEvidence("p2", 0, "s0"))
.then(SeedFaultyPrecommitEvidence("p4", 0, "s0"))
.then(SeedCorrectPrecommitEvidence("p2", 1, "s1"))
.then(SeedCorrectPrecommitEvidence("p3", 1, "s1"))
.then(SeedFaultyPrecommitEvidence("p4", 1, "s1"))
.then(all {
NoConflictingValueCommits,
unchangedAll,
})
.fail()
}
run falseNoAmnesiaEvidenceInvariantFailsTest = {
Init
.then(SeedCorrectPrecommitEvidence("p1", 0, "s0"))
.then(SeedCorrectPrecommitEvidence("p2", 0, "s0"))
.then(SeedFaultyPrecommitEvidence("p4", 0, "s0"))
.then(SeedCorrectPrecommitEvidence("p2", 1, "s1"))
.then(SeedCorrectPrecommitEvidence("p3", 1, "s1"))
.then(SeedFaultyPrecommitEvidence("p4", 1, "s1"))
.then(all {
not(AmnesiaBy("p2")),
unchangedAll,
})
.fail()
}
run falseNoEquivocationEvidenceInvariantFailsTest = {
Init
.then(SeedFaultyPrevoteEvidence("p4", 0, "s0"))
.then(SeedFaultyPrevoteEvidence("p4", 0, "s1"))
.then(all {
not(EquivocationBy("p4")),
unchangedAll,
})
.fail()
}
run falseAgreementInvariantFailsTest = {
Init
.then(SeedConflictingDecisions)
.then(all {
Agreement,
unchangedAll,
})
.fail()
}
run falseAgreementOrAmnesiaInvariantFailsTest = {
Init
.then(SeedConflictingDecisions)
.then(SeedFaultyProposalEvidence("p4", 0, "s0", -1))
.then(SeedFaultyProposalEvidence("p4", 0, "s1", -1))
.then(all {
AgreementOrAmnesia,
unchangedAll,
})
.fail()
}
run falseAmnesiaImpliesEquivocationInvariantFailsTest = {
Init
.then(SeedFaultyPrecommitEvidence("p4", 0, "s0"))
.then(SeedFaultyPrecommitEvidence("p4", 1, "s1"))
.then(all {
AmnesiaImpliesEquivocation,
unchangedAll,
})
.fail()
}
run falseShowMeAmnesiaWithoutEquivocationInvariantFailsTest = {
Init
.then(SeedConflictingDecisions)
.then(SeedFaultyPrecommitEvidence("p4", 0, "s0"))
.then(SeedFaultyPrecommitEvidence("p4", 1, "s1"))
.then(all {
ShowMeAmnesiaWithoutEquivocation,
unchangedAll,
})
.fail()
}
run falseNeverUndecidedInMaxRoundInvariantFailsTest = {
Init
.then(SeedAllCorrectAtMaxRoundWithoutDecision)
.then(all {
NeverUndecidedInMaxRound,
unchangedAll,
})
.fail()
}
run falseNilPrecommitClearsSameRoundLockInvariantFailsTest = {
Init
.then(SeedValidNilPrecommitState("p1"))
.then(SeedValidNilPrecommitState("p2"))
.then(SeedFaultyNilPrecommit("p4"))
.then(SeedSameRoundValueLock("p3"))
.then(StartNextRoundAfterPrecommitQuorum("p3"))
.then(all {
NilPrecommitClearsSameRoundTendermintState,
unchangedAll,
})
.fail()
}
}
// -*- mode: Bluespec; -*-
module CrosslinkBaselineBftHeightsModel {
/*
Baseline Crosslink finality advances at consecutive BFT consensus heights
while using a fixed PoW confirmation depth.
This gives the baseline variant a named heighted-finality witness alongside
the round-level, accountability, PoW-sampling, and single-decision finality
models.
*/
import CrosslinkBftHeights(
MaxConsensusHeight = 2,
MaxPowHeight = 4,
Sigma = 1,
Snapshots = Set("g", "a1", "a2", "a3", "a4", "b2", "b3", "b4"),
InitialFinalized = "g",
ScheduledDecision = Map(
0 -> "g",
1 -> "a2",
2 -> "a3",
3 -> "a4",
),
ScheduledTip = Map(
0 -> "g",
1 -> "a3",
2 -> "a4",
3 -> "a4",
),
HeightOf = Map(
"g" -> 0,
"a1" -> 1,
"a2" -> 2,
"a3" -> 3,
"a4" -> 4,
"b2" -> 2,
"b3" -> 3,
"b4" -> 4,
),
AncestorAt = Map(
"g" -> Map(0 -> "g", 1 -> "None", 2 -> "None", 3 -> "None", 4 -> "None"),
"a1" -> Map(0 -> "g", 1 -> "a1", 2 -> "None", 3 -> "None", 4 -> "None"),
"a2" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "None", 4 -> "None"),
"a3" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3", 4 -> "None"),
"a4" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3", 4 -> "a4"),
"b2" -> Map(0 -> "g", 1 -> "a1", 2 -> "b2", 3 -> "None", 4 -> "None"),
"b3" -> Map(0 -> "g", 1 -> "a1", 2 -> "b2", 3 -> "b3", 4 -> "None"),
"b4" -> Map(0 -> "g", 1 -> "a1", 2 -> "b2", 3 -> "b3", 4 -> "b4"),
),
).* from "./CrosslinkBftHeights"
action unchangedAll = all {
consensusHeight' = consensusHeight,
latestFinal' = latestFinal,
finalized' = finalized,
heightAction' = heightAction,
}
val BaselineBftHeightSafety =
Safety
val ForkFinalityAttemptIsValid =
ValidHeightDecision("b3", "b4")
run baselineConsecutiveBftHeightsAdvanceFinalityTest = {
Init
.then(ApplyScheduledDecision)
.then(all {
assert(consensusHeight == 1),
assert(latestFinal == "a2"),
assert(finalized.contains("a2")),
assert(BaselineBftHeightSafety),
unchangedAll,
})
.then(ApplyScheduledDecision)
.then(all {
assert(consensusHeight == 2),
assert(latestFinal == "a3"),
assert(finalized.contains("a3")),
assert(BaselineBftHeightSafety),
unchangedAll,
})
}
run baselineRejectsSkippedBftHeightTest = {
Init
.then(ApplyDecisionAt(2, "a3", "a4"))
.fail()
}
run baselineRejectsForkAfterPrefixFinalityTest = {
Init
.then(ApplyDecisionAt(1, "a2", "a3"))
.then(ApplyDecisionAt(2, "b3", "b4"))
.fail()
}
run falseForkFinalityAttemptIsValidTest = {
Init
.then(ApplyDecisionAt(1, "a2", "a3"))
.then(all {
ForkFinalityAttemptIsValid,
unchangedAll,
})
.fail()
}
}
// -*- mode: Bluespec; -*-
module CrosslinkBaselineFinalityStableModel {
/*
Baseline Crosslink finality composes the current fixed-sigma/sticky
Tenderlink behavior with Crosslink's finalized-prefix rule.
In this stable-stream model, the sampled PoW snapshot does not change across
rounds, so ordinary Tendermint voting can decide the sampled value and the
Crosslink finality rule can finalize it once the selected tail is confirmed.
*/
import CrosslinkComposed(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 2,
Proposer = Map(0 -> "p1", 1 -> "p2", 2 -> "p3"),
Stream = Map(0 -> "a2", 1 -> "a2", 2 -> "a2"),
Snapshots = Set("g", "a1", "a2", "a3", "b1", "b2", "b3"),
ResampleOnNilPrecommit = false,
MaxHeight = 3,
Sigma = 1,
InitialFinalized = "g",
HeightOf = Map(
"g" -> 0,
"a1" -> 1,
"a2" -> 2,
"a3" -> 3,
"b1" -> 1,
"b2" -> 2,
"b3" -> 3,
),
AncestorAt = Map(
"g" -> Map(0 -> "g", 1 -> "None", 2 -> "None", 3 -> "None"),
"a1" -> Map(0 -> "g", 1 -> "a1", 2 -> "None", 3 -> "None"),
"a2" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "None"),
"a3" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3"),
"b1" -> Map(0 -> "g", 1 -> "b1", 2 -> "None", 3 -> "None"),
"b2" -> Map(0 -> "g", 1 -> "b1", 2 -> "b2", 3 -> "None"),
"b3" -> Map(0 -> "g", 1 -> "b1", 2 -> "b2", 3 -> "b3"),
),
).* from "./CrosslinkComposed"
action composedUnchanged = all {
ProtocolUnchanged,
latestFinal' = latestFinal,
finalized' = finalized,
finalityAction' = finalityAction,
}
run baselineStableStreamFinalizesSampledCandidateTest = {
ComposedInit
.then(Propose("p1"))
.then(Prevote("p1", "a2"))
.then(Prevote("p2", "a2"))
.then(Prevote("p3", "a2"))
.then(PrecommitValue("p1", "a2"))
.then(PrecommitValue("p2", "a2"))
.then(PrecommitValue("p3", "a2"))
.then(DecideValue("p1", 0, "a2"))
.then(FinalizeDecision("p1", "a2", "a3"))
.then(all {
assert(decision.get("p1") == "a2"),
assert(latestFinal == "a2"),
assert(finalized.contains("a2")),
assert(ComposedSafety),
composedUnchanged,
})
}
}
module CrosslinkBaselineFinalityStreamChangeModel {
/*
This model keeps the same fixed-sigma/sticky baseline but lets the PoW stream
change after the abandoned round. A nil-precommit quorum advances the round,
but it does not resample or unlock same-round Tendermint state, so the
protocol can carry the stale sample while Crosslink finality remains stuck at
the prior prefix.
*/
import CrosslinkComposed(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 2,
Proposer = Map(0 -> "p1", 1 -> "p2", 2 -> "p3"),
Stream = Map(0 -> "a1", 1 -> "a2", 2 -> "a2"),
Snapshots = Set("g", "a1", "a2", "a3", "b1", "b2", "b3"),
ResampleOnNilPrecommit = false,
MaxHeight = 3,
Sigma = 1,
InitialFinalized = "g",
HeightOf = Map(
"g" -> 0,
"a1" -> 1,
"a2" -> 2,
"a3" -> 3,
"b1" -> 1,
"b2" -> 2,
"b3" -> 3,
),
AncestorAt = Map(
"g" -> Map(0 -> "g", 1 -> "None", 2 -> "None", 3 -> "None"),
"a1" -> Map(0 -> "g", 1 -> "a1", 2 -> "None", 3 -> "None"),
"a2" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "None"),
"a3" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3"),
"b1" -> Map(0 -> "g", 1 -> "b1", 2 -> "None", 3 -> "None"),
"b2" -> Map(0 -> "g", 1 -> "b1", 2 -> "b2", 3 -> "None"),
"b3" -> Map(0 -> "g", 1 -> "b1", 2 -> "b2", 3 -> "b3"),
),
).* from "./CrosslinkComposed"
action composedUnchanged = all {
ProtocolUnchanged,
latestFinal' = latestFinal,
finalized' = finalized,
finalityAction' = finalityAction,
}
run baselineStreamChangeCarriesStaleSampleWithFinalityUnchangedTest = {
ComposedInit
.then(SeedValidNil("p1"))
.then(SeedValidNil("p2"))
.then(SeedFaultyNil("p4"))
.then(SeedSameRoundLock("p3"))
.then(AdvanceAfterPrecommitQuorum("p1"))
.then(AdvanceAfterPrecommitQuorum("p2"))
.then(AdvanceAfterPrecommitQuorum("p3"))
.then(Propose("p2"))
.then(all {
assert(msgsPropose.get(1).exists(m =>
m.src == "p2" and m.round == 1 and m.proposal == "a1"
)),
assert(latestFinal == "g"),
assert(finalized == Set("g")),
assert(ComposedSafety),
composedUnchanged,
})
}
run baselineStreamChangeBlocksFreshFinalityTest = {
ComposedInit
.then(SeedValidNil("p1"))
.then(SeedValidNil("p2"))
.then(SeedFaultyNil("p4"))
.then(SeedSameRoundLock("p3"))
.then(AdvanceAfterPrecommitQuorum("p1"))
.then(AdvanceAfterPrecommitQuorum("p2"))
.then(AdvanceAfterPrecommitQuorum("p3"))
.then(Propose("p2"))
.then(Prevote("p1", "a2"))
.fail()
}
}
module CrosslinkBaselineFinalityLivenessModel {
import CrosslinkComposed(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 2,
Proposer = Map(0 -> "p1", 1 -> "p2", 2 -> "p3"),
Stream = Map(0 -> "a2", 1 -> "a2", 2 -> "a2"),
Snapshots = Set("g", "a1", "a2", "a3", "b1", "b2", "b3"),
ResampleOnNilPrecommit = false,
MaxHeight = 3,
Sigma = 1,
InitialFinalized = "g",
HeightOf = Map(
"g" -> 0,
"a1" -> 1,
"a2" -> 2,
"a3" -> 3,
"b1" -> 1,
"b2" -> 2,
"b3" -> 3,
),
AncestorAt = Map(
"g" -> Map(0 -> "g", 1 -> "None", 2 -> "None", 3 -> "None"),
"a1" -> Map(0 -> "g", 1 -> "a1", 2 -> "None", 3 -> "None"),
"a2" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "None"),
"a3" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3"),
"b1" -> Map(0 -> "g", 1 -> "b1", 2 -> "None", 3 -> "None"),
"b2" -> Map(0 -> "g", 1 -> "b1", 2 -> "b2", 3 -> "None"),
"b3" -> Map(0 -> "g", 1 -> "b1", 2 -> "b2", 3 -> "b3"),
),
).* from "./CrosslinkComposed"
var phase: int
action LivenessInit = all {
ComposedInit,
phase' = 0,
}
action LivenessStep = any {
all { phase == 0, Propose("p1"), phase' = 1 },
all { phase == 1, Prevote("p1", "a2"), phase' = 2 },
all { phase == 2, Prevote("p2", "a2"), phase' = 3 },
all { phase == 3, Prevote("p3", "a2"), phase' = 4 },
all { phase == 4, PrecommitValue("p1", "a2"), phase' = 5 },
all { phase == 5, PrecommitValue("p2", "a2"), phase' = 6 },
all { phase == 6, PrecommitValue("p3", "a2"), phase' = 7 },
all { phase == 7, DecideValue("p1", 0, "a2"), phase' = 8 },
all { phase == 8, FinalizeDecision("p1", "a2", "a3"), phase' = 9 },
all { phase == 9, ComposedStutter, phase' = 9 },
}
val StableFinalityByEnd =
phase < 9 or latestFinal == "a2"
val LivenessSafety =
ComposedSafety and StableFinalityByEnd
}
// -*- mode: Bluespec; -*-
module CrosslinkBaselineModels {
/*
Small model instances mirroring the upstream Tendermint examples. These are
import-only handles for debugging and for keeping the baseline parameter
surface honest across different validator/fault layouts.
*/
pure val ValidSnapshots = Set("g", "a1", "a2", "b1", "b2")
pure val InvalidSnapshots = Set("bad")
pure val HeightBySnapshot = Map(
"g" -> 0,
"a1" -> 1,
"a2" -> 2,
"b1" -> 1,
"b2" -> 2,
"bad" -> 0,
)
pure val AncestorByHeight = Map(
"g" -> Map(0 -> "g", 1 -> "None", 2 -> "None"),
"a1" -> Map(0 -> "g", 1 -> "a1", 2 -> "None"),
"a2" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2"),
"b1" -> Map(0 -> "g", 1 -> "b1", 2 -> "None"),
"b2" -> Map(0 -> "g", 1 -> "b1", 2 -> "b2"),
"bad" -> Map(0 -> "bad", 1 -> "bad", 2 -> "bad"),
)
pure val StableBestTip = Map(
0 -> "a2",
1 -> "a2",
2 -> "a2",
3 -> "a2",
4 -> "a2",
)
pure val ForkingBestTip = Map(
0 -> "a2",
1 -> "b2",
2 -> "b2",
3 -> "b2",
4 -> "b2",
)
pure val ProposerSchedule = Map(
0 -> "p1",
1 -> "p2",
2 -> "p3",
3 -> "p4",
4 -> "p1",
)
import CrosslinkBaselineTenderlink(
BaselineCorr = Set("p1", "p2", "p3"),
BaselineFaulty = Set("p4"),
BaselineN = 4,
BaselineT = 1,
BaselineValidSnapshots = ValidSnapshots,
BaselineInvalidSnapshots = InvalidSnapshots,
BaselineMaxRound = 4,
BaselineProposer = ProposerSchedule,
BaselineSigma = 1,
BaselineBestTip = StableBestTip,
BaselineHeight = HeightBySnapshot,
BaselineAncestorAt = AncestorByHeight,
) as n4_f1_stable from "./CrosslinkBaselineTenderlink"
import CrosslinkBaselineTenderlink(
BaselineCorr = Set("p1", "p2", "p3"),
BaselineFaulty = Set("p4"),
BaselineN = 4,
BaselineT = 1,
BaselineValidSnapshots = ValidSnapshots,
BaselineInvalidSnapshots = InvalidSnapshots,
BaselineMaxRound = 4,
BaselineProposer = ProposerSchedule,
BaselineSigma = 1,
BaselineBestTip = ForkingBestTip,
BaselineHeight = HeightBySnapshot,
BaselineAncestorAt = AncestorByHeight,
) as n4_f1_forking from "./CrosslinkBaselineTenderlink"
import CrosslinkBaselineTenderlink(
BaselineCorr = Set("p1", "p2", "p3", "p4"),
BaselineFaulty = Set("p5"),
BaselineN = 5,
BaselineT = 1,
BaselineValidSnapshots = ValidSnapshots,
BaselineInvalidSnapshots = InvalidSnapshots,
BaselineMaxRound = 4,
BaselineProposer = ProposerSchedule,
BaselineSigma = 1,
BaselineBestTip = ForkingBestTip,
BaselineHeight = HeightBySnapshot,
BaselineAncestorAt = AncestorByHeight,
) as n5_f1_forking from "./CrosslinkBaselineTenderlink"
import CrosslinkBaselineTenderlink(
BaselineCorr = Set("p1", "p2"),
BaselineFaulty = Set("p3", "p4"),
BaselineN = 4,
BaselineT = 2,
BaselineValidSnapshots = ValidSnapshots,
BaselineInvalidSnapshots = InvalidSnapshots,
BaselineMaxRound = 4,
BaselineProposer = ProposerSchedule,
BaselineSigma = 1,
BaselineBestTip = ForkingBestTip,
BaselineHeight = HeightBySnapshot,
BaselineAncestorAt = AncestorByHeight,
) as n4_f2_forking from "./CrosslinkBaselineTenderlink"
import CrosslinkBaselineTenderlink(
BaselineCorr = Set("p1", "p2", "p3"),
BaselineFaulty = Set("p4", "p5"),
BaselineN = 5,
BaselineT = 2,
BaselineValidSnapshots = ValidSnapshots,
BaselineInvalidSnapshots = InvalidSnapshots,
BaselineMaxRound = 4,
BaselineProposer = ProposerSchedule,
BaselineSigma = 1,
BaselineBestTip = ForkingBestTip,
BaselineHeight = HeightBySnapshot,
BaselineAncestorAt = AncestorByHeight,
) as n5_f2_forking from "./CrosslinkBaselineTenderlink"
import CrosslinkBaselineTenderlink(
BaselineCorr = Set("p1", "p2", "p3", "p4", "p5"),
BaselineFaulty = Set("p6", "p7"),
BaselineN = 7,
BaselineT = 2,
BaselineValidSnapshots = ValidSnapshots,
BaselineInvalidSnapshots = InvalidSnapshots,
BaselineMaxRound = 4,
BaselineProposer = ProposerSchedule,
BaselineSigma = 1,
BaselineBestTip = ForkingBestTip,
BaselineHeight = HeightBySnapshot,
BaselineAncestorAt = AncestorByHeight,
) as n7_f2_forking from "./CrosslinkBaselineTenderlink"
}
// -*- mode: Bluespec; -*-
module CrosslinkBaselinePowSamplingModel {
/*
Baseline Crosslink samples a fixed-depth PoW ancestor: `head - sigma`.
The round machine in CrosslinkBaseline.qnt accepts `Stream(round)` as an
input. This model pins that stream to an explicit best-tip schedule and a
fixed sigma value, then checks the sticky baseline behavior when a fork switch
rolls back the previously sampled ancestor.
*/
import CrosslinkResamplingTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 2,
Proposer = Map(0 -> "p1", 1 -> "p2", 2 -> "p3"),
Stream = Map(0 -> "a3", 1 -> "b3", 2 -> "b3"),
Snapshots = Set("g", "a1", "a2", "a3", "a4", "b3", "b4"),
ResampleOnNilPrecommit = false,
ExpectedRound1Proposal = "a3",
).* from "./CrosslinkResampling"
type Height_t = int
pure val FixedSigma = 1
pure val MaxHeight = 4
pure val Heights = 0.to(MaxHeight)
pure val BestTipByRound = Map(
0 -> "a4",
1 -> "b4",
2 -> "b4",
)
pure val HeightOf = Map(
"g" -> 0,
"a1" -> 1,
"a2" -> 2,
"a3" -> 3,
"a4" -> 4,
"b3" -> 3,
"b4" -> 4,
)
pure val AncestorAt = Map(
"g" -> Map(0 -> "g", 1 -> "None", 2 -> "None", 3 -> "None", 4 -> "None"),
"a1" -> Map(0 -> "g", 1 -> "a1", 2 -> "None", 3 -> "None", 4 -> "None"),
"a2" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "None", 4 -> "None"),
"a3" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3", 4 -> "None"),
"a4" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3", 4 -> "a4"),
"b3" -> Map(0 -> "g", 1 -> "a1", 2 -> "b2", 3 -> "b3", 4 -> "None"),
"b4" -> Map(0 -> "g", 1 -> "a1", 2 -> "b2", 3 -> "b3", 4 -> "b4"),
)
pure val LcaHeight = Map(
"a4" -> Map("a4" -> 4, "b4" -> 2),
"b4" -> Map("a4" -> 2, "b4" -> 4),
)
pure def BestTipAt(r: Round_t): Snapshot_t =
BestTipByRound.get(r)
pure def Height(v: Snapshot_t): Height_t =
HeightOf.get(v)
pure def AncestorAtHeight(v: Snapshot_t, h: Height_t): Snapshot_t =
AncestorAt.get(v).get(h)
pure def HeadMinusFixedSigma(r: Round_t): Snapshot_t =
AncestorAtHeight(BestTipAt(r), Height(BestTipAt(r)) - FixedSigma)
pure def RollbackDepth(previousTip: Snapshot_t, nextTip: Snapshot_t): int =
Height(previousTip) - LcaHeight.get(previousTip).get(nextTip)
pure def FixedSampleSurvivesRollback(previousTip: Snapshot_t, nextTip: Snapshot_t): bool =
RollbackDepth(previousTip, nextTip) < FixedSigma
val StreamMatchesFixedHeadMinusSigma =
Rounds.forall(r => Stream.get(r) == HeadMinusFixedSigma(r))
val BaselinePowSamplingSafety =
Safety and StreamMatchesFixedHeadMinusSigma
run fixedSigmaStreamDerivesFromBestTipAncestorsTest = all {
assert(HeadMinusFixedSigma(0) == "a3"),
assert(HeadMinusFixedSigma(1) == "b3"),
assert(StreamMatchesFixedHeadMinusSigma),
}
run forkSwitchRollsBackFixedSigmaSampleTest = all {
assert(RollbackDepth(BestTipAt(0), BestTipAt(1)) == 2),
assert(not(FixedSampleSurvivesRollback(BestTipAt(0), BestTipAt(1)))),
assert(HeadMinusFixedSigma(0) != HeadMinusFixedSigma(1)),
}
run stickyBaselineCarriesRolledBackHeadMinusSigmaTest = {
Init
.then(SeedAbandonedRoundState("p1"))
.then(SeedAbandonedRoundState("p2"))
.then(SeedAbandonedRoundState("p3"))
.then(StartNextRoundAfterPrecommitQuorum("p2"))
.then(InsertProposal("p2"))
.then(all {
assert(msgsPropose.get(1).exists(m =>
m.src == "p2" and m.round == 1 and m.proposal == HeadMinusFixedSigma(0)
)),
assert(HeadMinusFixedSigma(0) == "a3"),
assert(HeadMinusFixedSigma(1) == "b3"),
assert(not(FixedSampleSurvivesRollback(BestTipAt(0), BestTipAt(1)))),
assert(BaselinePowSamplingSafety),
unchangedAll,
})
}
run stickyBaselineCannotPrevoteFreshForkSampleTest = {
Init
.then(SeedValidNilPrecommitState("p1"))
.then(SeedValidNilPrecommitState("p2"))
.then(SeedFaultyNilPrecommit("p4"))
.then(SeedSameRoundValueLock("p3"))
.then(StartNextRoundAfterPrecommitQuorum("p1"))
.then(StartNextRoundAfterPrecommitQuorum("p2"))
.then(StartNextRoundAfterPrecommitQuorum("p3"))
.then(InsertProposal("p2"))
.then(UponProposalPrevote("p1", HeadMinusFixedSigma(1)))
.fail()
}
}
module CrosslinkBaselinePowLongReorgModel {
/*
A long-reorg fixed-sigma fixture. The best tip switches from `a5` to `c5`
with common ancestor `a2`, so the rollback depth is 3 while sigma is 2. The
sticky baseline still carries the old `head - sigma` sample after the fork
switch.
*/
import CrosslinkResamplingTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 2,
Proposer = Map(0 -> "p1", 1 -> "p2", 2 -> "p3"),
Stream = Map(0 -> "a3", 1 -> "c3", 2 -> "c3"),
Snapshots = Set("g", "a1", "a2", "a3", "a4", "a5", "c3", "c4", "c5"),
ResampleOnNilPrecommit = false,
ExpectedRound1Proposal = "a3",
).* from "./CrosslinkResampling"
type Height_t = int
pure val FixedSigma = 2
pure val MaxHeight = 5
pure val Heights = 0.to(MaxHeight)
pure val BestTipByRound = Map(
0 -> "a5",
1 -> "c5",
2 -> "c5",
)
pure val HeightOf = Map(
"g" -> 0,
"a1" -> 1,
"a2" -> 2,
"a3" -> 3,
"a4" -> 4,
"a5" -> 5,
"c3" -> 3,
"c4" -> 4,
"c5" -> 5,
)
pure val AncestorAt = Map(
"g" -> Map(0 -> "g", 1 -> "None", 2 -> "None", 3 -> "None", 4 -> "None", 5 -> "None"),
"a1" -> Map(0 -> "g", 1 -> "a1", 2 -> "None", 3 -> "None", 4 -> "None", 5 -> "None"),
"a2" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "None", 4 -> "None", 5 -> "None"),
"a3" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3", 4 -> "None", 5 -> "None"),
"a4" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3", 4 -> "a4", 5 -> "None"),
"a5" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3", 4 -> "a4", 5 -> "a5"),
"c3" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "c3", 4 -> "None", 5 -> "None"),
"c4" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "c3", 4 -> "c4", 5 -> "None"),
"c5" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "c3", 4 -> "c4", 5 -> "c5"),
)
pure val LcaHeight = Map(
"a5" -> Map("a5" -> 5, "c5" -> 2),
"c5" -> Map("a5" -> 2, "c5" -> 5),
)
pure def BestTipAt(r: Round_t): Snapshot_t =
BestTipByRound.get(r)
pure def Height(v: Snapshot_t): Height_t =
HeightOf.get(v)
pure def AncestorAtHeight(v: Snapshot_t, h: Height_t): Snapshot_t =
AncestorAt.get(v).get(h)
pure def HeadMinusFixedSigma(r: Round_t): Snapshot_t =
AncestorAtHeight(BestTipAt(r), Height(BestTipAt(r)) - FixedSigma)
pure def RollbackDepth(previousTip: Snapshot_t, nextTip: Snapshot_t): int =
Height(previousTip) - LcaHeight.get(previousTip).get(nextTip)
pure def FixedSampleSurvivesRollback(previousTip: Snapshot_t, nextTip: Snapshot_t): bool =
RollbackDepth(previousTip, nextTip) < FixedSigma
val StreamMatchesFixedHeadMinusSigma =
Rounds.forall(r => Stream.get(r) == HeadMinusFixedSigma(r))
val BaselinePowLongReorgSafety =
Safety and StreamMatchesFixedHeadMinusSigma
run longReorgExceedsFixedSigmaTest = all {
assert(HeadMinusFixedSigma(0) == "a3"),
assert(HeadMinusFixedSigma(1) == "c3"),
assert(RollbackDepth(BestTipAt(0), BestTipAt(1)) == 3),
assert(FixedSigma == 2),
assert(not(FixedSampleSurvivesRollback(BestTipAt(0), BestTipAt(1)))),
assert(StreamMatchesFixedHeadMinusSigma),
}
run stickyBaselineCarriesLongReorgHeadMinusSigmaTest = {
Init
.then(SeedAbandonedRoundState("p1"))
.then(SeedAbandonedRoundState("p2"))
.then(SeedAbandonedRoundState("p3"))
.then(StartNextRoundAfterPrecommitQuorum("p2"))
.then(InsertProposal("p2"))
.then(all {
assert(msgsPropose.get(1).exists(m =>
m.src == "p2" and m.round == 1 and m.proposal == HeadMinusFixedSigma(0)
)),
assert(HeadMinusFixedSigma(0) == "a3"),
assert(HeadMinusFixedSigma(1) == "c3"),
assert(not(FixedSampleSurvivesRollback(BestTipAt(0), BestTipAt(1)))),
assert(BaselinePowLongReorgSafety),
unchangedAll,
})
}
run stickyBaselineCannotPrevoteFreshLongReorgSampleTest = {
Init
.then(SeedValidNilPrecommitState("p1"))
.then(SeedValidNilPrecommitState("p2"))
.then(SeedFaultyNilPrecommit("p4"))
.then(SeedSameRoundValueLock("p3"))
.then(StartNextRoundAfterPrecommitQuorum("p1"))
.then(StartNextRoundAfterPrecommitQuorum("p2"))
.then(StartNextRoundAfterPrecommitQuorum("p3"))
.then(InsertProposal("p2"))
.then(UponProposalPrevote("p1", HeadMinusFixedSigma(1)))
.fail()
}
}
module CrosslinkBaselinePowGeneratedScheduleModel {
/*
A bounded generated PoW schedule for the fixed-sigma baseline. Published work
first selects the honest `a` branch, then an adversarially released `b4`
branch outworks it. The baseline stream is derived from the generated best
tips instead of being treated as a standalone hand-authored map.
*/
import CrosslinkResamplingTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 2,
Proposer = Map(0 -> "p1", 1 -> "p2", 2 -> "p3"),
Stream = Map(0 -> "a2", 1 -> "a3", 2 -> "b3"),
Snapshots = Set("g", "a1", "a2", "a3", "a4", "b3", "b4"),
ResampleOnNilPrecommit = false,
ExpectedRound1Proposal = "a2",
).* from "./CrosslinkResampling"
type Height_t = int
type Work_t = int
pure val FixedSigma = 1
pure val MaxHeight = 4
pure val MaxWork = 5
pure val BestTipByRound = Map(
0 -> "a3",
1 -> "a4",
2 -> "b4",
)
pure val PublishedAt = Map(
0 -> Map(
"g" -> true,
"a1" -> true,
"a2" -> true,
"a3" -> true,
"a4" -> false,
"b3" -> false,
"b4" -> false,
),
1 -> Map(
"g" -> true,
"a1" -> true,
"a2" -> true,
"a3" -> true,
"a4" -> true,
"b3" -> false,
"b4" -> false,
),
2 -> Map(
"g" -> true,
"a1" -> true,
"a2" -> true,
"a3" -> true,
"a4" -> true,
"b3" -> true,
"b4" -> true,
),
)
pure val HonestWorkAt = Map(
0 -> Map("g" -> 0, "a1" -> 1, "a2" -> 2, "a3" -> 3, "a4" -> 0, "b3" -> 0, "b4" -> 0),
1 -> Map("g" -> 0, "a1" -> 1, "a2" -> 2, "a3" -> 3, "a4" -> 4, "b3" -> 0, "b4" -> 0),
2 -> Map("g" -> 0, "a1" -> 1, "a2" -> 2, "a3" -> 3, "a4" -> 4, "b3" -> 0, "b4" -> 0),
)
pure val AdversarialWorkAt = Map(
0 -> Map("g" -> 0, "a1" -> 0, "a2" -> 0, "a3" -> 0, "a4" -> 0, "b3" -> 4, "b4" -> 4),
1 -> Map("g" -> 0, "a1" -> 0, "a2" -> 0, "a3" -> 0, "a4" -> 0, "b3" -> 4, "b4" -> 4),
2 -> Map("g" -> 0, "a1" -> 0, "a2" -> 0, "a3" -> 0, "a4" -> 0, "b3" -> 5, "b4" -> 5),
)
pure val TieBreakRank = Map(
"g" -> 0,
"a1" -> 1,
"a2" -> 2,
"a3" -> 3,
"a4" -> 4,
"b3" -> 5,
"b4" -> 6,
)
pure val HeightOf = Map(
"g" -> 0,
"a1" -> 1,
"a2" -> 2,
"a3" -> 3,
"a4" -> 4,
"b3" -> 3,
"b4" -> 4,
)
pure val AncestorAt = Map(
"g" -> Map(0 -> "g", 1 -> "None", 2 -> "None", 3 -> "None", 4 -> "None"),
"a1" -> Map(0 -> "g", 1 -> "a1", 2 -> "None", 3 -> "None", 4 -> "None"),
"a2" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "None", 4 -> "None"),
"a3" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3", 4 -> "None"),
"a4" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3", 4 -> "a4"),
"b3" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "b3", 4 -> "None"),
"b4" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "b3", 4 -> "b4"),
)
pure val LcaHeight = Map(
"a3" -> Map("a3" -> 3, "a4" -> 3, "b4" -> 2),
"a4" -> Map("a3" -> 3, "a4" -> 4, "b4" -> 2),
"b4" -> Map("a3" -> 2, "a4" -> 2, "b4" -> 4),
)
pure def Height(v: Snapshot_t): Height_t =
HeightOf.get(v)
pure def TotalWorkAt(r: Round_t, v: Snapshot_t): Work_t =
HonestWorkAt.get(r).get(v) + AdversarialWorkAt.get(r).get(v)
pure def CandidateAt(r: Round_t, v: Snapshot_t): bool =
Snapshots.contains(v) and PublishedAt.get(r).get(v)
pure def BetterOrEqualAt(r: Round_t, a: Snapshot_t, b: Snapshot_t): bool =
TotalWorkAt(r, a) > TotalWorkAt(r, b) or
(
TotalWorkAt(r, a) == TotalWorkAt(r, b) and
TieBreakRank.get(a) >= TieBreakRank.get(b)
)
pure def IsBestTipAt(r: Round_t, v: Snapshot_t): bool =
CandidateAt(r, v) and
Snapshots.forall(w =>
not(CandidateAt(r, w)) or BetterOrEqualAt(r, v, w)
)
pure def BestTipAt(r: Round_t): Snapshot_t =
BestTipByRound.get(r)
pure def AncestorAtHeight(v: Snapshot_t, h: Height_t): Snapshot_t =
AncestorAt.get(v).get(h)
pure def HeadMinusFixedSigma(r: Round_t): Snapshot_t =
AncestorAtHeight(BestTipAt(r), Height(BestTipAt(r)) - FixedSigma)
pure def RollbackDepth(previousTip: Snapshot_t, nextTip: Snapshot_t): int =
Height(previousTip) - LcaHeight.get(previousTip).get(nextTip)
pure def SampleSurvivesRollback(previousTip: Snapshot_t, nextTip: Snapshot_t): bool =
RollbackDepth(previousTip, nextTip) < FixedSigma
val GeneratedBestTipsMatchCompetition =
Rounds.forall(r => IsBestTipAt(r, BestTipAt(r)))
val StreamMatchesGeneratedHeadMinusSigma =
Rounds.forall(r => Stream.get(r) == HeadMinusFixedSigma(r))
val BaselinePowGeneratedScheduleSafety =
Safety and
GeneratedBestTipsMatchCompetition and
StreamMatchesGeneratedHeadMinusSigma
action SeedGeneratedRound1Abandonment(p: Proc_t): bool = {
val r = 1
val stale = Stream.get(r)
all {
r.in(Rounds),
p.in(Corr),
round' = round.set(p, r),
step' = step.set(p, "precommit"),
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal.set(p, stale),
cachedProposalRound' = cachedProposalRound.set(p, r),
BroadcastPrecommit(p, r, NilSnapshot),
firedAction' = "SeedGeneratedRound1Abandonment",
decision' = decision,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
}
}
run generatedScheduleSelectsHonestThenAdversarialTipsTest = all {
assert(IsBestTipAt(0, "a3")),
assert(IsBestTipAt(1, "a4")),
assert(not(CandidateAt(1, "b4"))),
assert(IsBestTipAt(2, "b4")),
assert(TotalWorkAt(2, "b4") > TotalWorkAt(2, "a4")),
assert(RollbackDepth(BestTipAt(1), BestTipAt(2)) == 2),
assert(not(SampleSurvivesRollback(BestTipAt(1), BestTipAt(2)))),
assert(GeneratedBestTipsMatchCompetition),
}
run generatedScheduleDerivesBaselineStreamTest = all {
assert(HeadMinusFixedSigma(0) == "a2"),
assert(HeadMinusFixedSigma(1) == "a3"),
assert(HeadMinusFixedSigma(2) == "b3"),
assert(StreamMatchesGeneratedHeadMinusSigma),
}
run stickyBaselineCarriesGeneratedAdversarialForkSampleTest = {
Init
.then(SeedGeneratedRound1Abandonment("p1"))
.then(SeedGeneratedRound1Abandonment("p2"))
.then(SeedGeneratedRound1Abandonment("p3"))
.then(StartNextRoundAfterPrecommitQuorum("p3"))
.then(InsertProposal("p3"))
.then(all {
assert(msgsPropose.get(2).exists(m =>
m.src == "p3" and m.round == 2 and m.proposal == HeadMinusFixedSigma(1)
)),
assert(HeadMinusFixedSigma(1) == "a3"),
assert(HeadMinusFixedSigma(2) == "b3"),
assert(not(SampleSurvivesRollback(BestTipAt(1), BestTipAt(2)))),
assert(BaselinePowGeneratedScheduleSafety),
unchangedAll,
})
}
}
module CrosslinkBaselinePowRepeatedGeneratedScheduleModel {
/*
A repeated generated PoW schedule. Published work first selects the honest
`a` branch, then adversarially released `b4`, then adversarially released
`c4`. This keeps repeated stream changes in the baseline proof surface while
still deriving the selected tips from bounded work competition.
*/
import CrosslinkResamplingTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 3,
Proposer = Map(0 -> "p1", 1 -> "p2", 2 -> "p3", 3 -> "p1"),
Stream = Map(0 -> "a2", 1 -> "a3", 2 -> "b3", 3 -> "c3"),
Snapshots = Set("g", "a1", "a2", "a3", "a4", "b3", "b4", "c3", "c4"),
ResampleOnNilPrecommit = false,
ExpectedRound1Proposal = "a2",
).* from "./CrosslinkResampling"
type Height_t = int
type Work_t = int
pure val FixedSigma = 1
pure val BestTipByRound = Map(
0 -> "a3",
1 -> "a4",
2 -> "b4",
3 -> "c4",
)
pure val HeightOf = Map(
"g" -> 0,
"a1" -> 1,
"a2" -> 2,
"a3" -> 3,
"a4" -> 4,
"b3" -> 3,
"b4" -> 4,
"c3" -> 3,
"c4" -> 4,
)
pure val AncestorAt = Map(
"g" -> Map(0 -> "g", 1 -> "None", 2 -> "None", 3 -> "None", 4 -> "None"),
"a1" -> Map(0 -> "g", 1 -> "a1", 2 -> "None", 3 -> "None", 4 -> "None"),
"a2" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "None", 4 -> "None"),
"a3" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3", 4 -> "None"),
"a4" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3", 4 -> "a4"),
"b3" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "b3", 4 -> "None"),
"b4" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "b3", 4 -> "b4"),
"c3" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "c3", 4 -> "None"),
"c4" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "c3", 4 -> "c4"),
)
pure val TieBreakRank = Map(
"g" -> 0,
"a1" -> 1,
"a2" -> 2,
"a3" -> 3,
"a4" -> 4,
"b3" -> 5,
"b4" -> 6,
"c3" -> 7,
"c4" -> 8,
)
pure def Height(v: Snapshot_t): Height_t =
HeightOf.get(v)
pure def PublishedAt(r: Round_t, v: Snapshot_t): bool =
if (r == 0) {
Set("g", "a1", "a2", "a3").contains(v)
} else if (r == 1) {
Set("g", "a1", "a2", "a3", "a4").contains(v)
} else if (r == 2) {
Set("g", "a1", "a2", "a3", "a4", "b3", "b4").contains(v)
} else {
Set("g", "a1", "a2", "a3", "a4", "b3", "b4", "c3", "c4").contains(v)
}
pure def HonestWorkAt(r: Round_t, v: Snapshot_t): Work_t =
if (v == "a4" and r >= 1) {
4
} else if (v == "a3") {
3
} else if (v == "a2") {
2
} else if (v == "a1") {
1
} else {
0
}
pure def AdversarialWorkAt(r: Round_t, v: Snapshot_t): Work_t =
if (v.in(Set("c3", "c4")) and r >= 3) {
6
} else if (v.in(Set("b3", "b4")) and r >= 2) {
5
} else {
0
}
pure def TotalWorkAt(r: Round_t, v: Snapshot_t): Work_t =
HonestWorkAt(r, v) + AdversarialWorkAt(r, v)
pure def CandidateAt(r: Round_t, v: Snapshot_t): bool =
Snapshots.contains(v) and PublishedAt(r, v)
pure def BetterOrEqualAt(r: Round_t, a: Snapshot_t, b: Snapshot_t): bool =
TotalWorkAt(r, a) > TotalWorkAt(r, b) or
(
TotalWorkAt(r, a) == TotalWorkAt(r, b) and
TieBreakRank.get(a) >= TieBreakRank.get(b)
)
pure def IsBestTipAt(r: Round_t, v: Snapshot_t): bool =
CandidateAt(r, v) and
Snapshots.forall(w =>
not(CandidateAt(r, w)) or BetterOrEqualAt(r, v, w)
)
pure def BestTipAt(r: Round_t): Snapshot_t =
BestTipByRound.get(r)
pure def AncestorAtHeight(v: Snapshot_t, h: Height_t): Snapshot_t =
AncestorAt.get(v).get(h)
pure def HeadMinusFixedSigma(r: Round_t): Snapshot_t =
AncestorAtHeight(BestTipAt(r), Height(BestTipAt(r)) - FixedSigma)
pure def CommonAncestorHeight(previousTip: Snapshot_t, nextTip: Snapshot_t): Height_t =
if (previousTip == nextTip) {
Height(previousTip)
} else if (previousTip == "a3" and nextTip == "a4") {
3
} else {
2
}
pure def RollbackDepth(previousTip: Snapshot_t, nextTip: Snapshot_t): int =
Height(previousTip) - CommonAncestorHeight(previousTip, nextTip)
pure def SampleSurvivesRollback(previousTip: Snapshot_t, nextTip: Snapshot_t): bool =
RollbackDepth(previousTip, nextTip) < FixedSigma
val GeneratedBestTipsMatchRepeatedCompetition =
Rounds.forall(r => IsBestTipAt(r, BestTipAt(r)))
val StreamMatchesRepeatedGeneratedHeadMinusSigma =
Rounds.forall(r => Stream.get(r) == HeadMinusFixedSigma(r))
val BaselinePowRepeatedGeneratedScheduleSafety =
Safety and
GeneratedBestTipsMatchRepeatedCompetition and
StreamMatchesRepeatedGeneratedHeadMinusSigma
action SeedGeneratedRoundAbandonment(p: Proc_t, r: Round_t): bool = {
val stale = Stream.get(r)
all {
r.in(Rounds),
p.in(Corr),
round' = round.set(p, r),
step' = step.set(p, "precommit"),
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal.set(p, stale),
cachedProposalRound' = cachedProposalRound.set(p, r),
BroadcastPrecommit(p, r, NilSnapshot),
firedAction' = "SeedGeneratedRoundAbandonment",
decision' = decision,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
}
}
run repeatedGeneratedScheduleSelectsAdversarialTipsTest = all {
assert(IsBestTipAt(0, "a3")),
assert(IsBestTipAt(1, "a4")),
assert(not(CandidateAt(1, "b4"))),
assert(IsBestTipAt(2, "b4")),
assert(not(CandidateAt(2, "c4"))),
assert(IsBestTipAt(3, "c4")),
assert(RollbackDepth(BestTipAt(1), BestTipAt(2)) == 2),
assert(RollbackDepth(BestTipAt(2), BestTipAt(3)) == 2),
assert(not(SampleSurvivesRollback(BestTipAt(1), BestTipAt(2)))),
assert(not(SampleSurvivesRollback(BestTipAt(2), BestTipAt(3)))),
assert(GeneratedBestTipsMatchRepeatedCompetition),
}
run repeatedGeneratedScheduleDerivesBaselineStreamTest = all {
assert(HeadMinusFixedSigma(0) == "a2"),
assert(HeadMinusFixedSigma(1) == "a3"),
assert(HeadMinusFixedSigma(2) == "b3"),
assert(HeadMinusFixedSigma(3) == "c3"),
assert(StreamMatchesRepeatedGeneratedHeadMinusSigma),
}
run stickyBaselineCarriesRepeatedGeneratedForkSamplesTest = {
Init
.then(SeedGeneratedRoundAbandonment("p1", 1))
.then(SeedGeneratedRoundAbandonment("p2", 1))
.then(SeedGeneratedRoundAbandonment("p3", 1))
.then(StartNextRoundAfterPrecommitQuorum("p3"))
.then(InsertProposal("p3"))
.then(SeedGeneratedRoundAbandonment("p1", 2))
.then(SeedGeneratedRoundAbandonment("p2", 2))
.then(SeedGeneratedRoundAbandonment("p3", 2))
.then(StartNextRoundAfterPrecommitQuorum("p1"))
.then(InsertProposal("p1"))
.then(all {
assert(msgsPropose.get(2).exists(m =>
m.src == "p3" and m.round == 2 and m.proposal == HeadMinusFixedSigma(1)
)),
assert(msgsPropose.get(3).exists(m =>
m.src == "p1" and m.round == 3 and m.proposal == HeadMinusFixedSigma(2)
)),
assert(HeadMinusFixedSigma(1) == "a3"),
assert(HeadMinusFixedSigma(2) == "b3"),
assert(HeadMinusFixedSigma(3) == "c3"),
assert(BaselinePowRepeatedGeneratedScheduleSafety),
unchangedAll,
})
}
}
module CrosslinkBaselinePowStochasticProductionModel {
/*
A finite stochastic-production abstraction for the fixed-sigma baseline. This
does not assign probabilities. Instead, it buckets observed hash-power
participation, hidden-work risk, and block-time variance into production
outcomes, then derives the visible best-tip schedule from those outcomes and
bounded work competition.
The witness keeps the baseline rule unchanged: even when stochastic
production risk releases a heavier fork between rounds, the sticky baseline
can carry the prior `head - sigma` sample into the next round.
*/
import CrosslinkResamplingTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 3,
Proposer = Map(0 -> "p1", 1 -> "p2", 2 -> "p3", 3 -> "p1"),
Stream = Map(0 -> "a2", 1 -> "a3", 2 -> "b3", 3 -> "c3"),
Snapshots = Set("g", "a1", "a2", "a3", "a4", "b3", "b4", "c3", "c4"),
ResampleOnNilPrecommit = false,
ExpectedRound1Proposal = "a2",
).* from "./CrosslinkResampling"
type Height_t = int
type Work_t = int
type Pct_t = int
pure val FixedSigma = 1
pure val TargetHashParticipationPct = 75
pure val CriticalHashParticipationPct = 60
pure val RaisedRiskThreshold = 60
pure val CriticalRiskThreshold = 100
pure val HeightOf = Map(
"g" -> 0,
"a1" -> 1,
"a2" -> 2,
"a3" -> 3,
"a4" -> 4,
"b3" -> 3,
"b4" -> 4,
"c3" -> 3,
"c4" -> 4,
)
pure val AncestorAt = Map(
"g" -> Map(0 -> "g", 1 -> "None", 2 -> "None", 3 -> "None", 4 -> "None"),
"a1" -> Map(0 -> "g", 1 -> "a1", 2 -> "None", 3 -> "None", 4 -> "None"),
"a2" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "None", 4 -> "None"),
"a3" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3", 4 -> "None"),
"a4" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3", 4 -> "a4"),
"b3" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "b3", 4 -> "None"),
"b4" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "b3", 4 -> "b4"),
"c3" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "c3", 4 -> "None"),
"c4" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "c3", 4 -> "c4"),
)
pure val TieBreakRank = Map(
"g" -> 0,
"a1" -> 1,
"a2" -> 2,
"a3" -> 3,
"a4" -> 4,
"b3" -> 5,
"b4" -> 6,
"c3" -> 7,
"c4" -> 8,
)
pure val ObservedHashParticipationPct = Map(
0 -> 92,
1 -> 86,
2 -> 58,
3 -> 52,
)
pure val HiddenWorkRiskPct = Map(
0 -> 0,
1 -> 5,
2 -> 45,
3 -> 55,
)
pure val ObservedBlockVariancePct = Map(
0 -> 5,
1 -> 8,
2 -> 30,
3 -> 35,
)
pure def Height(v: Snapshot_t): Height_t =
HeightOf.get(v)
pure def HashCoverageGapPct(r: Round_t): Pct_t =
100 - ObservedHashParticipationPct.get(r)
pure def StochasticProductionRiskScore(r: Round_t): int =
HashCoverageGapPct(r) +
HiddenWorkRiskPct.get(r) +
ObservedBlockVariancePct.get(r)
pure def StochasticRiskStatusAt(r: Round_t): str =
if (StochasticProductionRiskScore(r) >= CriticalRiskThreshold) {
"critical-stochastic-risk"
} else if (StochasticProductionRiskScore(r) >= RaisedRiskThreshold) {
"elevated-stochastic-risk"
} else {
"low-stochastic-risk"
}
pure def HashParticipationStatusAt(r: Round_t): str =
if (ObservedHashParticipationPct.get(r) < CriticalHashParticipationPct) {
"critical-hash-participation"
} else if (ObservedHashParticipationPct.get(r) < TargetHashParticipationPct) {
"degraded-hash-participation"
} else {
"healthy-hash-participation"
}
pure def ProductionOutcomeAt(r: Round_t): str =
if (r >= 3 and StochasticRiskStatusAt(r) == "critical-stochastic-risk") {
"repeated-hidden-work-release"
} else if (r >= 2 and StochasticRiskStatusAt(r) != "low-stochastic-risk") {
"hidden-work-release"
} else {
"honest-visible-extension"
}
pure def PublishedAt(r: Round_t, v: Snapshot_t): bool =
if (v.in(Set("g", "a1", "a2", "a3"))) {
true
} else if (v == "a4") {
r >= 1
} else if (v.in(Set("b3", "b4"))) {
ProductionOutcomeAt(r).in(Set("hidden-work-release", "repeated-hidden-work-release"))
} else if (v.in(Set("c3", "c4"))) {
ProductionOutcomeAt(r) == "repeated-hidden-work-release"
} else {
false
}
pure def HonestWorkAt(r: Round_t, v: Snapshot_t): Work_t =
if (v == "a4" and r >= 1) {
4
} else if (v == "a3") {
3
} else if (v == "a2") {
2
} else if (v == "a1") {
1
} else {
0
}
pure def AdversarialWorkAt(r: Round_t, v: Snapshot_t): Work_t =
if (v.in(Set("c3", "c4")) and ProductionOutcomeAt(r) == "repeated-hidden-work-release") {
6
} else if (
v.in(Set("b3", "b4")) and
ProductionOutcomeAt(r).in(Set("hidden-work-release", "repeated-hidden-work-release"))
) {
5
} else {
0
}
pure def TotalWorkAt(r: Round_t, v: Snapshot_t): Work_t =
HonestWorkAt(r, v) + AdversarialWorkAt(r, v)
pure def CandidateAt(r: Round_t, v: Snapshot_t): bool =
Snapshots.contains(v) and PublishedAt(r, v)
pure def BetterOrEqualAt(r: Round_t, a: Snapshot_t, b: Snapshot_t): bool =
TotalWorkAt(r, a) > TotalWorkAt(r, b) or
(
TotalWorkAt(r, a) == TotalWorkAt(r, b) and
TieBreakRank.get(a) >= TieBreakRank.get(b)
)
pure def IsBestTipAt(r: Round_t, v: Snapshot_t): bool =
CandidateAt(r, v) and
Snapshots.forall(w =>
not(CandidateAt(r, w)) or BetterOrEqualAt(r, v, w)
)
pure def BestTipAt(r: Round_t): Snapshot_t =
if (ProductionOutcomeAt(r) == "repeated-hidden-work-release") {
"c4"
} else if (ProductionOutcomeAt(r) == "hidden-work-release") {
"b4"
} else if (r >= 1) {
"a4"
} else {
"a3"
}
pure def AncestorAtHeight(v: Snapshot_t, h: Height_t): Snapshot_t =
AncestorAt.get(v).get(h)
pure def HeadMinusFixedSigma(r: Round_t): Snapshot_t =
AncestorAtHeight(BestTipAt(r), Height(BestTipAt(r)) - FixedSigma)
pure def CommonAncestorHeight(previousTip: Snapshot_t, nextTip: Snapshot_t): Height_t =
if (previousTip == nextTip) {
Height(previousTip)
} else if (previousTip == "a3" and nextTip == "a4") {
3
} else {
2
}
pure def RollbackDepth(previousTip: Snapshot_t, nextTip: Snapshot_t): int =
Height(previousTip) - CommonAncestorHeight(previousTip, nextTip)
pure def SampleSurvivesRollback(previousTip: Snapshot_t, nextTip: Snapshot_t): bool =
RollbackDepth(previousTip, nextTip) < FixedSigma
val StochasticProductionScheduleWellFormed =
Rounds.forall(r =>
HashParticipationStatusAt(r).in(Set(
"healthy-hash-participation",
"degraded-hash-participation",
"critical-hash-participation",
)) and
StochasticRiskStatusAt(r).in(Set(
"low-stochastic-risk",
"elevated-stochastic-risk",
"critical-stochastic-risk",
)) and
IsBestTipAt(r, BestTipAt(r))
)
val StreamMatchesStochasticHeadMinusSigma =
Rounds.forall(r => Stream.get(r) == HeadMinusFixedSigma(r))
val BaselinePowStochasticProductionSafety =
Safety and
StochasticProductionScheduleWellFormed and
StreamMatchesStochasticHeadMinusSigma
action SeedStochasticRoundAbandonment(p: Proc_t, r: Round_t): bool = {
val stale = Stream.get(r)
all {
r.in(Rounds),
p.in(Corr),
round' = round.set(p, r),
step' = step.set(p, "precommit"),
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal.set(p, stale),
cachedProposalRound' = cachedProposalRound.set(p, r),
BroadcastPrecommit(p, r, NilSnapshot),
firedAction' = "SeedStochasticRoundAbandonment",
decision' = decision,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
}
}
run stochasticProductionClassifiesCoverageRiskTest = all {
assert(HashParticipationStatusAt(0) == "healthy-hash-participation"),
assert(HashParticipationStatusAt(1) == "healthy-hash-participation"),
assert(HashParticipationStatusAt(2) == "critical-hash-participation"),
assert(HashParticipationStatusAt(3) == "critical-hash-participation"),
assert(StochasticRiskStatusAt(0) == "low-stochastic-risk"),
assert(StochasticRiskStatusAt(1) == "low-stochastic-risk"),
assert(StochasticRiskStatusAt(2) == "critical-stochastic-risk"),
assert(StochasticRiskStatusAt(3) == "critical-stochastic-risk"),
assert(ProductionOutcomeAt(2) == "hidden-work-release"),
assert(ProductionOutcomeAt(3) == "repeated-hidden-work-release"),
assert(StochasticProductionScheduleWellFormed),
}
run stochasticProductionSelectsTipsFromRiskBucketsTest = all {
assert(BestTipAt(0) == "a3"),
assert(BestTipAt(1) == "a4"),
assert(BestTipAt(2) == "b4"),
assert(BestTipAt(3) == "c4"),
assert(not(CandidateAt(1, "b4"))),
assert(IsBestTipAt(2, "b4")),
assert(IsBestTipAt(3, "c4")),
assert(TotalWorkAt(2, "b4") > TotalWorkAt(2, "a4")),
assert(TotalWorkAt(3, "c4") > TotalWorkAt(3, "b4")),
assert(RollbackDepth(BestTipAt(1), BestTipAt(2)) == 2),
assert(RollbackDepth(BestTipAt(2), BestTipAt(3)) == 2),
assert(not(SampleSurvivesRollback(BestTipAt(1), BestTipAt(2)))),
assert(not(SampleSurvivesRollback(BestTipAt(2), BestTipAt(3)))),
}
run stochasticProductionDerivesBaselineStreamTest = all {
assert(HeadMinusFixedSigma(0) == "a2"),
assert(HeadMinusFixedSigma(1) == "a3"),
assert(HeadMinusFixedSigma(2) == "b3"),
assert(HeadMinusFixedSigma(3) == "c3"),
assert(StreamMatchesStochasticHeadMinusSigma),
}
run stickyBaselineCarriesStochasticForkSamplesTest = {
Init
.then(SeedStochasticRoundAbandonment("p1", 1))
.then(SeedStochasticRoundAbandonment("p2", 1))
.then(SeedStochasticRoundAbandonment("p3", 1))
.then(StartNextRoundAfterPrecommitQuorum("p3"))
.then(InsertProposal("p3"))
.then(SeedStochasticRoundAbandonment("p1", 2))
.then(SeedStochasticRoundAbandonment("p2", 2))
.then(SeedStochasticRoundAbandonment("p3", 2))
.then(StartNextRoundAfterPrecommitQuorum("p1"))
.then(InsertProposal("p1"))
.then(all {
assert(msgsPropose.get(2).exists(m =>
m.src == "p3" and m.round == 2 and m.proposal == HeadMinusFixedSigma(1)
)),
assert(msgsPropose.get(3).exists(m =>
m.src == "p1" and m.round == 3 and m.proposal == HeadMinusFixedSigma(2)
)),
assert(HeadMinusFixedSigma(1) == "a3"),
assert(HeadMinusFixedSigma(2) == "b3"),
assert(HeadMinusFixedSigma(3) == "c3"),
assert(BaselinePowStochasticProductionSafety),
unchangedAll,
})
}
}
// -*- mode: Bluespec; -*-
module CrosslinkBaselineParameterizedShellTest {
/*
Tests for the upstream-shaped parameterized baseline shell itself. These keep
the shell honest as it grows beyond import-only model handles.
*/
import CrosslinkBaselineModels.* from "./CrosslinkBaselineModels"
import CrosslinkBaselineTenderlink(
BaselineCorr = Set("p1", "p2", "p3"),
BaselineFaulty = Set("p4"),
BaselineN = 4,
BaselineT = 1,
BaselineValidSnapshots = ValidSnapshots,
BaselineInvalidSnapshots = InvalidSnapshots,
BaselineMaxRound = 4,
BaselineProposer = ProposerSchedule,
BaselineSigma = 1,
BaselineBestTip = ForkingBestTip,
BaselineHeight = HeightBySnapshot,
BaselineAncestorAt = AncestorByHeight,
).* from "./CrosslinkBaselineTenderlink"
run parameterizedShellFaultyInitCoversEvidenceTest = {
BaselineInitWithFaultyEvidence.then(all {
assert(BaselineFaultyInitDomainWellFormed),
assert(BaselineEvidenceCoversObservedMessages),
assert(BaselineFaultyInitSafety),
BaselineUnchangedAll,
})
}
run parameterizedShellFaultyInitAllowsCorrectProposalTest = {
BaselineInitWithFaultyEvidence
.then(BaselineInsertProposal("p1"))
.then(all {
assert(BaselineHasProposal(0, "a1")),
assert(BaselineEvidenceCoversObservedMessages),
assert(BaselineFaultyInitSafety),
BaselineUnchangedAll,
})
}
run parameterizedShellFaultyInitNextPreservesSafetyTest = {
BaselineInitWithFaultyEvidence
.then(BaselineNext)
.then(all {
assert(BaselineFaultyInitSafety),
BaselineUnchangedAll,
})
}
run parameterizedShellStartRoundAdvancesToProposeTest = {
BaselineInitWithFaultyEvidence
.then(BaselineStartRound("p1", 1))
.then(all {
assert(BaselineRoundOf("p1") == 1),
assert(BaselineStepOf("p1") == "propose"),
assert(BaselineFaultyInitSafety),
BaselineUnchangedAll,
})
}
run parameterizedShellTransitionAliasesDriveDecisionPathTest = {
BaselineInitWithFaultyEvidence
.then(BaselineInsertProposal("p1"))
.then(BaselineUponProposalInPropose("p1", "a1"))
.then(BaselineUponProposalInPropose("p2", "a1"))
.then(BaselineUponProposalInPropose("p3", "a1"))
.then(BaselineUponValuePrevoteQuorum("p1", "a1"))
.then(BaselineUponValuePrevoteQuorum("p2", "a1"))
.then(BaselineUponValuePrevoteQuorum("p3", "a1"))
.then(BaselineDecide("p1", 0, "a1"))
.then(all {
assert(BaselineDecisionOf("p1") == "a1"),
assert(BaselinePrevoteQuorum(0, "a1")),
assert(BaselinePrecommitQuorum(0, "a1")),
assert(BaselineFaultyInitSafety),
BaselineUnchangedAll,
})
}
run parameterizedShellStreamChangeAliasPrecommitsNilTest = {
BaselineInitWithFaultyEvidence
.then(BaselineInsertProposal("p1"))
.then(BaselineUponProposalPrevote("p1", "a1"))
.then(BaselineUponStreamChangePrecommitNil("p1"))
.then(all {
assert(BaselineHasPrevoteFrom("p1", 0)),
assert(BaselineHasPrecommitFrom("p1", 0)),
assert(not(BaselineNilPrecommitCert(0))),
assert(BaselineFaultyInitSafety),
BaselineUnchangedAll,
})
}
run parameterizedShellNilTimeoutAliasesStartNextRoundTest = {
BaselineInitWithFaultyEvidence
.then(BaselineTimeoutProposePrevoteNil("p1"))
.then(BaselineTimeoutProposePrevoteNil("p2"))
.then(BaselineTimeoutProposePrevoteNil("p3"))
.then(BaselineUponNilPrevoteQuorum("p1"))
.then(BaselineUponNilPrevoteQuorum("p2"))
.then(BaselineUponNilPrevoteQuorum("p3"))
.then(BaselineStartNextRoundAfterPrecommitQuorum("p1"))
.then(all {
assert(BaselinePrevoteQuorum(0, BaselineNilSnapshot)),
assert(BaselineNilPrecommitCert(0)),
assert(BaselineRoundOf("p1") == 1),
assert(BaselineStepOf("p1") == "propose"),
assert(BaselineFaultyInitSafety),
BaselineUnchangedAll,
})
}
run parameterizedShellLateNilCertificateAliasStaysDisabledTest = {
BaselineInitWithFaultyEvidence
.then(BaselineTimeoutProposePrevoteNil("p1"))
.then(BaselineTimeoutProposePrevoteNil("p2"))
.then(BaselineTimeoutProposePrevoteNil("p3"))
.then(BaselineUponNilPrevoteQuorum("p1"))
.then(BaselineUponNilPrevoteQuorum("p2"))
.then(BaselineUponNilPrevoteQuorum("p3"))
.then(BaselineStartNextRoundAfterPrecommitQuorum("p1"))
.then(all {
assert(BaselineNilPrecommitCert(0)),
assert(BaselineRoundOf("p1") == 1),
assert(BaselineFaultyInitSafety),
BaselineUnchangedAll,
})
.then(BaselineApplyLateNilPrecommitCertificate("p1", 0))
.fail()
}
run parameterizedShellTimeoutPrecommitAndCatchupAliasesTest = {
BaselineInitWithFaultyEvidence
.then(BaselineTimeoutProposePrevoteNil("p1"))
.then(BaselineTimeoutPrevotePrecommitNil("p1"))
.then(BaselineTimeoutPrecommitStartNextRound("p1"))
.then(BaselineStartRound("p2", 2))
.then(BaselineTimeoutProposePrevoteNil("p2"))
.then(BaselineStartRound("p3", 2))
.then(BaselineTimeoutProposePrevoteNil("p3"))
.then(BaselineCatchUpToRound("p1", 2))
.then(all {
assert(BaselineHasPrevoteFrom("p2", 2)),
assert(BaselineHasPrevoteFrom("p3", 2)),
assert(BaselineRoundCatchupEvidence(2)),
assert(BaselineRoundOf("p1") == 2),
assert(BaselineStepOf("p1") == "propose"),
assert(BaselineFaultyInitSafety),
BaselineUnchangedAll,
})
}
run parameterizedShellConcreteValidRoundAliasAcceptsPrevoteQuorumTest = {
BaselineInitWithFaultyEvidence
.then(BaselineInsertProposal("p1"))
.then(BaselineUponProposalInPropose("p1", "a1"))
.then(BaselineUponProposalInPropose("p2", "a1"))
.then(BaselineUponProposalInPropose("p3", "a1"))
.then(BaselineUponValuePrevoteQuorum("p2", "a1"))
.then(BaselineTimeoutPrevotePrecommitNil("p1"))
.then(BaselineTimeoutPrecommitStartNextRound("p1"))
.then(BaselineTimeoutPrecommitStartNextRound("p2"))
.then(BaselineInsertProposal("p2"))
.then(BaselineUponProposalInProposeAndPrevote("p1", "a1", 0))
.then(all {
assert(BaselinePrevoteQuorum(0, "a1")),
assert(BaselineValidValueOf("p2") == "a1"),
assert(BaselineValidRoundOf("p2") == 0),
assert(BaselineLockedValueOf("p2") == "a1"),
assert(BaselineLockedRoundOf("p2") == 0),
assert(BaselineHasPrevoteFrom("p1", 1)),
assert(BaselineFaultyInitSafety),
BaselineUnchangedAll,
})
}
}
module CrosslinkBaselineN4F1StableTest {
/*
Upstream-style smoke tests for the parameterized baseline shell. This mirrors
the upstream Tendermint test shape while checking Crosslink's fixed
`head - sigma` value selection.
*/
import CrosslinkBaselineModels.* from "./CrosslinkBaselineModels"
pure val TestRounds = 0.to(4)
pure def TestHeadMinusSigma(r: int): str =
AncestorByHeight
.get(StableBestTip.get(r))
.get(HeightBySnapshot.get(StableBestTip.get(r)) - 1)
pure val TestStream: int -> str =
TestRounds.mapBy(r => TestHeadMinusSigma(r))
pure val TestStreamMatchesFixedSigma =
TestRounds.forall(r => TestStream.get(r) == TestHeadMinusSigma(r))
import CrosslinkResamplingTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 4,
Proposer = ProposerSchedule,
Stream = TestStream,
Snapshots = ValidSnapshots,
ResampleOnNilPrecommit = false,
ExpectedRound1Proposal = TestStream.get(0),
).* from "./CrosslinkResampling"
val BaselineN4F1StableSafety =
Safety and TestStreamMatchesFixedSigma
run fixedSigmaSampleTest = all {
assert(TestHeadMinusSigma(0) == "a1"),
assert(TestHeadMinusSigma(1) == "a1"),
assert(TestStreamMatchesFixedSigma),
}
run decisionTest = {
Init
.then(InsertProposal("p1"))
.then(UponProposalPrevote("p1", "a1"))
.then(UponProposalPrevote("p2", "a1"))
.then(UponProposalPrevote("p3", "a1"))
.then(UponValuePrevoteQuorum("p1", "a1"))
.then(UponValuePrevoteQuorum("p2", "a1"))
.then(UponValuePrevoteQuorum("p3", "a1"))
.then(Decide("p1", 0, "a1"))
.then(all {
assert(decision.get("p1") == "a1"),
assert(BaselineN4F1StableSafety),
unchangedAll,
})
}
run noProposeTwiceTest = {
Init
.then(InsertProposal("p1"))
.then(InsertProposal("p1"))
.fail()
}
run nilPrevoteQuorumPrecommitsNilTest = {
Init
.then(SeedCorrectNilPrevoteState("p1", 0))
.then(SeedCorrectNilPrevoteState("p2", 0))
.then(SeedCorrectNilPrevoteState("p3", 0))
.then(UponNilPrevoteQuorum("p1"))
.then(UponNilPrevoteQuorum("p2"))
.then(UponNilPrevoteQuorum("p3"))
.then(all {
assert(PrevoteQuorum(0, NilSnapshot)),
assert(NilPrecommitCert(0)),
assert(BaselineN4F1StableSafety),
unchangedAll,
})
}
run timeoutPrevotePathFormsNilPrecommitCertTest = {
Init
.then(TimeoutProposePrevoteNil("p1"))
.then(TimeoutProposePrevoteNil("p2"))
.then(TimeoutProposePrevoteNil("p3"))
.then(TimeoutPrevotePrecommitNil("p1"))
.then(TimeoutPrevotePrecommitNil("p2"))
.then(TimeoutPrevotePrecommitNil("p3"))
.then(all {
assert(PrevoteQuorum(0, NilSnapshot)),
assert(NilPrecommitCert(0)),
assert(BaselineN4F1StableSafety),
unchangedAll,
})
}
run timeoutPrecommitAdvancesWithoutPrecommitQuorumTest = {
Init
.then(TimeoutProposePrevoteNil("p1"))
.then(TimeoutPrevotePrecommitNil("p1"))
.then(TimeoutPrecommitStartNextRound("p1"))
.then(all {
assert(round.get("p1") == 1),
assert(step.get("p1") == "propose"),
assert(not(NilPrecommitCert(0))),
assert(BaselineN4F1StableSafety),
unchangedAll,
})
}
run roundCatchupStartsFutureRoundTest = {
Init
.then(SeedCorrectNilPrevoteState("p2", 2))
.then(SeedCorrectNilPrevoteState("p3", 2))
.then(CatchUpToRound("p1", 2))
.then(all {
assert(RoundCatchupEvidence(2)),
assert(round.get("p1") == 2),
assert(step.get("p1") == "propose"),
assert(BaselineN4F1StableSafety),
unchangedAll,
})
}
run validRoundProposalWithoutPrevoteQuorumIsRejectedTest = {
Init
.then(SeedValidNilPrecommitState("p1"))
.then(SeedValidNilPrecommitState("p2"))
.then(SeedValidNilPrecommitState("p3"))
.then(StartNextRoundAfterPrecommitQuorum("p1"))
.then(StartNextRoundAfterPrecommitQuorum("p2"))
.then(StartNextRoundAfterPrecommitQuorum("p3"))
.then(InsertProposal("p2"))
.then(UponProposalPrevote("p1", "a1"))
.fail()
}
run validRoundProposalWithPrevoteQuorumIsAcceptedTest = {
Init
.then(InsertProposal("p1"))
.then(UponProposalPrevote("p1", "a1"))
.then(UponProposalPrevote("p2", "a1"))
.then(UponProposalPrevote("p3", "a1"))
.then(UponValuePrevoteQuorum("p2", "a1"))
.then(TimeoutPrevotePrecommitNil("p1"))
.then(TimeoutPrecommitStartNextRound("p1"))
.then(TimeoutPrecommitStartNextRound("p2"))
.then(InsertProposal("p2"))
.then(UponProposalPrevote("p1", "a1"))
.then(all {
assert(PrevoteQuorum(0, "a1")),
assert(msgsPropose.get(1).exists(m =>
m.src == "p2" and
m.round == 1 and
m.proposal == "a1" and
m.validRound == 0
)),
assert(msgsPrevote.get(1).exists(m =>
m.src == "p1" and m.round == 1 and m.id == "a1"
)),
assert(BaselineN4F1StableSafety),
unchangedAll,
})
}
run nilValidRoundProposalHandlerPrevotesTest = {
Init
.then(InsertProposal("p1"))
.then(UponProposalInPropose("p1", "a1"))
.then(all {
assert(msgsPrevote.get(0).exists(m =>
m.src == "p1" and m.round == 0 and m.id == "a1"
)),
assert(BaselineN4F1StableSafety),
unchangedAll,
})
}
run validRoundProposalHandlerWithoutPrevoteQuorumIsRejectedTest = {
Init
.then(SeedValidNilPrecommitState("p1"))
.then(SeedValidNilPrecommitState("p2"))
.then(SeedValidNilPrecommitState("p3"))
.then(StartNextRoundAfterPrecommitQuorum("p1"))
.then(StartNextRoundAfterPrecommitQuorum("p2"))
.then(StartNextRoundAfterPrecommitQuorum("p3"))
.then(InsertProposal("p2"))
.then(UponProposalInProposeAndPrevote("p1", "a1", 0))
.fail()
}
run validRoundProposalHandlerWithPrevoteQuorumIsAcceptedTest = {
Init
.then(InsertProposal("p1"))
.then(UponProposalInPropose("p1", "a1"))
.then(UponProposalInPropose("p2", "a1"))
.then(UponProposalInPropose("p3", "a1"))
.then(UponValuePrevoteQuorum("p2", "a1"))
.then(TimeoutPrevotePrecommitNil("p1"))
.then(TimeoutPrecommitStartNextRound("p1"))
.then(TimeoutPrecommitStartNextRound("p2"))
.then(InsertProposal("p2"))
.then(UponProposalInProposeAndPrevote("p1", "a1", 0))
.then(all {
assert(PrevoteQuorum(0, "a1")),
assert(msgsPrevote.get(1).exists(m =>
m.src == "p1" and m.round == 1 and m.id == "a1"
)),
assert(BaselineN4F1StableSafety),
unchangedAll,
})
}
run correctValuePrevotesRequireJustifiedProposalTest = {
Init
.then(InsertProposal("p1"))
.then(UponProposalPrevote("p1", "a1"))
.then(all {
assert(CorrectValuePrevotesHaveJustifiedProposal),
assert(BaselineN4F1StableSafety),
unchangedAll,
})
}
}
module CrosslinkBaselineN4F1ForkingTest {
/*
A small stream-change instance for the upstream-shaped baseline shell. It
keeps the baseline sticky behavior explicit: a nil-precommit round advance
still carries the round-0 fixed-sigma sample.
*/
import CrosslinkBaselineModels.* from "./CrosslinkBaselineModels"
pure val TestRounds = 0.to(4)
pure def TestHeadMinusSigma(r: int): str =
AncestorByHeight
.get(ForkingBestTip.get(r))
.get(HeightBySnapshot.get(ForkingBestTip.get(r)) - 1)
pure val TestStream: int -> str =
TestRounds.mapBy(r => TestHeadMinusSigma(r))
pure val TestStreamMatchesFixedSigma =
TestRounds.forall(r => TestStream.get(r) == TestHeadMinusSigma(r))
import CrosslinkResamplingTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 4,
Proposer = ProposerSchedule,
Stream = TestStream,
Snapshots = ValidSnapshots,
ResampleOnNilPrecommit = false,
ExpectedRound1Proposal = TestStream.get(0),
).* from "./CrosslinkResampling"
val BaselineN4F1ForkingSafety =
Safety and TestStreamMatchesFixedSigma
val NoStaleFixedSigmaProposal =
Rounds.forall(r =>
msgsPropose.get(r).forall(m => m.proposal == TestHeadMinusSigma(r))
)
run streamChangeDerivesFreshHeadMinusSigmaTest = all {
assert(TestHeadMinusSigma(0) == "a1"),
assert(TestHeadMinusSigma(1) == "b1"),
assert(TestStreamMatchesFixedSigma),
}
run stickyBaselineCarriesStaleFixedSigmaSampleTest = {
Init
.then(SeedAbandonedRoundState("p1"))
.then(SeedAbandonedRoundState("p2"))
.then(SeedAbandonedRoundState("p3"))
.then(StartNextRoundAfterPrecommitQuorum("p2"))
.then(InsertProposal("p2"))
.then(all {
assert(msgsPropose.get(1).exists(m =>
m.src == "p2" and
m.round == 1 and
m.proposal == TestHeadMinusSigma(0)
)),
assert(TestHeadMinusSigma(0) != TestHeadMinusSigma(1)),
assert(BaselineN4F1ForkingSafety),
unchangedAll,
})
}
run falseNoStaleFixedSigmaProposalInvariantFailsTest = {
Init
.then(SeedAbandonedRoundState("p1"))
.then(SeedAbandonedRoundState("p2"))
.then(SeedAbandonedRoundState("p3"))
.then(StartNextRoundAfterPrecommitQuorum("p2"))
.then(InsertProposal("p2"))
.then(all {
NoStaleFixedSigmaProposal,
unchangedAll,
})
.fail()
}
}
module CrosslinkBaselineN4F2ForkingTest {
/*
An above-live-boundary f=2 instance. With N=4 and T=2, the quorum size is
larger than the validator set, so this witness records the boundary instead
of pretending the instance has a normal decision path.
*/
import CrosslinkBaselineModels.* from "./CrosslinkBaselineModels"
pure val TestRounds = 0.to(4)
pure def TestHeadMinusSigma(r: int): str =
AncestorByHeight
.get(ForkingBestTip.get(r))
.get(HeightBySnapshot.get(ForkingBestTip.get(r)) - 1)
pure val TestStream: int -> str =
TestRounds.mapBy(r => TestHeadMinusSigma(r))
pure val TestStreamMatchesFixedSigma =
TestRounds.forall(r => TestStream.get(r) == TestHeadMinusSigma(r))
import CrosslinkResampling(
Corr = Set("p1", "p2"),
Faulty = Set("p3", "p4"),
N = 4,
T = 2,
MaxRound = 4,
Proposer = ProposerSchedule,
Stream = TestStream,
Snapshots = ValidSnapshots,
ResampleOnNilPrecommit = false,
).* from "./CrosslinkResampling"
action unchangedAll = all {
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
firedAction' = firedAction,
}
val CorrectCanMakeCatchupEvidence =
size(Corr) >= THRESHOLD1
val CorrectCanMakeValueQuorum =
size(Corr) >= THRESHOLD2
val BaselineN4F2ForkingSafety =
Safety and TestStreamMatchesFixedSigma
run n4F2DocumentsAboveLiveFaultBoundaryTest = {
Init.then(all {
assert(THRESHOLD1 == 3),
assert(THRESHOLD2 == 5),
assert(not(CorrectCanMakeCatchupEvidence)),
assert(not(CorrectCanMakeValueQuorum)),
assert(BaselineN4F2ForkingSafety),
unchangedAll,
})
}
}
module CrosslinkBaselineN5F2ForkingTest {
/*
A second above-live-boundary f=2 instance. Correct validators can produce
f+1 future-round activity, but they still cannot produce a 2f+1 value quorum.
*/
import CrosslinkBaselineModels.* from "./CrosslinkBaselineModels"
pure val TestRounds = 0.to(4)
pure def TestHeadMinusSigma(r: int): str =
AncestorByHeight
.get(ForkingBestTip.get(r))
.get(HeightBySnapshot.get(ForkingBestTip.get(r)) - 1)
pure val TestStream: int -> str =
TestRounds.mapBy(r => TestHeadMinusSigma(r))
pure val TestStreamMatchesFixedSigma =
TestRounds.forall(r => TestStream.get(r) == TestHeadMinusSigma(r))
import CrosslinkResampling(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4", "p5"),
N = 5,
T = 2,
MaxRound = 4,
Proposer = ProposerSchedule,
Stream = TestStream,
Snapshots = ValidSnapshots,
ResampleOnNilPrecommit = false,
).* from "./CrosslinkResampling"
action unchangedAll = all {
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
firedAction' = firedAction,
}
val CorrectCanMakeCatchupEvidence =
size(Corr) >= THRESHOLD1
val CorrectCanMakeValueQuorum =
size(Corr) >= THRESHOLD2
val BaselineN5F2ForkingSafety =
Safety and TestStreamMatchesFixedSigma
run n5F2CatchupEvidenceButNoCorrectValueQuorumTest = {
Init
.then(SeedCorrectNilPrevoteState("p1", 2))
.then(SeedCorrectNilPrevoteState("p2", 2))
.then(SeedCorrectNilPrevoteState("p3", 2))
.then(all {
assert(THRESHOLD1 == 3),
assert(THRESHOLD2 == 5),
assert(CorrectCanMakeCatchupEvidence),
assert(RoundCatchupEvidence(2)),
assert(not(CorrectCanMakeValueQuorum)),
assert(BaselineN5F2ForkingSafety),
unchangedAll,
})
}
}
module CrosslinkBaselineN7F2ForkingTest {
/*
A proper f=2 BFT-boundary instance. With N=7 and five correct validators,
the baseline shell still has the ordinary fixed-sigma decision path.
*/
import CrosslinkBaselineModels.* from "./CrosslinkBaselineModels"
pure val TestRounds = 0.to(4)
pure def TestHeadMinusSigma(r: int): str =
AncestorByHeight
.get(ForkingBestTip.get(r))
.get(HeightBySnapshot.get(ForkingBestTip.get(r)) - 1)
pure val TestStream: int -> str =
TestRounds.mapBy(r => TestHeadMinusSigma(r))
pure val TestStreamMatchesFixedSigma =
TestRounds.forall(r => TestStream.get(r) == TestHeadMinusSigma(r))
import CrosslinkResampling(
Corr = Set("p1", "p2", "p3", "p4", "p5"),
Faulty = Set("p6", "p7"),
N = 7,
T = 2,
MaxRound = 4,
Proposer = ProposerSchedule,
Stream = TestStream,
Snapshots = ValidSnapshots,
ResampleOnNilPrecommit = false,
).* from "./CrosslinkResampling"
action unchangedAll = all {
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
firedAction' = firedAction,
}
val CorrectCanMakeCatchupEvidence =
size(Corr) >= THRESHOLD1
val CorrectCanMakeValueQuorum =
size(Corr) >= THRESHOLD2
val BaselineN7F2ForkingSafety =
Safety and TestStreamMatchesFixedSigma
run n7F2DecisionPathTest = {
Init
.then(InsertProposal("p1"))
.then(UponProposalPrevote("p1", "a1"))
.then(UponProposalPrevote("p2", "a1"))
.then(UponProposalPrevote("p3", "a1"))
.then(UponProposalPrevote("p4", "a1"))
.then(UponProposalPrevote("p5", "a1"))
.then(UponValuePrevoteQuorum("p1", "a1"))
.then(UponValuePrevoteQuorum("p2", "a1"))
.then(UponValuePrevoteQuorum("p3", "a1"))
.then(UponValuePrevoteQuorum("p4", "a1"))
.then(UponValuePrevoteQuorum("p5", "a1"))
.then(Decide("p1", 0, "a1"))
.then(all {
assert(THRESHOLD1 == 3),
assert(THRESHOLD2 == 5),
assert(CorrectCanMakeCatchupEvidence),
assert(CorrectCanMakeValueQuorum),
assert(decision.get("p1") == "a1"),
assert(BaselineN7F2ForkingSafety),
unchangedAll,
})
}
}
// -*- mode: Bluespec; -*-
module CrosslinkBftHeights {
/*
A bounded model for Crosslink finality across successive BFT heights.
Earlier composed witnesses finalize a single Tenderlink decision. This model
adds the height dimension: consecutive BFT decisions can update Crosslink
finality directly, while the finalized PoW prefix remains linear.
*/
type ConsensusHeight_t = int
type PowHeight_t = int
type Snapshot_t = str
type Block_t = str
const MaxConsensusHeight: ConsensusHeight_t
const MaxPowHeight: PowHeight_t
const Sigma: int
const Snapshots: Set[Snapshot_t]
const InitialFinalized: Snapshot_t
const ScheduledDecision: ConsensusHeight_t -> Snapshot_t
const ScheduledTip: ConsensusHeight_t -> Snapshot_t
const HeightOf: Snapshot_t -> PowHeight_t
const AncestorAt: Snapshot_t -> PowHeight_t -> Block_t
pure val ConsensusHeights = 0.to(MaxConsensusHeight)
pure val PowHeights = 0.to(MaxPowHeight)
assume max_consensus_height_non_negative =
MaxConsensusHeight >= 0
assume max_pow_height_non_negative =
MaxPowHeight >= 0
assume sigma_is_positive =
Sigma >= 1
assume initial_finalized_is_snapshot =
Snapshots.contains(InitialFinalized)
assume scheduled_values_are_snapshots =
ConsensusHeights.forall(h =>
h == 0 or
(
Snapshots.contains(ScheduledDecision.get(h)) and
Snapshots.contains(ScheduledTip.get(h))
)
)
var consensusHeight: ConsensusHeight_t
var latestFinal: Snapshot_t
var finalized: Set[Snapshot_t]
var heightAction: str
pure def IsSnapshot(v: Snapshot_t): bool =
Snapshots.contains(v)
pure def Height(v: Snapshot_t): PowHeight_t =
HeightOf.get(v)
pure def BlockAt(v: Snapshot_t, h: PowHeight_t): Block_t =
AncestorAt.get(v).get(h)
def Extends(newer: Snapshot_t, older: Snapshot_t): bool =
IsSnapshot(newer) and
IsSnapshot(older) and
Height(newer) >= Height(older) and
PowHeights.forall(h =>
h > Height(older) or BlockAt(newer, h) == BlockAt(older, h)
)
def Agrees(a: Snapshot_t, b: Snapshot_t): bool =
Extends(a, b) or Extends(b, a)
def TailConfirms(tip: Snapshot_t, candidate: Snapshot_t): bool =
IsSnapshot(tip) and
IsSnapshot(candidate) and
Height(tip) >= Height(candidate) + Sigma and
BlockAt(tip, Height(candidate)) == BlockAt(candidate, Height(candidate))
def ValidHeightDecision(candidate: Snapshot_t, tip: Snapshot_t): bool =
Extends(candidate, latestFinal) and
Height(candidate) > Height(latestFinal) and
TailConfirms(tip, candidate)
action Init = all {
consensusHeight' = 0,
latestFinal' = InitialFinalized,
finalized' = Set(InitialFinalized),
heightAction' = "Init",
}
action ApplyDecisionAt(
nextHeight: ConsensusHeight_t,
candidate: Snapshot_t,
tip: Snapshot_t
): bool = all {
nextHeight == consensusHeight + 1,
nextHeight.in(ConsensusHeights),
ValidHeightDecision(candidate, tip),
consensusHeight' = nextHeight,
latestFinal' = candidate,
finalized' = finalized.union(Set(candidate)),
heightAction' = "ApplyDecisionAt",
}
action ApplyScheduledDecision = {
val nextHeight = consensusHeight + 1
ApplyDecisionAt(
nextHeight,
ScheduledDecision.get(nextHeight),
ScheduledTip.get(nextHeight)
)
}
action Stutter = all {
consensusHeight' = consensusHeight,
latestFinal' = latestFinal,
finalized' = finalized,
heightAction' = heightAction,
}
action Next =
any {
ApplyScheduledDecision,
Stutter,
}
val ConsensusHeightIsBounded =
0 <= consensusHeight and consensusHeight <= MaxConsensusHeight
val FinalizedPrefixLinear =
tuples(finalized, finalized).forall(((a, b)) =>
Agrees(a, b)
)
val LatestFinalExtendsAllFinalized =
finalized.forall(v => Extends(latestFinal, v))
val InitialFinalizedRemainsFinalized =
finalized.contains(InitialFinalized)
val Safety =
ConsensusHeightIsBounded and
FinalizedPrefixLinear and
LatestFinalExtendsAllFinalized and
InitialFinalizedRemainsFinalized
}
module CrosslinkBftHeightsTest {
import CrosslinkBftHeights.* from "./CrosslinkBftHeights"
export CrosslinkBftHeights.*
action unchangedAll = all {
consensusHeight' = consensusHeight,
latestFinal' = latestFinal,
finalized' = finalized,
heightAction' = heightAction,
}
run successiveBftDecisionsAdvanceCrosslinkFinalityTest = {
Init
.then(ApplyScheduledDecision)
.then(all {
assert(consensusHeight == 1),
assert(latestFinal == "a2"),
assert(finalized.contains("a2")),
assert(Safety),
Stutter,
})
.then(ApplyScheduledDecision)
.then(all {
assert(consensusHeight == 2),
assert(latestFinal == "a3"),
assert(finalized.contains("a3")),
assert(Safety),
Stutter,
})
}
run rejectsForkDecisionAfterPrefixFinalityTest = {
Init
.then(ApplyDecisionAt(1, "a2", "a3"))
.then(ApplyDecisionAt(2, "b3", "b4"))
.fail()
}
run rejectsSkippingConsensusHeightTest = {
Init
.then(ApplyDecisionAt(2, "a3", "a4"))
.fail()
}
}
module CrosslinkBftHeightsModel {
import CrosslinkBftHeightsTest(
MaxConsensusHeight = 2,
MaxPowHeight = 4,
Sigma = 1,
Snapshots = Set("g", "a1", "a2", "a3", "a4", "b2", "b3", "b4"),
InitialFinalized = "g",
ScheduledDecision = Map(
0 -> "g",
1 -> "a2",
2 -> "a3",
3 -> "a4",
),
ScheduledTip = Map(
0 -> "g",
1 -> "a3",
2 -> "a4",
3 -> "a4",
),
HeightOf = Map(
"g" -> 0,
"a1" -> 1,
"a2" -> 2,
"a3" -> 3,
"a4" -> 4,
"b2" -> 2,
"b3" -> 3,
"b4" -> 4,
),
AncestorAt = Map(
"g" -> Map(0 -> "g", 1 -> "None", 2 -> "None", 3 -> "None", 4 -> "None"),
"a1" -> Map(0 -> "g", 1 -> "a1", 2 -> "None", 3 -> "None", 4 -> "None"),
"a2" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "None", 4 -> "None"),
"a3" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3", 4 -> "None"),
"a4" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3", 4 -> "a4"),
"b2" -> Map(0 -> "g", 1 -> "a1", 2 -> "b2", 3 -> "None", 4 -> "None"),
"b3" -> Map(0 -> "g", 1 -> "a1", 2 -> "b2", 3 -> "b3", 4 -> "None"),
"b4" -> Map(0 -> "g", 1 -> "a1", 2 -> "b2", 3 -> "b3", 4 -> "b4"),
),
).*
}
module CrosslinkComposed {
/*
Composition layer for the focused Crosslink models.
CrosslinkResampling models Tenderlink's moving-value round recovery.
This module adds the finality-prefix state and checks the end-to-end flow:
a nil-precommit certificate triggers resampling, the next round decides a
fresh PoW snapshot, and that decision advances Crosslink finality without
violating the finalized-prefix rules.
*/
import CrosslinkResampling.* from "./CrosslinkResampling"
export CrosslinkResampling.*
type Height_t = int
type Block_t = str
const MaxHeight: Height_t
const Sigma: int
const InitialFinalized: Snapshot_t
const HeightOf: Snapshot_t -> Height_t
const AncestorAt: Snapshot_t -> Height_t -> Block_t
assume initial_finalized_is_snapshot =
Snapshots.contains(InitialFinalized)
assume sigma_is_positive =
Sigma >= 1
pure val Heights = 0.to(MaxHeight)
var latestFinal: Snapshot_t
var finalized: Set[Snapshot_t]
var finalityAction: str
pure def IsSnapshot(v: Snapshot_t): bool =
Snapshots.contains(v)
pure def Height(v: Snapshot_t): Height_t =
HeightOf.get(v)
pure def BlockAt(v: Snapshot_t, h: Height_t): Block_t =
AncestorAt.get(v).get(h)
def Extends(newer: Snapshot_t, older: Snapshot_t): bool =
IsSnapshot(newer) and
IsSnapshot(older) and
Height(newer) >= Height(older) and
Heights.forall(h =>
h > Height(older) or BlockAt(newer, h) == BlockAt(older, h)
)
def Agrees(a: Snapshot_t, b: Snapshot_t): bool =
Extends(a, b) or Extends(b, a)
def TailConfirms(tip: Snapshot_t, candidate: Snapshot_t): bool =
IsSnapshot(tip) and
IsSnapshot(candidate) and
Height(tip) >= Height(candidate) + Sigma and
BlockAt(tip, Height(candidate)) == BlockAt(candidate, Height(candidate))
def ValidFinalityCandidate(candidate: Snapshot_t, tip: Snapshot_t): bool =
Extends(candidate, latestFinal) and
Height(candidate) > Height(latestFinal) and
TailConfirms(tip, candidate)
action ComposedInit = all {
Init,
latestFinal' = InitialFinalized,
finalized' = Set(InitialFinalized),
finalityAction' = "ComposedInit",
}
action FinalityUnchanged = all {
latestFinal' = latestFinal,
finalized' = finalized,
finalityAction' = finalityAction,
}
action ProtocolUnchanged = all {
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
firedAction' = firedAction,
}
action SeedValidNil(p: Proc_t): bool =
all { SeedValidNilPrecommitState(p), FinalityUnchanged }
action SeedFaultyNil(p: Proc_t): bool =
all { SeedFaultyNilPrecommit(p), FinalityUnchanged }
action SeedSameRoundLock(p: Proc_t): bool =
all { SeedSameRoundValueLock(p), FinalityUnchanged }
action AdvanceAfterPrecommitQuorum(p: Proc_t): bool =
all { StartNextRoundAfterPrecommitQuorum(p), FinalityUnchanged }
action Propose(p: Proc_t): bool =
all { InsertProposal(p), FinalityUnchanged }
action Prevote(p: Proc_t, v: Snapshot_t): bool =
all { UponProposalPrevote(p, v), FinalityUnchanged }
action PrecommitValue(p: Proc_t, v: Snapshot_t): bool =
all { UponValuePrevoteQuorum(p, v), FinalityUnchanged }
action DecideValue(p: Proc_t, r: Round_t, v: Snapshot_t): bool =
all { Decide(p, r, v), FinalityUnchanged }
action FinalizeDecision(p: Proc_t, candidate: Snapshot_t, tip: Snapshot_t): bool = all {
decision.get(p) == candidate,
ValidFinalityCandidate(candidate, tip),
ProtocolUnchanged,
latestFinal' = candidate,
finalized' = finalized.union(Set(candidate)),
finalityAction' = "FinalizeDecision",
}
action ProtocolNext =
all { Next, FinalityUnchanged }
action ComposedStutter = all {
ProtocolUnchanged,
FinalityUnchanged,
}
action ComposedNext =
any {
ProtocolNext,
nondet p = oneOf(Corr)
nondet candidate = oneOf(Snapshots)
nondet tip = oneOf(Snapshots)
FinalizeDecision(p, candidate, tip),
ComposedStutter,
}
val FinalizedPrefixLinear =
tuples(finalized, finalized).forall(((a, b)) =>
Agrees(a, b)
)
val LatestFinalExtendsAllFinalized =
finalized.forall(v => Extends(latestFinal, v))
val InitialFinalizedRemainsFinalized =
finalized.contains(InitialFinalized)
val FinalitySafety =
FinalizedPrefixLinear and
LatestFinalExtendsAllFinalized and
InitialFinalizedRemainsFinalized
val ComposedSafety =
Safety and FinalitySafety
}
module CrosslinkComposedTest {
import CrosslinkComposed.* from "./CrosslinkComposed"
export CrosslinkComposed.*
action composedUnchanged = all {
ProtocolUnchanged,
latestFinal' = latestFinal,
finalized' = finalized,
finalityAction' = finalityAction,
}
run resamplingNilPrecommitFinalizesFreshCandidateTest = {
ComposedInit
.then(SeedValidNil("p1"))
.then(SeedValidNil("p2"))
.then(SeedFaultyNil("p4"))
.then(SeedSameRoundLock("p3"))
.then(AdvanceAfterPrecommitQuorum("p1"))
.then(AdvanceAfterPrecommitQuorum("p2"))
.then(AdvanceAfterPrecommitQuorum("p3"))
.then(Propose("p2"))
.then(Prevote("p1", "a2"))
.then(Prevote("p2", "a2"))
.then(Prevote("p3", "a2"))
.then(PrecommitValue("p1", "a2"))
.then(PrecommitValue("p2", "a2"))
.then(PrecommitValue("p3", "a2"))
.then(DecideValue("p1", 1, "a2"))
.then(FinalizeDecision("p1", "a2", "a3"))
.then(all {
assert(latestFinal == "a2"),
assert(finalized.contains("a2")),
assert(ComposedSafety),
composedUnchanged,
})
}
}
module CrosslinkComposedResamplingModel {
import CrosslinkComposedTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 2,
Proposer = Map(0 -> "p1", 1 -> "p2", 2 -> "p3"),
Stream = Map(0 -> "a1", 1 -> "a2", 2 -> "a2"),
Snapshots = Set("g", "a1", "a2", "a3", "b1", "b2", "b3"),
ResampleOnNilPrecommit = true,
MaxHeight = 3,
Sigma = 1,
InitialFinalized = "g",
HeightOf = Map(
"g" -> 0,
"a1" -> 1,
"a2" -> 2,
"a3" -> 3,
"b1" -> 1,
"b2" -> 2,
"b3" -> 3,
),
AncestorAt = Map(
"g" -> Map(0 -> "g", 1 -> "None", 2 -> "None", 3 -> "None"),
"a1" -> Map(0 -> "g", 1 -> "a1", 2 -> "None", 3 -> "None"),
"a2" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "None"),
"a3" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3"),
"b1" -> Map(0 -> "g", 1 -> "b1", 2 -> "None", 3 -> "None"),
"b2" -> Map(0 -> "g", 1 -> "b1", 2 -> "b2", 3 -> "None"),
"b3" -> Map(0 -> "g", 1 -> "b1", 2 -> "b2", 3 -> "b3"),
),
).*
}
module CrosslinkComposedLivenessModel {
import CrosslinkComposedTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 2,
Proposer = Map(0 -> "p1", 1 -> "p2", 2 -> "p3"),
Stream = Map(0 -> "a1", 1 -> "a2", 2 -> "a2"),
Snapshots = Set("g", "a1", "a2", "a3", "b1", "b2", "b3"),
ResampleOnNilPrecommit = true,
MaxHeight = 3,
Sigma = 1,
InitialFinalized = "g",
HeightOf = Map(
"g" -> 0,
"a1" -> 1,
"a2" -> 2,
"a3" -> 3,
"b1" -> 1,
"b2" -> 2,
"b3" -> 3,
),
AncestorAt = Map(
"g" -> Map(0 -> "g", 1 -> "None", 2 -> "None", 3 -> "None"),
"a1" -> Map(0 -> "g", 1 -> "a1", 2 -> "None", 3 -> "None"),
"a2" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "None"),
"a3" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3"),
"b1" -> Map(0 -> "g", 1 -> "b1", 2 -> "None", 3 -> "None"),
"b2" -> Map(0 -> "g", 1 -> "b1", 2 -> "b2", 3 -> "None"),
"b3" -> Map(0 -> "g", 1 -> "b1", 2 -> "b2", 3 -> "b3"),
),
).*
var phase: int
action LivenessInit = all {
ComposedInit,
phase' = 0,
}
action LivenessStep = any {
all { phase == 0, SeedValidNil("p1"), phase' = 1 },
all { phase == 1, SeedValidNil("p2"), phase' = 2 },
all { phase == 2, SeedFaultyNil("p4"), phase' = 3 },
all { phase == 3, SeedSameRoundLock("p3"), phase' = 4 },
all { phase == 4, AdvanceAfterPrecommitQuorum("p1"), phase' = 5 },
all { phase == 5, AdvanceAfterPrecommitQuorum("p2"), phase' = 6 },
all { phase == 6, AdvanceAfterPrecommitQuorum("p3"), phase' = 7 },
all { phase == 7, Propose("p2"), phase' = 8 },
all { phase == 8, Prevote("p1", "a2"), phase' = 9 },
all { phase == 9, Prevote("p2", "a2"), phase' = 10 },
all { phase == 10, Prevote("p3", "a2"), phase' = 11 },
all { phase == 11, PrecommitValue("p1", "a2"), phase' = 12 },
all { phase == 12, PrecommitValue("p2", "a2"), phase' = 13 },
all { phase == 13, PrecommitValue("p3", "a2"), phase' = 14 },
all { phase == 14, DecideValue("p1", 1, "a2"), phase' = 15 },
all { phase == 15, FinalizeDecision("p1", "a2", "a3"), phase' = 16 },
all { phase == 16, ComposedStutter, phase' = 16 },
}
val FreshFinalityByEnd =
phase < 16 or latestFinal == "a2"
val LivenessSafety =
ComposedSafety and FreshFinalityByEnd
}
// -*- mode: Bluespec; -*-
module CrosslinkDynamicSigma {
/*
A focused model of the third Crosslink variant: dynamic sigma.
The resampling model handles a moving PoW stream by treating a 2f + 1
PRECOMMIT nil certificate as a same-round unlock certificate. This model
isolates one input to the dynamic-sigma controller: the percentage of total
PoW hash power that is participating in Crosslink.
Hash-power participation is not a Tendermint voting threshold. It is a
coverage signal for the PoW stream that finalizers see. If Crosslink only
observes or is respected by a small fraction of hash power, a nonparticipating
branch has a better chance of becoming the eventual longest chain, so sigma
must have a higher floor. Below a critical participation floor, increasing
sigma improves conservatism but does not by itself recover the same assurance
as broad PoW participation.
The calibrated controller sketch adds a bounded risk score over the signals
a deployed controller would estimate: hash-power coverage, recent round
failure rate, block-interval variance, and observed rollback depth. The
thresholds here are fixtures, not production economics, but the model makes
the intended monotone shape explicit.
*/
type Round_t = int
type Sigma_t = int
type Pct_t = int
type Snapshot_t = str
const MaxRound: Round_t
const BaseSigma: Sigma_t
const RaisedSigma: Sigma_t
const MaxSigma: Sigma_t
const TargetHashParticipationPct: Pct_t
const CriticalHashParticipationPct: Pct_t
const HashParticipationPct: Round_t -> Pct_t
const RoundFailed: Round_t -> bool
const RoundFailureRatePct: Round_t -> Pct_t
const BlockIntervalVariancePct: Round_t -> Pct_t
const ObservedReorgDepth: Round_t -> int
const CoverageRiskWeight: int
const RoundFailureRiskWeight: int
const BlockVarianceRiskWeight: int
const ReorgDepthRiskWeight: int
const RiskScoreRaisedThreshold: int
const RiskScoreMaxThreshold: int
const Snapshots: Set[Snapshot_t]
const SnapshotAtSigma: Round_t -> Sigma_t -> Snapshot_t
pure val Rounds = 0.to(MaxRound)
pure val Percentages = 0.to(100)
pure val SigmaLadder = Set(BaseSigma, RaisedSigma, MaxSigma)
assume max_round_non_negative =
MaxRound >= 0
assume sigma_ladder_is_ordered =
1 <= BaseSigma and BaseSigma < RaisedSigma and RaisedSigma < MaxSigma
assume participation_thresholds_are_ordered =
0 <= CriticalHashParticipationPct and
CriticalHashParticipationPct < TargetHashParticipationPct and
TargetHashParticipationPct <= 100
assume observed_participation_is_percentage =
Rounds.forall(r =>
0 <= HashParticipationPct.get(r) and HashParticipationPct.get(r) <= 100
)
assume observed_round_failure_rate_is_percentage =
Rounds.forall(r =>
0 <= RoundFailureRatePct.get(r) and RoundFailureRatePct.get(r) <= 100
)
assume observed_block_variance_is_percentage =
Rounds.forall(r =>
0 <= BlockIntervalVariancePct.get(r) and BlockIntervalVariancePct.get(r) <= 100
)
assume observed_reorg_depth_is_bounded =
Rounds.forall(r =>
0 <= ObservedReorgDepth.get(r) and ObservedReorgDepth.get(r) <= MaxSigma
)
assume risk_weights_are_non_negative =
CoverageRiskWeight >= 0 and
RoundFailureRiskWeight >= 0 and
BlockVarianceRiskWeight >= 0 and
ReorgDepthRiskWeight >= 0
assume risk_score_thresholds_are_ordered =
0 <= RiskScoreRaisedThreshold and RiskScoreRaisedThreshold < RiskScoreMaxThreshold
assume sampled_snapshots_are_valid =
Rounds.forall(r =>
SigmaLadder.forall(s =>
Snapshots.contains(SnapshotAtSigma.get(r).get(s))
)
)
var round: Round_t
var sigma: Sigma_t
var sampledSnapshot: Snapshot_t
var sampleStability: str
var controllerStatus: str
var riskScore: int
var riskStatus: str
var firedAction: str
pure def MaxInt(a: int, b: int): int =
if (a >= b) { a } else { b }
pure def HashParticipationSigmaFloor(pct: Pct_t): Sigma_t =
if (pct < CriticalHashParticipationPct) {
MaxSigma
} else if (pct < TargetHashParticipationPct) {
RaisedSigma
} else {
BaseSigma
}
pure def ReorgSigmaFloor(depth: int): Sigma_t =
if (depth >= RaisedSigma) {
MaxSigma
} else if (depth >= BaseSigma) {
RaisedSigma
} else {
BaseSigma
}
pure def CoverageRiskPct(hashParticipationPct: Pct_t): int =
100 - hashParticipationPct
pure def CalibratedRiskScore(
hashParticipationPct: Pct_t,
roundFailureRatePct: Pct_t,
blockIntervalVariancePct: Pct_t,
observedReorgDepth: int
): int =
CoverageRiskWeight * CoverageRiskPct(hashParticipationPct) +
RoundFailureRiskWeight * roundFailureRatePct +
BlockVarianceRiskWeight * blockIntervalVariancePct +
ReorgDepthRiskWeight * observedReorgDepth
pure def RiskScoreSigmaFloor(score: int): Sigma_t =
if (score >= RiskScoreMaxThreshold) {
MaxSigma
} else if (score >= RiskScoreRaisedThreshold) {
RaisedSigma
} else {
BaseSigma
}
pure def CalibratedRiskScoreForRound(r: Round_t): int =
CalibratedRiskScore(
HashParticipationPct.get(r),
RoundFailureRatePct.get(r),
BlockIntervalVariancePct.get(r),
ObservedReorgDepth.get(r)
)
pure def CalibratedSigmaFloorForRound(r: Round_t): Sigma_t =
MaxInt(
RiskScoreSigmaFloor(CalibratedRiskScoreForRound(r)),
MaxInt(
HashParticipationSigmaFloor(HashParticipationPct.get(r)),
ReorgSigmaFloor(ObservedReorgDepth.get(r))
)
)
pure def EscalatedSigma(current: Sigma_t): Sigma_t =
if (current < RaisedSigma) {
RaisedSigma
} else {
MaxSigma
}
pure def ControllerSigmaWithReorg(
previousSigma: Sigma_t,
nextHashParticipationPct: Pct_t,
previousRoundFailed: bool,
nextObservedReorgDepth: int
): Sigma_t =
MaxInt(
ReorgSigmaFloor(nextObservedReorgDepth),
MaxInt(
HashParticipationSigmaFloor(nextHashParticipationPct),
if (previousRoundFailed) { EscalatedSigma(previousSigma) } else { BaseSigma }
)
)
pure def ControllerSigma(
previousSigma: Sigma_t,
nextHashParticipationPct: Pct_t,
previousRoundFailed: bool
): Sigma_t =
ControllerSigmaWithReorg(previousSigma, nextHashParticipationPct, previousRoundFailed, 0)
pure def CalibratedControllerSigma(
previousSigma: Sigma_t,
nextRound: Round_t,
previousRoundFailed: bool
): Sigma_t =
MaxInt(
CalibratedSigmaFloorForRound(nextRound),
if (previousRoundFailed) { EscalatedSigma(previousSigma) } else { BaseSigma }
)
pure def ControllerStatusFor(pct: Pct_t): str =
if (pct < CriticalHashParticipationPct) {
"critical-hash-participation"
} else if (pct < TargetHashParticipationPct) {
"degraded-hash-participation"
} else {
"healthy-hash-participation"
}
pure def RiskStatusFor(score: int): str =
if (score >= RiskScoreMaxThreshold) {
"critical-stochastic-risk"
} else if (score >= RiskScoreRaisedThreshold) {
"elevated-stochastic-risk"
} else {
"low-stochastic-risk"
}
pure def HeadMinusSigma(r: Round_t, s: Sigma_t): Snapshot_t =
SnapshotAtSigma.get(r).get(s)
pure def SampleStableAcross(previousRound: Round_t, nextRound: Round_t, s: Sigma_t): bool =
HeadMinusSigma(previousRound, s) == HeadMinusSigma(nextRound, s)
pure def SampleStabilityStatusFor(r: Round_t, s: Sigma_t): str =
if (r == 0) {
"initial-sample"
} else if (SampleStableAcross(r - 1, r, s)) {
"stable-under-current-sigma"
} else {
"unstable-under-current-sigma"
}
action Init = {
val initialRiskScore = CalibratedRiskScoreForRound(0)
val initialSigma = CalibratedSigmaFloorForRound(0)
all {
round' = 0,
sigma' = initialSigma,
sampledSnapshot' = HeadMinusSigma(0, initialSigma),
sampleStability' = SampleStabilityStatusFor(0, initialSigma),
controllerStatus' = ControllerStatusFor(HashParticipationPct.get(0)),
riskScore' = initialRiskScore,
riskStatus' = RiskStatusFor(initialRiskScore),
firedAction' = "Init",
}
}
action AdvanceRound = {
val oldRound = round
val nextRound = oldRound + 1
val nextHashParticipation = HashParticipationPct.get(nextRound)
val nextReorgDepth = ObservedReorgDepth.get(nextRound)
val nextRiskScore = CalibratedRiskScoreForRound(nextRound)
val nextSigma = CalibratedControllerSigma(sigma, nextRound, RoundFailed.get(oldRound))
all {
nextRound.in(Rounds),
round' = nextRound,
sigma' = nextSigma,
sampledSnapshot' = HeadMinusSigma(nextRound, nextSigma),
sampleStability' = SampleStabilityStatusFor(nextRound, nextSigma),
controllerStatus' = ControllerStatusFor(nextHashParticipation),
riskScore' = nextRiskScore,
riskStatus' = RiskStatusFor(nextRiskScore),
firedAction' =
if (RoundFailed.get(oldRound)) {
"AdvanceAfterRoundFailure"
} else if (nextReorgDepth >= BaseSigma) {
"AdvanceAfterObservedReorg"
} else if (nextHashParticipation < TargetHashParticipationPct) {
"AdvanceWithLowHashParticipation"
} else if (RiskScoreSigmaFloor(nextRiskScore) > BaseSigma) {
"AdvanceWithElevatedStochasticRisk"
} else {
"AdvanceStable"
},
}
}
action Stutter = all {
round' = round,
sigma' = sigma,
sampledSnapshot' = sampledSnapshot,
sampleStability' = sampleStability,
controllerStatus' = controllerStatus,
riskScore' = riskScore,
riskStatus' = riskStatus,
firedAction' = firedAction,
}
action Next =
any {
AdvanceRound,
Stutter,
}
val SigmaWithinBounds =
BaseSigma <= sigma and sigma <= MaxSigma
val SigmaRespectsHashParticipationFloor =
sigma >= HashParticipationSigmaFloor(HashParticipationPct.get(round))
val SigmaRespectsObservedReorgFloor =
sigma >= ReorgSigmaFloor(ObservedReorgDepth.get(round))
val SigmaRespectsCalibratedRiskFloor =
sigma >= RiskScoreSigmaFloor(CalibratedRiskScoreForRound(round))
val RiskScoreMatchesObservedInputs =
riskScore == CalibratedRiskScoreForRound(round)
val RiskStatusMatchesObservedInputs =
riskStatus == RiskStatusFor(CalibratedRiskScoreForRound(round))
val SampledSnapshotIsValid =
Snapshots.contains(sampledSnapshot)
val SampledSnapshotMatchesSigma =
sampledSnapshot == HeadMinusSigma(round, sigma)
val SampleStabilityMatchesSigma =
sampleStability == SampleStabilityStatusFor(round, sigma)
val HashParticipationFloorIsMonotone =
Percentages.forall(lower =>
Percentages.forall(higher =>
(lower <= higher) implies
HashParticipationSigmaFloor(lower) >= HashParticipationSigmaFloor(higher)
)
)
val CalibratedRiskScoreIsMonotoneInParticipation =
Percentages.forall(lower =>
Percentages.forall(higher =>
(lower <= higher) implies
CalibratedRiskScore(lower, 0, 0, 0) >= CalibratedRiskScore(higher, 0, 0, 0)
)
)
val ControllerStatusMatchesHashParticipation =
controllerStatus == ControllerStatusFor(HashParticipationPct.get(round))
val Safety =
SigmaWithinBounds and
SigmaRespectsHashParticipationFloor and
SigmaRespectsObservedReorgFloor and
SigmaRespectsCalibratedRiskFloor and
RiskScoreMatchesObservedInputs and
RiskStatusMatchesObservedInputs and
SampledSnapshotIsValid and
SampledSnapshotMatchesSigma and
SampleStabilityMatchesSigma and
HashParticipationFloorIsMonotone and
CalibratedRiskScoreIsMonotoneInParticipation and
ControllerStatusMatchesHashParticipation
}
module CrosslinkDynamicSigmaTest {
import CrosslinkDynamicSigma.* from "./CrosslinkDynamicSigma"
export CrosslinkDynamicSigma.*
run highHashParticipationStartsAtBaseSigmaTest = {
Init.then(all {
assert(sigma == BaseSigma),
assert(controllerStatus == "healthy-hash-participation"),
assert(Safety),
Stutter,
})
}
run roundFailureEscalatesSigmaEvenWithHighHashParticipationTest = {
Init
.then(AdvanceRound)
.then(all {
assert(sigma == RaisedSigma),
assert(firedAction == "AdvanceAfterRoundFailure"),
assert(sampledSnapshot == "deep-a"),
assert(sampleStability == "stable-under-current-sigma"),
assert(Safety),
Stutter,
})
}
run lowHashParticipationRaisesSigmaFloorWithoutRoundFailureTest = {
assert(ControllerSigma(BaseSigma, TargetHashParticipationPct - 1, false) == RaisedSigma)
}
run criticalHashParticipationForcesMaxSigmaTest = {
assert(ControllerSigma(BaseSigma, CriticalHashParticipationPct - 1, false) == MaxSigma)
}
run adversarialReorgDepthRaisesSigmaFloorTest = all {
assert(ReorgSigmaFloor(BaseSigma) == RaisedSigma),
assert(ReorgSigmaFloor(RaisedSigma) == MaxSigma),
}
run hashParticipationSigmaFloorIsMonotoneTest = {
assert(HashParticipationFloorIsMonotone)
}
run calibratedRiskScoreIsMonotoneInParticipationTest = {
assert(CalibratedRiskScoreIsMonotoneInParticipation)
}
run combinedStochasticRiskRaisesSigmaWithoutSingleHardSignalTest = {
assert(RiskScoreSigmaFloor(CalibratedRiskScore(75, 30, 30, 0)) == RaisedSigma)
}
run criticalStochasticRiskForcesMaxSigmaTest = {
assert(RiskScoreSigmaFloor(CalibratedRiskScore(60, 60, 60, 1)) == MaxSigma)
}
run blockIntervalVarianceCanRaiseSigmaTest = {
assert(RiskScoreSigmaFloor(CalibratedRiskScore(90, 0, 90, 0)) == RaisedSigma)
}
run raisedSigmaCanStabilizeMovingPowSampleTest = all {
assert(not(SampleStableAcross(0, 1, BaseSigma))),
assert(SampleStableAcross(0, 1, RaisedSigma)),
}
run observedReorgRaisesLiveSigmaTest = {
Init
.then(AdvanceRound)
.then(AdvanceRound)
.then(all {
assert(sigma == RaisedSigma),
assert(firedAction == "AdvanceAfterObservedReorg"),
assert(controllerStatus == "healthy-hash-participation"),
assert(riskStatus == "low-stochastic-risk"),
assert(sampledSnapshot == "deep-a"),
assert(sampleStability == "stable-under-current-sigma"),
assert(Safety),
Stutter,
})
}
run observedLowHashParticipationRaisesLiveSigmaTest = {
Init
.then(AdvanceRound)
.then(AdvanceRound)
.then(AdvanceRound)
.then(all {
assert(sigma == RaisedSigma),
assert(firedAction == "AdvanceWithLowHashParticipation"),
assert(controllerStatus == "degraded-hash-participation"),
assert(riskStatus == "low-stochastic-risk"),
assert(sampledSnapshot == "deep-a"),
assert(sampleStability == "stable-under-current-sigma"),
assert(Safety),
Stutter,
})
}
run observedCriticalHashParticipationForcesLiveMaxSigmaTest = {
Init
.then(AdvanceRound)
.then(AdvanceRound)
.then(AdvanceRound)
.then(AdvanceRound)
.then(all {
assert(sigma == MaxSigma),
assert(firedAction == "AdvanceWithLowHashParticipation"),
assert(controllerStatus == "critical-hash-participation"),
assert(riskStatus == "low-stochastic-risk"),
assert(sampledSnapshot == "final-a"),
assert(sampleStability == "stable-under-current-sigma"),
assert(Safety),
Stutter,
})
}
}
module CrosslinkDynamicSigmaHashParticipationModel {
import CrosslinkDynamicSigmaTest(
MaxRound = 4,
BaseSigma = 1,
RaisedSigma = 3,
MaxSigma = 6,
TargetHashParticipationPct = 67,
CriticalHashParticipationPct = 50,
HashParticipationPct = Map(
0 -> 90,
1 -> 90,
2 -> 90,
3 -> 66,
4 -> 45,
),
RoundFailed = Map(
0 -> true,
1 -> false,
2 -> false,
3 -> false,
4 -> false,
),
RoundFailureRatePct = Map(
0 -> 0,
1 -> 0,
2 -> 0,
3 -> 0,
4 -> 0,
),
BlockIntervalVariancePct = Map(
0 -> 0,
1 -> 0,
2 -> 0,
3 -> 0,
4 -> 0,
),
ObservedReorgDepth = Map(
0 -> 0,
1 -> 0,
2 -> 1,
3 -> 0,
4 -> 0,
),
CoverageRiskWeight = 1,
RoundFailureRiskWeight = 1,
BlockVarianceRiskWeight = 1,
ReorgDepthRiskWeight = 40,
RiskScoreRaisedThreshold = 80,
RiskScoreMaxThreshold = 160,
Snapshots = Set("tip-a0", "tip-b1", "tip-c2", "tip-d3", "deep-a", "final-a"),
SnapshotAtSigma = Map(
0 -> Map(
1 -> "tip-a0",
3 -> "deep-a",
6 -> "final-a",
),
1 -> Map(
1 -> "tip-b1",
3 -> "deep-a",
6 -> "final-a",
),
2 -> Map(
1 -> "tip-c2",
3 -> "deep-a",
6 -> "final-a",
),
3 -> Map(
1 -> "tip-d3",
3 -> "deep-a",
6 -> "final-a",
),
4 -> Map(
1 -> "tip-d3",
3 -> "deep-a",
6 -> "final-a",
),
),
).*
}
// -*- mode: Bluespec; -*-
module CrosslinkDynamicSigmaBranchCompetition {
/*
Composition layer between dynamic sigma and generated PoW branch competition.
The branch-competition model derives best-tip changes from published tips and
honest plus adversarial work. This module feeds the resulting rollback depth
into the dynamic-sigma controller.
*/
import CrosslinkPowBranchCompetition.* from "./CrosslinkPowBranchCompetition"
export CrosslinkPowBranchCompetition.*
const BaseSigma: int
const RaisedSigma: int
const MaxSigma: int
const TargetHashParticipationPct: int
const CriticalHashParticipationPct: int
const HashParticipationPct: Round_t -> int
const RoundFailed: Round_t -> bool
assume sigma_ladder_is_ordered =
1 <= BaseSigma and BaseSigma < RaisedSigma and RaisedSigma < MaxSigma
assume participation_thresholds_are_ordered =
0 <= CriticalHashParticipationPct and
CriticalHashParticipationPct < TargetHashParticipationPct and
TargetHashParticipationPct <= 100
assume observed_participation_is_percentage =
Rounds.forall(r =>
0 <= HashParticipationPct.get(r) and HashParticipationPct.get(r) <= 100
)
var dynSigma: int
var dynStatus: str
var dynAction: str
pure def MaxInt(a: int, b: int): int =
if (a >= b) { a } else { b }
pure def DerivedReorgDepth(r: Round_t): int =
RoundRollbackDepth(r)
pure def DynHashParticipationSigmaFloor(pct: int): int =
if (pct < CriticalHashParticipationPct) {
MaxSigma
} else if (pct < TargetHashParticipationPct) {
RaisedSigma
} else {
BaseSigma
}
pure def DynReorgSigmaFloor(depth: int): int =
if (depth >= RaisedSigma) {
MaxSigma
} else if (depth >= BaseSigma) {
RaisedSigma
} else {
BaseSigma
}
pure def DynEscalatedSigma(current: int): int =
if (current < RaisedSigma) {
RaisedSigma
} else {
MaxSigma
}
pure def BranchCompetitionControllerSigma(
previousSigma: int,
nextRound: Round_t,
previousRoundFailed: bool
): int =
MaxInt(
DynReorgSigmaFloor(DerivedReorgDepth(nextRound)),
MaxInt(
DynHashParticipationSigmaFloor(HashParticipationPct.get(nextRound)),
if (previousRoundFailed) { DynEscalatedSigma(previousSigma) } else { BaseSigma }
)
)
pure def DynStatusFor(pct: int): str =
if (pct < CriticalHashParticipationPct) {
"critical-hash-participation"
} else if (pct < TargetHashParticipationPct) {
"degraded-hash-participation"
} else {
"healthy-hash-participation"
}
action BranchCompetitionDynamicInit = all {
Init,
dynSigma' = DynHashParticipationSigmaFloor(HashParticipationPct.get(0)),
dynStatus' = DynStatusFor(HashParticipationPct.get(0)),
dynAction' = "BranchCompetitionDynamicInit",
}
action BranchCompetitionDynamicAdvanceRound = {
val oldRound = round
val nextRound = oldRound + 1
val nextSigma = BranchCompetitionControllerSigma(
dynSigma,
nextRound,
RoundFailed.get(oldRound)
)
all {
AdvanceRound,
dynSigma' = nextSigma,
dynStatus' = DynStatusFor(HashParticipationPct.get(nextRound)),
dynAction' =
if (RoundFailed.get(oldRound)) {
"GeneratedAdvanceAfterRoundFailure"
} else if (DerivedReorgDepth(nextRound) >= BaseSigma) {
"GeneratedAdvanceAfterAdversarialForkSwitch"
} else if (HashParticipationPct.get(nextRound) < TargetHashParticipationPct) {
"GeneratedAdvanceWithLowHashParticipation"
} else {
"GeneratedAdvanceStable"
},
}
}
action BranchCompetitionDynamicStutter = all {
Stutter,
dynSigma' = dynSigma,
dynStatus' = dynStatus,
dynAction' = dynAction,
}
action BranchCompetitionDynamicNext =
any {
BranchCompetitionDynamicAdvanceRound,
BranchCompetitionDynamicStutter,
}
val DynSigmaWithinBounds =
BaseSigma <= dynSigma and dynSigma <= MaxSigma
val DynSigmaRespectsHashParticipationFloor =
dynSigma >= DynHashParticipationSigmaFloor(HashParticipationPct.get(round))
val DynSigmaRespectsGeneratedReorgFloor =
dynSigma >= DynReorgSigmaFloor(DerivedReorgDepth(round))
val DynStatusMatchesHashParticipation =
dynStatus == DynStatusFor(HashParticipationPct.get(round))
val BranchCompetitionDynamicSafety =
Safety and
DynSigmaWithinBounds and
DynSigmaRespectsHashParticipationFloor and
DynSigmaRespectsGeneratedReorgFloor and
DynStatusMatchesHashParticipation
}
module CrosslinkDynamicSigmaBranchCompetitionTest {
import CrosslinkDynamicSigmaBranchCompetition.* from "./CrosslinkDynamicSigmaBranchCompetition"
export CrosslinkDynamicSigmaBranchCompetition.*
run generatedCompetitionFeedsDynamicSigmaTest = all {
assert(IsBestTipAt(2, "b4")),
assert(DerivedReorgDepth(2) == 2)
}
run generatedCompetitionForkSwitchRaisesDynamicSigmaTest = {
BranchCompetitionDynamicInit
.then(BranchCompetitionDynamicAdvanceRound)
.then(all {
assert(round == 1),
assert(bestTip == "a4"),
assert(DerivedReorgDepth(round) == 0),
assert(dynSigma == BaseSigma),
assert(dynAction == "GeneratedAdvanceStable"),
assert(BranchCompetitionDynamicSafety),
BranchCompetitionDynamicStutter,
})
.then(BranchCompetitionDynamicAdvanceRound)
.then(all {
assert(round == 2),
assert(bestTip == "b4"),
assert(bestTipWork == 5),
assert(DerivedReorgDepth(round) == 2),
assert(dynSigma == RaisedSigma),
assert(dynAction == "GeneratedAdvanceAfterAdversarialForkSwitch"),
assert(BranchCompetitionDynamicSafety),
BranchCompetitionDynamicStutter,
})
}
run generatedRaisedSigmaSurvivesAdversarialSwitchTest = all {
assert(not(SampleSurvivesRollback(BestTipAt(1), BestTipAt(2), BaseSigma))),
assert(SampleSurvivesRollback(BestTipAt(1), BestTipAt(2), RaisedSigma)),
}
}
module CrosslinkDynamicSigmaBranchCompetitionModel {
import CrosslinkDynamicSigmaBranchCompetitionTest(
MaxRound = 2,
MaxHeight = 4,
MaxWork = 5,
Sigma = 3,
Snapshots = Set("g", "a3", "a4", "b4"),
BestTip = Map(
0 -> "a3",
1 -> "a4",
2 -> "b4",
),
PublishedAt = Map(
0 -> Map("g" -> true, "a3" -> true, "a4" -> false, "b4" -> false),
1 -> Map("g" -> true, "a3" -> true, "a4" -> true, "b4" -> false),
2 -> Map("g" -> true, "a3" -> true, "a4" -> true, "b4" -> true),
),
HonestWorkAt = Map(
0 -> Map("g" -> 0, "a3" -> 3, "a4" -> 0, "b4" -> 0),
1 -> Map("g" -> 0, "a3" -> 3, "a4" -> 4, "b4" -> 0),
2 -> Map("g" -> 0, "a3" -> 3, "a4" -> 4, "b4" -> 0),
),
AdversarialWorkAt = Map(
0 -> Map("g" -> 0, "a3" -> 0, "a4" -> 0, "b4" -> 4),
1 -> Map("g" -> 0, "a3" -> 0, "a4" -> 0, "b4" -> 4),
2 -> Map("g" -> 0, "a3" -> 0, "a4" -> 0, "b4" -> 5),
),
TieBreakRank = Map(
"g" -> 0,
"a3" -> 1,
"a4" -> 2,
"b4" -> 3,
),
HeightOf = Map(
"g" -> 0,
"a3" -> 3,
"a4" -> 4,
"b4" -> 4,
),
LcaHeight = Map(
"g" -> Map("g" -> 0, "a3" -> 0, "a4" -> 0, "b4" -> 0),
"a3" -> Map("g" -> 0, "a3" -> 3, "a4" -> 3, "b4" -> 2),
"a4" -> Map("g" -> 0, "a3" -> 3, "a4" -> 4, "b4" -> 2),
"b4" -> Map("g" -> 0, "a3" -> 2, "a4" -> 2, "b4" -> 4),
),
BaseSigma = 1,
RaisedSigma = 3,
MaxSigma = 6,
TargetHashParticipationPct = 67,
CriticalHashParticipationPct = 50,
HashParticipationPct = Map(
0 -> 90,
1 -> 90,
2 -> 90,
),
RoundFailed = Map(
0 -> false,
1 -> false,
2 -> false,
),
).*
}
// -*- mode: Bluespec; -*-
module CrosslinkDynamicSigmaCalibration {
/*
Calibration harness for the dynamic-sigma controller.
The controller consumes four measured signals:
- percentage of total PoW hash power participating in Crosslink
- recent Tenderlink round-failure rate
- recent PoW block-interval variance
- observed rollback/reorg depth
This model does not claim production economics. It captures the calibration
contract: for a bounded measurement window, the selected weights and risk
thresholds must classify each observed window into the expected sigma floor.
*/
type Observation_t = int
type Pct_t = int
type Sigma_t = int
const MaxObservation: Observation_t
const BaseSigma: Sigma_t
const RaisedSigma: Sigma_t
const MaxSigma: Sigma_t
const TargetHashParticipationPct: Pct_t
const CriticalHashParticipationPct: Pct_t
const MeasuredHashParticipationPct: Observation_t -> Pct_t
const MeasuredRoundFailureRatePct: Observation_t -> Pct_t
const MeasuredBlockIntervalVariancePct: Observation_t -> Pct_t
const MeasuredObservedReorgDepth: Observation_t -> int
const ExpectedMeasuredSigmaFloor: Observation_t -> Sigma_t
const CoverageRiskWeight: int
const RoundFailureRiskWeight: int
const BlockVarianceRiskWeight: int
const ReorgDepthRiskWeight: int
const RiskScoreRaisedThreshold: int
const RiskScoreMaxThreshold: int
pure val Observations = 0.to(MaxObservation)
pure val Percentages = 0.to(100)
pure val SigmaLadder = Set(BaseSigma, RaisedSigma, MaxSigma)
var observation: Observation_t
assume max_observation_non_negative =
MaxObservation >= 0
assume sigma_ladder_is_ordered =
1 <= BaseSigma and BaseSigma < RaisedSigma and RaisedSigma < MaxSigma
assume participation_thresholds_are_ordered =
0 <= CriticalHashParticipationPct and
CriticalHashParticipationPct < TargetHashParticipationPct and
TargetHashParticipationPct <= 100
assume measured_hash_participation_is_percentage =
Observations.forall(o =>
0 <= MeasuredHashParticipationPct.get(o) and MeasuredHashParticipationPct.get(o) <= 100
)
assume measured_round_failure_rate_is_percentage =
Observations.forall(o =>
0 <= MeasuredRoundFailureRatePct.get(o) and MeasuredRoundFailureRatePct.get(o) <= 100
)
assume measured_block_variance_is_percentage =
Observations.forall(o =>
0 <= MeasuredBlockIntervalVariancePct.get(o) and
MeasuredBlockIntervalVariancePct.get(o) <= 100
)
assume measured_reorg_depth_is_bounded =
Observations.forall(o =>
0 <= MeasuredObservedReorgDepth.get(o) and
MeasuredObservedReorgDepth.get(o) <= MaxSigma
)
assume measured_expected_floors_are_in_ladder =
Observations.forall(o =>
SigmaLadder.contains(ExpectedMeasuredSigmaFloor.get(o))
)
assume risk_weights_are_material =
CoverageRiskWeight > 0 and
RoundFailureRiskWeight > 0 and
BlockVarianceRiskWeight > 0 and
ReorgDepthRiskWeight > 0
assume risk_score_thresholds_are_ordered =
0 <= RiskScoreRaisedThreshold and RiskScoreRaisedThreshold < RiskScoreMaxThreshold
pure def MaxInt(a: int, b: int): int =
if (a >= b) { a } else { b }
pure def HashParticipationSigmaFloor(pct: Pct_t): Sigma_t =
if (pct < CriticalHashParticipationPct) {
MaxSigma
} else if (pct < TargetHashParticipationPct) {
RaisedSigma
} else {
BaseSigma
}
pure def ReorgSigmaFloor(depth: int): Sigma_t =
if (depth >= RaisedSigma) {
MaxSigma
} else if (depth >= BaseSigma) {
RaisedSigma
} else {
BaseSigma
}
pure def CoverageRiskPct(hashParticipationPct: Pct_t): int =
100 - hashParticipationPct
pure def CalibratedRiskScore(
hashParticipationPct: Pct_t,
roundFailureRatePct: Pct_t,
blockIntervalVariancePct: Pct_t,
observedReorgDepth: int
): int =
CoverageRiskWeight * CoverageRiskPct(hashParticipationPct) +
RoundFailureRiskWeight * roundFailureRatePct +
BlockVarianceRiskWeight * blockIntervalVariancePct +
ReorgDepthRiskWeight * observedReorgDepth
pure def RiskScoreSigmaFloor(score: int): Sigma_t =
if (score >= RiskScoreMaxThreshold) {
MaxSigma
} else if (score >= RiskScoreRaisedThreshold) {
RaisedSigma
} else {
BaseSigma
}
pure def CalibratedRiskScoreForObservation(o: Observation_t): int =
CalibratedRiskScore(
MeasuredHashParticipationPct.get(o),
MeasuredRoundFailureRatePct.get(o),
MeasuredBlockIntervalVariancePct.get(o),
MeasuredObservedReorgDepth.get(o)
)
pure def CalibratedSigmaFloorForObservation(o: Observation_t): Sigma_t =
MaxInt(
RiskScoreSigmaFloor(CalibratedRiskScoreForObservation(o)),
MaxInt(
HashParticipationSigmaFloor(MeasuredHashParticipationPct.get(o)),
ReorgSigmaFloor(MeasuredObservedReorgDepth.get(o))
)
)
val HashParticipationFloorIsMonotone =
Percentages.forall(lower =>
Percentages.forall(higher =>
(lower <= higher) implies
HashParticipationSigmaFloor(lower) >= HashParticipationSigmaFloor(higher)
)
)
val CalibratedRiskScoreIsMonotoneInParticipation =
Percentages.forall(lower =>
Percentages.forall(higher =>
(lower <= higher) implies
CalibratedRiskScore(lower, 0, 0, 0) >= CalibratedRiskScore(higher, 0, 0, 0)
)
)
val RoundFailureWeightIsMaterial =
CalibratedRiskScore(90, 10, 0, 0) > CalibratedRiskScore(90, 0, 0, 0)
val BlockVarianceWeightIsMaterial =
CalibratedRiskScore(90, 0, 10, 0) > CalibratedRiskScore(90, 0, 0, 0)
val ReorgDepthWeightIsMaterial =
CalibratedRiskScore(90, 0, 0, 1) > CalibratedRiskScore(90, 0, 0, 0)
val CalibrationMatchesMeasuredSigmaLabels =
Observations.forall(o =>
CalibratedSigmaFloorForObservation(o) == ExpectedMeasuredSigmaFloor.get(o)
)
val CalibrationSafety =
HashParticipationFloorIsMonotone and
CalibratedRiskScoreIsMonotoneInParticipation and
RoundFailureWeightIsMaterial and
BlockVarianceWeightIsMaterial and
ReorgDepthWeightIsMaterial and
CalibrationMatchesMeasuredSigmaLabels
val CurrentObservationMatchesCalibration =
CalibratedSigmaFloorForObservation(observation) == ExpectedMeasuredSigmaFloor.get(observation)
action Init = all {
observation' = 0,
}
action AdvanceObservation = {
val nextObservation = observation + 1
all {
nextObservation.in(Observations),
observation' = nextObservation,
}
}
action Stutter = all {
observation' = observation,
}
action Next =
any {
AdvanceObservation,
Stutter,
}
val Safety =
observation.in(Observations) and
CalibrationSafety and
CurrentObservationMatchesCalibration
}
module CrosslinkDynamicSigmaCalibrationTest {
import CrosslinkDynamicSigmaCalibration.* from "./CrosslinkDynamicSigmaCalibration"
export CrosslinkDynamicSigmaCalibration.*
run healthyMeasuredWindowKeepsBaseSigmaTest = all {
assert(CalibratedRiskScoreForObservation(0) < RiskScoreRaisedThreshold),
assert(CalibratedSigmaFloorForObservation(0) == BaseSigma),
}
run marginalHashParticipationRaisesSigmaTest = all {
assert(MeasuredHashParticipationPct.get(1) < TargetHashParticipationPct),
assert(MeasuredHashParticipationPct.get(1) >= CriticalHashParticipationPct),
assert(CalibratedSigmaFloorForObservation(1) == RaisedSigma),
}
run combinedMeasuredRiskRaisesSigmaTest = all {
assert(MeasuredHashParticipationPct.get(2) >= TargetHashParticipationPct),
assert(MeasuredObservedReorgDepth.get(2) < BaseSigma),
assert(CalibratedRiskScoreForObservation(2) >= RiskScoreRaisedThreshold),
assert(CalibratedRiskScoreForObservation(2) < RiskScoreMaxThreshold),
assert(CalibratedSigmaFloorForObservation(2) == RaisedSigma),
}
run deepReorgMeasuredWindowForcesMaxSigmaTest = all {
assert(MeasuredObservedReorgDepth.get(3) >= RaisedSigma),
assert(CalibratedSigmaFloorForObservation(3) == MaxSigma),
}
run criticalParticipationMeasuredWindowForcesMaxSigmaTest = all {
assert(MeasuredHashParticipationPct.get(4) < CriticalHashParticipationPct),
assert(CalibratedSigmaFloorForObservation(4) == MaxSigma),
}
run criticalCombinedRiskForcesMaxSigmaTest = all {
assert(MeasuredHashParticipationPct.get(5) >= CriticalHashParticipationPct),
assert(MeasuredObservedReorgDepth.get(5) < RaisedSigma),
assert(CalibratedRiskScoreForObservation(5) >= RiskScoreMaxThreshold),
assert(CalibratedSigmaFloorForObservation(5) == MaxSigma),
}
run calibrationMatchesAllMeasuredWindowsTest = {
assert(CalibrationSafety)
}
}
module CrosslinkDynamicSigmaCalibrationModel {
import CrosslinkDynamicSigmaCalibrationTest(
MaxObservation = 5,
BaseSigma = 1,
RaisedSigma = 3,
MaxSigma = 6,
TargetHashParticipationPct = 67,
CriticalHashParticipationPct = 50,
MeasuredHashParticipationPct = Map(
0 -> 92,
1 -> 63,
2 -> 75,
3 -> 90,
4 -> 45,
5 -> 60,
),
MeasuredRoundFailureRatePct = Map(
0 -> 0,
1 -> 0,
2 -> 15,
3 -> 0,
4 -> 0,
5 -> 30,
),
MeasuredBlockIntervalVariancePct = Map(
0 -> 5,
1 -> 10,
2 -> 15,
3 -> 5,
4 -> 0,
5 -> 30,
),
MeasuredObservedReorgDepth = Map(
0 -> 0,
1 -> 0,
2 -> 0,
3 -> 3,
4 -> 0,
5 -> 0,
),
ExpectedMeasuredSigmaFloor = Map(
0 -> 1,
1 -> 3,
2 -> 3,
3 -> 6,
4 -> 6,
5 -> 6,
),
CoverageRiskWeight = 2,
RoundFailureRiskWeight = 3,
BlockVarianceRiskWeight = 2,
ReorgDepthRiskWeight = 60,
RiskScoreRaisedThreshold = 100,
RiskScoreMaxThreshold = 220,
).*
}
// -*- mode: Bluespec; -*-
module CrosslinkDynamicSigmaFinality {
/*
Full bounded composition for the current Crosslink Quint slices.
A derived PoW fork signal raises dynamic sigma, nil-precommit resampling lets
Tenderlink decide the fresh stream value, and Crosslink finality uses the live
dynamic sigma as its tail-confirmation depth.
*/
import CrosslinkDynamicSigmaResampling.* from "./CrosslinkDynamicSigmaResampling"
export CrosslinkDynamicSigmaResampling.*
type ConsensusHeight_t = int
type Height_t = int
type Block_t = str
const MaxConsensusHeight: ConsensusHeight_t
const MaxHeight: Height_t
const InitialFinalized: Snapshot_t
const AncestorAt: Snapshot_t -> Height_t -> Block_t
pure val ConsensusHeights = 0.to(MaxConsensusHeight)
pure val Heights = 0.to(MaxHeight)
assume max_consensus_height_non_negative =
MaxConsensusHeight >= 0
assume initial_finalized_is_snapshot =
Snapshots.contains(InitialFinalized)
var consensusHeight: ConsensusHeight_t
var latestFinal: Snapshot_t
var finalized: Set[Snapshot_t]
var finalityAction: str
pure def IsSnapshot(v: Snapshot_t): bool =
Snapshots.contains(v)
pure def Height(v: Snapshot_t): Height_t =
HeightOf.get(v)
pure def BlockAt(v: Snapshot_t, h: Height_t): Block_t =
AncestorAt.get(v).get(h)
def Extends(newer: Snapshot_t, older: Snapshot_t): bool =
IsSnapshot(newer) and
IsSnapshot(older) and
Height(newer) >= Height(older) and
Heights.forall(h =>
h > Height(older) or BlockAt(newer, h) == BlockAt(older, h)
)
def Agrees(a: Snapshot_t, b: Snapshot_t): bool =
Extends(a, b) or Extends(b, a)
def TailConfirmsAtSigma(tip: Snapshot_t, candidate: Snapshot_t, sigma: int): bool =
IsSnapshot(tip) and
IsSnapshot(candidate) and
Height(tip) >= Height(candidate) + sigma and
BlockAt(tip, Height(candidate)) == BlockAt(candidate, Height(candidate))
def DynamicTailConfirms(tip: Snapshot_t, candidate: Snapshot_t): bool =
TailConfirmsAtSigma(tip, candidate, dynSigma)
def ValidFinalityCandidate(candidate: Snapshot_t, tip: Snapshot_t): bool =
Extends(candidate, latestFinal) and
Height(candidate) > Height(latestFinal) and
DynamicTailConfirms(tip, candidate)
action FullComposedInit = all {
DynamicResamplingInit,
consensusHeight' = 0,
latestFinal' = InitialFinalized,
finalized' = Set(InitialFinalized),
finalityAction' = "FullComposedInit",
}
action FinalityUnchanged = all {
consensusHeight' = consensusHeight,
latestFinal' = latestFinal,
finalized' = finalized,
finalityAction' = finalityAction,
}
action DynamicAndProtocolUnchanged = all {
DynamicResamplingStutter,
}
action FullSeedValidNil(p: Proc_t): bool =
all { DynamicSeedValidNil(p), FinalityUnchanged }
action FullSeedFaultyNil(p: Proc_t): bool =
all { DynamicSeedFaultyNil(p), FinalityUnchanged }
action FullSeedSameRoundLock(p: Proc_t): bool =
all { DynamicSeedSameRoundLock(p), FinalityUnchanged }
action FullAdvanceDerivedForkSignal =
all { AdvanceDerivedForkSignal, FinalityUnchanged }
action FullAdvanceHashParticipationSignal =
all { AdvanceHashParticipationSignal, FinalityUnchanged }
action FullStartNextRound(p: Proc_t): bool =
all { DynamicStartNextRound(p), FinalityUnchanged }
action FullInsertProposal(p: Proc_t): bool =
all { DynamicInsertProposal(p), FinalityUnchanged }
action FullPrevote(p: Proc_t, v: Snapshot_t): bool =
all { DynamicPrevote(p, v), FinalityUnchanged }
action FullPrecommitValue(p: Proc_t, v: Snapshot_t): bool =
all { DynamicPrecommitValue(p, v), FinalityUnchanged }
action FullDecide(p: Proc_t, r: Round_t, v: Snapshot_t): bool =
all { DynamicDecide(p, r, v), FinalityUnchanged }
action FullFinalizeDecisionAt(
nextHeight: ConsensusHeight_t,
p: Proc_t,
candidate: Snapshot_t,
tip: Snapshot_t
): bool = all {
nextHeight == consensusHeight + 1,
nextHeight.in(ConsensusHeights),
decision.get(p) == candidate,
ValidFinalityCandidate(candidate, tip),
DynamicAndProtocolUnchanged,
consensusHeight' = nextHeight,
latestFinal' = candidate,
finalized' = finalized.union(Set(candidate)),
finalityAction' = "FullFinalizeDecision",
}
action FullFinalizeDecision(p: Proc_t, candidate: Snapshot_t, tip: Snapshot_t): bool =
FullFinalizeDecisionAt(consensusHeight + 1, p, candidate, tip)
action FullProtocolNext =
all { DynamicResamplingNext, FinalityUnchanged }
action FullComposedStutter = all {
DynamicAndProtocolUnchanged,
FinalityUnchanged,
}
action FullComposedNext =
any {
FullProtocolNext,
nondet p = oneOf(Corr)
nondet candidate = oneOf(Snapshots)
nondet tip = oneOf(Snapshots)
FullFinalizeDecision(p, candidate, tip),
FullComposedStutter,
}
val FinalizedPrefixLinear =
tuples(finalized, finalized).forall(((a, b)) =>
Agrees(a, b)
)
val LatestFinalExtendsAllFinalized =
finalized.forall(v => Extends(latestFinal, v))
val InitialFinalizedRemainsFinalized =
finalized.contains(InitialFinalized)
val ConsensusHeightIsBounded =
0 <= consensusHeight and consensusHeight <= MaxConsensusHeight
val FinalitySafety =
ConsensusHeightIsBounded and
FinalizedPrefixLinear and
LatestFinalExtendsAllFinalized and
InitialFinalizedRemainsFinalized
val FullProtocolProjectionSafety =
DynamicResamplingSafety
val FullFinalityProjectionSafety =
FinalitySafety
val FullWorkCompetitionProjectionSafety =
CurrentDynamicBestTipMatchesGeneratedCompetition
val FullComposedSafety =
FullProtocolProjectionSafety and
FullFinalityProjectionSafety and
FullWorkCompetitionProjectionSafety
}
module CrosslinkDynamicSigmaFinalityTest {
import CrosslinkDynamicSigmaFinality.* from "./CrosslinkDynamicSigmaFinality"
export CrosslinkDynamicSigmaFinality.*
run dynamicSigmaResamplingFinalizesTailConfirmedFreshCandidateTest = {
FullComposedInit
.then(FullSeedValidNil("p1"))
.then(FullSeedValidNil("p2"))
.then(FullSeedFaultyNil("p4"))
.then(FullSeedSameRoundLock("p3"))
.then(FullAdvanceDerivedForkSignal)
.then(all {
assert(dynSigma == RaisedSigma),
assert(dynamicAction == "DerivedForkRaiseSigma"),
assert(FullComposedSafety),
FullComposedStutter,
})
.then(FullStartNextRound("p1"))
.then(FullStartNextRound("p2"))
.then(FullStartNextRound("p3"))
.then(FullInsertProposal("p2"))
.then(FullPrevote("p1", "b2"))
.then(FullPrevote("p2", "b2"))
.then(FullPrevote("p3", "b2"))
.then(FullPrecommitValue("p1", "b2"))
.then(FullPrecommitValue("p2", "b2"))
.then(FullPrecommitValue("p3", "b2"))
.then(FullDecide("p1", 1, "b2"))
.then(FullFinalizeDecision("p1", "b2", "b5"))
.then(all {
assert(consensusHeight == 1),
assert(latestFinal == "b2"),
assert(finalized.contains("b2")),
assert(dynSigma == RaisedSigma),
assert(FullComposedSafety),
FullComposedStutter,
})
}
run dynamicSigmaRejectsUnderconfirmedFreshCandidateTest = {
FullComposedInit
.then(FullSeedValidNil("p1"))
.then(FullSeedValidNil("p2"))
.then(FullSeedFaultyNil("p4"))
.then(FullSeedSameRoundLock("p3"))
.then(FullAdvanceDerivedForkSignal)
.then(FullStartNextRound("p1"))
.then(FullStartNextRound("p2"))
.then(FullStartNextRound("p3"))
.then(FullInsertProposal("p2"))
.then(FullPrevote("p1", "b2"))
.then(FullPrevote("p2", "b2"))
.then(FullPrevote("p3", "b2"))
.then(FullPrecommitValue("p1", "b2"))
.then(FullPrecommitValue("p2", "b2"))
.then(FullPrecommitValue("p3", "b2"))
.then(FullDecide("p1", 1, "b2"))
.then(FullFinalizeDecision("p1", "b2", "b4"))
.fail()
}
run dynamicSigmaRejectsSkippedBftHeightFinalityTest = {
FullComposedInit
.then(FullSeedValidNil("p1"))
.then(FullSeedValidNil("p2"))
.then(FullSeedFaultyNil("p4"))
.then(FullSeedSameRoundLock("p3"))
.then(FullAdvanceDerivedForkSignal)
.then(FullStartNextRound("p1"))
.then(FullStartNextRound("p2"))
.then(FullStartNextRound("p3"))
.then(FullInsertProposal("p2"))
.then(FullPrevote("p1", "b2"))
.then(FullPrevote("p2", "b2"))
.then(FullPrevote("p3", "b2"))
.then(FullPrecommitValue("p1", "b2"))
.then(FullPrecommitValue("p2", "b2"))
.then(FullPrecommitValue("p3", "b2"))
.then(FullDecide("p1", 1, "b2"))
.then(FullFinalizeDecisionAt(2, "p1", "b2", "b5"))
.fail()
}
run hashParticipationSignalRaisesFullCompositionSigmaTest = {
FullComposedInit
.then(FullAdvanceDerivedForkSignal)
.then(FullAdvanceHashParticipationSignal)
.then(all {
assert(dynRound == 2),
assert(DerivedReorgDepth(dynRound) == 0),
assert(dynSigma == MaxSigma),
assert(controllerStatus == "critical-hash-participation"),
assert(dynamicAction == "CriticalHashParticipationRaiseSigma"),
assert(FullComposedSafety),
FullComposedStutter,
})
}
run hashParticipationSigmaCanDelayFullFinalityTest = {
FullComposedInit
.then(FullSeedValidNil("p1"))
.then(FullSeedValidNil("p2"))
.then(FullSeedFaultyNil("p4"))
.then(FullSeedSameRoundLock("p3"))
.then(FullAdvanceDerivedForkSignal)
.then(FullStartNextRound("p1"))
.then(FullStartNextRound("p2"))
.then(FullStartNextRound("p3"))
.then(FullInsertProposal("p2"))
.then(FullPrevote("p1", "b2"))
.then(FullPrevote("p2", "b2"))
.then(FullPrevote("p3", "b2"))
.then(FullPrecommitValue("p1", "b2"))
.then(FullPrecommitValue("p2", "b2"))
.then(FullPrecommitValue("p3", "b2"))
.then(FullDecide("p1", 1, "b2"))
.then(FullAdvanceHashParticipationSignal)
.then(all {
assert(dynSigma == MaxSigma),
assert(DerivedReorgDepth(dynRound) == 0),
assert(controllerStatus == "critical-hash-participation"),
assert(FullComposedSafety),
FullComposedStutter,
})
.then(FullFinalizeDecision("p1", "b2", "b5"))
.fail()
}
run generatedCompetitionBacksFullCompositionForkSignalTest = all {
assert(IsBestTipAt(0, "a3")),
assert(not(CandidateAt(0, "b4"))),
assert(IsBestTipAt(1, "b4")),
assert(DerivedReorgDepth(1) == 2),
}
}
module CrosslinkDynamicSigmaFinalityModel {
import CrosslinkDynamicSigmaFinalityTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxConsensusHeight = 2,
MaxRound = 2,
Proposer = Map(0 -> "p1", 1 -> "p2", 2 -> "p3"),
Stream = Map(0 -> "a2", 1 -> "b2", 2 -> "b2"),
Snapshots = Set("g", "a1", "a2", "a3", "b2", "b3", "b4", "b5"),
ResampleOnNilPrecommit = true,
BaseSigma = 1,
RaisedSigma = 3,
MaxSigma = 6,
MaxWork = 6,
TargetHashParticipationPct = 67,
CriticalHashParticipationPct = 50,
HashParticipationPct = Map(
0 -> 90,
1 -> 90,
2 -> 45,
),
BestTip = Map(
0 -> "a3",
1 -> "b4",
2 -> "b5",
),
PublishedAt = Map(
0 -> Map(
"g" -> true,
"a1" -> true,
"a2" -> true,
"a3" -> true,
"b2" -> false,
"b3" -> false,
"b4" -> false,
"b5" -> false,
),
1 -> Map(
"g" -> true,
"a1" -> true,
"a2" -> true,
"a3" -> true,
"b2" -> true,
"b3" -> true,
"b4" -> true,
"b5" -> false,
),
2 -> Map(
"g" -> true,
"a1" -> true,
"a2" -> true,
"a3" -> true,
"b2" -> true,
"b3" -> true,
"b4" -> true,
"b5" -> true,
),
),
HonestWorkAt = Map(
0 -> Map("g" -> 0, "a1" -> 1, "a2" -> 2, "a3" -> 3, "b2" -> 0, "b3" -> 0, "b4" -> 0, "b5" -> 0),
1 -> Map("g" -> 0, "a1" -> 1, "a2" -> 2, "a3" -> 3, "b2" -> 0, "b3" -> 0, "b4" -> 0, "b5" -> 0),
2 -> Map("g" -> 0, "a1" -> 1, "a2" -> 2, "a3" -> 3, "b2" -> 0, "b3" -> 0, "b4" -> 0, "b5" -> 0),
),
AdversarialWorkAt = Map(
0 -> Map("g" -> 0, "a1" -> 0, "a2" -> 0, "a3" -> 0, "b2" -> 2, "b3" -> 3, "b4" -> 5, "b5" -> 0),
1 -> Map("g" -> 0, "a1" -> 0, "a2" -> 0, "a3" -> 0, "b2" -> 2, "b3" -> 3, "b4" -> 5, "b5" -> 0),
2 -> Map("g" -> 0, "a1" -> 0, "a2" -> 0, "a3" -> 0, "b2" -> 2, "b3" -> 3, "b4" -> 5, "b5" -> 6),
),
TieBreakRank = Map(
"g" -> 0,
"a1" -> 1,
"a2" -> 2,
"a3" -> 3,
"b2" -> 4,
"b3" -> 5,
"b4" -> 6,
"b5" -> 7,
),
MaxHeight = 5,
InitialFinalized = "g",
HeightOf = Map(
"g" -> 0,
"a1" -> 1,
"a2" -> 2,
"a3" -> 3,
"b2" -> 2,
"b3" -> 3,
"b4" -> 4,
"b5" -> 5,
),
AncestorAt = Map(
"g" -> Map(0 -> "g", 1 -> "None", 2 -> "None", 3 -> "None", 4 -> "None", 5 -> "None"),
"a1" -> Map(0 -> "g", 1 -> "a1", 2 -> "None", 3 -> "None", 4 -> "None", 5 -> "None"),
"a2" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "None", 4 -> "None", 5 -> "None"),
"a3" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3", 4 -> "None", 5 -> "None"),
"b2" -> Map(0 -> "g", 1 -> "a1", 2 -> "b2", 3 -> "None", 4 -> "None", 5 -> "None"),
"b3" -> Map(0 -> "g", 1 -> "a1", 2 -> "b2", 3 -> "b3", 4 -> "None", 5 -> "None"),
"b4" -> Map(0 -> "g", 1 -> "a1", 2 -> "b2", 3 -> "b3", 4 -> "b4", 5 -> "None"),
"b5" -> Map(0 -> "g", 1 -> "a1", 2 -> "b2", 3 -> "b3", 4 -> "b4", 5 -> "b5"),
),
LcaHeight = Map(
"a3" -> Map("b4" -> 1, "b5" -> 1),
"b4" -> Map("b4" -> 4, "b5" -> 4),
"b5" -> Map("b4" -> 4, "b5" -> 5),
),
).*
}
// -*- mode: Bluespec; -*-
module CrosslinkDynamicSigmaForkSchedule {
/*
Composition layer between the dynamic-sigma controller and the PoW fork
schedule. The purpose is to remove one layer of hand-wiring: dynamic sigma
should consume rollback depth derived from best-tip changes, not a supplied
ObservedReorgDepth map.
*/
import CrosslinkPowForkSchedule.* from "./CrosslinkPowForkSchedule"
export CrosslinkPowForkSchedule.*
const BaseSigma: int
const RaisedSigma: int
const MaxSigma: int
const TargetHashParticipationPct: int
const CriticalHashParticipationPct: int
const HashParticipationPct: Round_t -> int
const RoundFailed: Round_t -> bool
assume sigma_ladder_is_ordered =
1 <= BaseSigma and BaseSigma < RaisedSigma and RaisedSigma < MaxSigma
assume participation_thresholds_are_ordered =
0 <= CriticalHashParticipationPct and
CriticalHashParticipationPct < TargetHashParticipationPct and
TargetHashParticipationPct <= 100
assume observed_participation_is_percentage =
Rounds.forall(r =>
0 <= HashParticipationPct.get(r) and HashParticipationPct.get(r) <= 100
)
var dynSigma: int
var dynStatus: str
var dynAction: str
pure def MaxInt(a: int, b: int): int =
if (a >= b) { a } else { b }
pure def DerivedReorgDepth(r: Round_t): int =
RoundRollbackDepth(r)
pure def DynHashParticipationSigmaFloor(pct: int): int =
if (pct < CriticalHashParticipationPct) {
MaxSigma
} else if (pct < TargetHashParticipationPct) {
RaisedSigma
} else {
BaseSigma
}
pure def DynReorgSigmaFloor(depth: int): int =
if (depth >= RaisedSigma) {
MaxSigma
} else if (depth >= BaseSigma) {
RaisedSigma
} else {
BaseSigma
}
pure def DynEscalatedSigma(current: int): int =
if (current < RaisedSigma) {
RaisedSigma
} else {
MaxSigma
}
pure def DerivedControllerSigma(
previousSigma: int,
nextRound: Round_t,
previousRoundFailed: bool
): int =
MaxInt(
DynReorgSigmaFloor(DerivedReorgDepth(nextRound)),
MaxInt(
DynHashParticipationSigmaFloor(HashParticipationPct.get(nextRound)),
if (previousRoundFailed) { DynEscalatedSigma(previousSigma) } else { BaseSigma }
)
)
pure def DynStatusFor(pct: int): str =
if (pct < CriticalHashParticipationPct) {
"critical-hash-participation"
} else if (pct < TargetHashParticipationPct) {
"degraded-hash-participation"
} else {
"healthy-hash-participation"
}
action DerivedInit = all {
Init,
dynSigma' = DynHashParticipationSigmaFloor(HashParticipationPct.get(0)),
dynStatus' = DynStatusFor(HashParticipationPct.get(0)),
dynAction' = "DerivedInit",
}
action DerivedAdvanceRound = {
val oldRound = round
val nextRound = oldRound + 1
val nextSigma = DerivedControllerSigma(
dynSigma,
nextRound,
RoundFailed.get(oldRound)
)
all {
AdvanceRound,
dynSigma' = nextSigma,
dynStatus' = DynStatusFor(HashParticipationPct.get(nextRound)),
dynAction' =
if (RoundFailed.get(oldRound)) {
"DerivedAdvanceAfterRoundFailure"
} else if (DerivedReorgDepth(nextRound) >= BaseSigma) {
"DerivedAdvanceAfterForkSwitch"
} else if (HashParticipationPct.get(nextRound) < TargetHashParticipationPct) {
"DerivedAdvanceWithLowHashParticipation"
} else {
"DerivedAdvanceStable"
},
}
}
action DerivedStutter = all {
Stutter,
dynSigma' = dynSigma,
dynStatus' = dynStatus,
dynAction' = dynAction,
}
action DerivedNext =
any {
DerivedAdvanceRound,
DerivedStutter,
}
val DynSigmaWithinBounds =
BaseSigma <= dynSigma and dynSigma <= MaxSigma
val DynSigmaRespectsHashParticipationFloor =
dynSigma >= DynHashParticipationSigmaFloor(HashParticipationPct.get(round))
val DynSigmaRespectsDerivedReorgFloor =
dynSigma >= DynReorgSigmaFloor(DerivedReorgDepth(round))
val DynStatusMatchesHashParticipation =
dynStatus == DynStatusFor(HashParticipationPct.get(round))
val DerivedSafety =
Safety and
DynSigmaWithinBounds and
DynSigmaRespectsHashParticipationFloor and
DynSigmaRespectsDerivedReorgFloor and
DynStatusMatchesHashParticipation
}
module CrosslinkDynamicSigmaForkScheduleTest {
import CrosslinkDynamicSigmaForkSchedule.* from "./CrosslinkDynamicSigmaForkSchedule"
export CrosslinkDynamicSigmaForkSchedule.*
run derivedReorgDepthFeedsDynamicSigmaTest = {
assert(DerivedReorgDepth(2) == 2)
}
run forkScheduleDerivedReorgRaisesDynamicSigmaTest = {
DerivedInit
.then(DerivedAdvanceRound)
.then(all {
assert(round == 1),
assert(bestTip == "a4"),
assert(DerivedReorgDepth(round) == 0),
assert(dynSigma == BaseSigma),
assert(dynAction == "DerivedAdvanceStable"),
assert(DerivedSafety),
DerivedStutter,
})
.then(DerivedAdvanceRound)
.then(all {
assert(round == 2),
assert(bestTip == "b4"),
assert(DerivedReorgDepth(round) == 2),
assert(dynSigma == RaisedSigma),
assert(dynAction == "DerivedAdvanceAfterForkSwitch"),
assert(DerivedSafety),
DerivedStutter,
})
}
run derivedRaisedSigmaSurvivesForkSwitchTest = all {
assert(not(SampleSurvivesRollback("a4", "b4", BaseSigma))),
assert(SampleSurvivesRollback("a4", "b4", RaisedSigma)),
}
}
module CrosslinkDynamicSigmaForkScheduleModel {
import CrosslinkDynamicSigmaForkScheduleTest(
MaxRound = 2,
MaxHeight = 4,
Sigma = 3,
Snapshots = Set("g", "a1", "a2", "a3", "a4", "b3", "b4"),
BestTip = Map(
0 -> "a3",
1 -> "a4",
2 -> "b4",
),
HeightOf = Map(
"g" -> 0,
"a1" -> 1,
"a2" -> 2,
"a3" -> 3,
"a4" -> 4,
"b3" -> 3,
"b4" -> 4,
),
LcaHeight = Map(
"a3" -> Map("a4" -> 3, "b4" -> 2),
"a4" -> Map("a4" -> 4, "b4" -> 2),
"b4" -> Map("a4" -> 2, "b4" -> 4),
),
BaseSigma = 1,
RaisedSigma = 3,
MaxSigma = 6,
TargetHashParticipationPct = 67,
CriticalHashParticipationPct = 50,
HashParticipationPct = Map(
0 -> 90,
1 -> 90,
2 -> 90,
),
RoundFailed = Map(
0 -> false,
1 -> false,
2 -> false,
),
).*
}
// -*- mode: Bluespec; -*-
module CrosslinkDynamicSigmaHysteresis {
/*
A focused model of bounded dynamic-sigma decreases.
The dynamic-sigma controller may need to raise sigma immediately when a
telemetry window reports worse fork, participation, or economic risk. The
reverse direction should be slower: short windows near a threshold should not
rapidly lower the confirmation depth. This model captures the policy shape
implemented by the Rust helper `apply_dynamic_sigma_hysteresis`.
*/
type Step_t = int
type Sigma_t = int
const MaxStep: Step_t
const BaseSigma: Sigma_t
const RaisedSigma: Sigma_t
const MaxSigma: Sigma_t
const InitialSigma: Sigma_t
const DecreaseConfirmationWindows: int
const RequiredSigma: Step_t -> Sigma_t
pure val Steps = 0.to(MaxStep)
pure val SigmaLadder = Set(BaseSigma, RaisedSigma, MaxSigma)
var step: Step_t
var currentSigma: Sigma_t
var stableWindowsBelowCurrent: int
assume max_step_non_negative =
MaxStep >= 0
assume sigma_ladder_is_ordered =
1 <= BaseSigma and BaseSigma < RaisedSigma and RaisedSigma < MaxSigma
assume initial_sigma_is_in_ladder =
SigmaLadder.contains(InitialSigma)
assume decrease_confirmation_windows_is_non_negative =
DecreaseConfirmationWindows >= 0
assume required_sigmas_are_in_ladder =
Steps.forall(s => SigmaLadder.contains(RequiredSigma.get(s)))
pure def MaxInt(a: int, b: int): int =
if (a >= b) { a } else { b }
pure def NextLowerSigma(sigma: Sigma_t): Sigma_t =
if (sigma > RaisedSigma) { RaisedSigma } else { BaseSigma }
pure def HysteresisSigma(
current: Sigma_t,
stableBelow: int,
required: Sigma_t,
): Sigma_t =
if (required >= current) {
required
} else if (stableBelow + 1 < DecreaseConfirmationWindows) {
current
} else {
MaxInt(NextLowerSigma(current), required)
}
pure def HysteresisStableWindows(
current: Sigma_t,
stableBelow: int,
required: Sigma_t,
): int =
if (required >= current) {
0
} else if (stableBelow + 1 < DecreaseConfirmationWindows) {
stableBelow + 1
} else {
0
}
action Init = all {
step' = 0,
currentSigma' = InitialSigma,
stableWindowsBelowCurrent' = 0,
}
action Advance = {
val nextStep = step + 1
val required = RequiredSigma.get(nextStep)
all {
nextStep.in(Steps),
step' = nextStep,
currentSigma' = HysteresisSigma(currentSigma, stableWindowsBelowCurrent, required),
stableWindowsBelowCurrent' =
HysteresisStableWindows(currentSigma, stableWindowsBelowCurrent, required),
}
}
action Stutter = all {
step' = step,
currentSigma' = currentSigma,
stableWindowsBelowCurrent' = stableWindowsBelowCurrent,
}
action Next =
any {
Advance,
Stutter,
}
val CurrentSigmaIsInLadder =
SigmaLadder.contains(currentSigma)
val CurrentSigmaRespectsRequiredFloor =
currentSigma >= RequiredSigma.get(step)
val StableCounterIsNonNegative =
stableWindowsBelowCurrent >= 0
val Safety =
step.in(Steps) and
CurrentSigmaIsInLadder and
CurrentSigmaRespectsRequiredFloor and
StableCounterIsNonNegative
}
module CrosslinkDynamicSigmaHysteresisTest {
import CrosslinkDynamicSigmaHysteresis.* from "./CrosslinkDynamicSigmaHysteresis"
export CrosslinkDynamicSigmaHysteresis.*
run higherRequiredSigmaAppliesImmediatelyTest = {
Init
.then(Advance)
.then(all {
assert(currentSigma == MaxSigma),
assert(stableWindowsBelowCurrent == 0),
Stutter,
})
}
run lowerRequiredSigmaWaitsForStableWindowTest = {
Init
.then(Advance)
.then(Advance)
.then(all {
assert(currentSigma == MaxSigma),
assert(stableWindowsBelowCurrent == 1),
Stutter,
})
}
run stableLowerRequiredSigmaStepsDownOneLevelTest = {
Init
.then(Advance)
.then(Advance)
.then(Advance)
.then(all {
assert(currentSigma == RaisedSigma),
assert(stableWindowsBelowCurrent == 0),
Stutter,
})
}
run secondStableDecreaseReturnsToBaseTest = {
Init
.then(Advance)
.then(Advance)
.then(Advance)
.then(Advance)
.then(Advance)
.then(all {
assert(currentSigma == BaseSigma),
assert(stableWindowsBelowCurrent == 0),
Stutter,
})
}
run hysteresisSafetyHoldsAtWitnessEndTest = {
Init
.then(Advance)
.then(Advance)
.then(Advance)
.then(Advance)
.then(Advance)
.then(all {
assert(Safety),
Stutter,
})
}
}
module CrosslinkDynamicSigmaHysteresisModel {
import CrosslinkDynamicSigmaHysteresisTest(
MaxStep = 5,
BaseSigma = 1,
RaisedSigma = 3,
MaxSigma = 6,
InitialSigma = 1,
DecreaseConfirmationWindows = 2,
RequiredSigma = Map(
0 -> 1,
1 -> 6,
2 -> 1,
3 -> 1,
4 -> 1,
5 -> 1,
),
).*
}
// -*- mode: Bluespec; -*-
module CrosslinkDynamicSigmaResampling {
/*
Composition layer for dynamic sigma and nil-precommit resampling.
The target witness is a fork switch that derives rollback depth, raises sigma,
and then lets the nil-precommit recovery path resample into a fresh
Tenderlink decision.
*/
import CrosslinkResampling.* from "./CrosslinkResampling"
export CrosslinkResampling.*
const BaseSigma: int
const RaisedSigma: int
const MaxSigma: int
const MaxWork: int
const TargetHashParticipationPct: int
const CriticalHashParticipationPct: int
const HashParticipationPct: Round_t -> int
const BestTip: Round_t -> Snapshot_t
const PublishedAt: Round_t -> Snapshot_t -> bool
const HonestWorkAt: Round_t -> Snapshot_t -> int
const AdversarialWorkAt: Round_t -> Snapshot_t -> int
const TieBreakRank: Snapshot_t -> int
const HeightOf: Snapshot_t -> int
const LcaHeight: Snapshot_t -> Snapshot_t -> int
pure val Percentages = 0.to(100)
assume sigma_ladder_is_ordered =
1 <= BaseSigma and BaseSigma < RaisedSigma and RaisedSigma < MaxSigma
assume max_work_non_negative =
MaxWork >= 0
assume participation_thresholds_are_ordered =
0 <= CriticalHashParticipationPct and
CriticalHashParticipationPct < TargetHashParticipationPct and
TargetHashParticipationPct <= 100
assume observed_participation_is_percentage =
Rounds.forall(r =>
0 <= HashParticipationPct.get(r) and HashParticipationPct.get(r) <= 100
)
assume best_tips_are_snapshots =
Rounds.forall(r => Snapshots.contains(BestTip.get(r)))
assume published_work_is_bounded =
Rounds.forall(r =>
Snapshots.forall(v =>
0 <= HonestWorkAt.get(r).get(v) and
0 <= AdversarialWorkAt.get(r).get(v) and
TotalWorkAt(r, v) <= MaxWork
)
)
assume generated_best_tips_match_work_competition =
Rounds.forall(r => IsBestTipAt(r, BestTip.get(r)))
var dynRound: Round_t
var dynSigma: int
var controllerStatus: str
var dynamicAction: str
pure def MaxInt(a: int, b: int): int =
if (a >= b) { a } else { b }
pure def TotalWorkAt(r: Round_t, v: Snapshot_t): int =
HonestWorkAt.get(r).get(v) + AdversarialWorkAt.get(r).get(v)
pure def CandidateAt(r: Round_t, v: Snapshot_t): bool =
Snapshots.contains(v) and PublishedAt.get(r).get(v)
pure def BetterOrEqualAt(r: Round_t, a: Snapshot_t, b: Snapshot_t): bool =
TotalWorkAt(r, a) > TotalWorkAt(r, b) or
(
TotalWorkAt(r, a) == TotalWorkAt(r, b) and
TieBreakRank.get(a) >= TieBreakRank.get(b)
)
pure def IsBestTipAt(r: Round_t, v: Snapshot_t): bool =
CandidateAt(r, v) and
Snapshots.forall(w =>
not(CandidateAt(r, w)) or BetterOrEqualAt(r, v, w)
)
pure def CommonAncestorHeight(previousTip: Snapshot_t, nextTip: Snapshot_t): int =
LcaHeight.get(previousTip).get(nextTip)
pure def RollbackDepth(previousTip: Snapshot_t, nextTip: Snapshot_t): int =
HeightOf.get(previousTip) - CommonAncestorHeight(previousTip, nextTip)
pure def DerivedReorgDepth(r: Round_t): int =
if (r == 0) {
0
} else {
RollbackDepth(BestTip.get(r - 1), BestTip.get(r))
}
pure def ReorgSigmaFloor(depth: int): int =
if (depth >= RaisedSigma) {
MaxSigma
} else if (depth >= BaseSigma) {
RaisedSigma
} else {
BaseSigma
}
pure def HashParticipationSigmaFloor(pct: int): int =
if (pct < CriticalHashParticipationPct) {
MaxSigma
} else if (pct < TargetHashParticipationPct) {
RaisedSigma
} else {
BaseSigma
}
pure def ControllerStatusFor(pct: int): str =
if (pct < CriticalHashParticipationPct) {
"critical-hash-participation"
} else if (pct < TargetHashParticipationPct) {
"degraded-hash-participation"
} else {
"healthy-hash-participation"
}
def DynamicControllerSigma(nextRound: Round_t): int =
MaxInt(
dynSigma,
MaxInt(
ReorgSigmaFloor(DerivedReorgDepth(nextRound)),
HashParticipationSigmaFloor(HashParticipationPct.get(nextRound))
)
)
action DynamicStateUnchanged = all {
dynRound' = dynRound,
dynSigma' = dynSigma,
controllerStatus' = controllerStatus,
dynamicAction' = dynamicAction,
}
action DynamicResamplingInit = all {
Init,
dynRound' = 0,
dynSigma' = HashParticipationSigmaFloor(HashParticipationPct.get(0)),
controllerStatus' = ControllerStatusFor(HashParticipationPct.get(0)),
dynamicAction' = "DynamicResamplingInit",
}
action AdvanceDerivedForkSignal = {
val nextRound = dynRound + 1
all {
nextRound.in(Rounds),
DerivedReorgDepth(nextRound) >= BaseSigma,
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
firedAction' = firedAction,
dynRound' = nextRound,
dynSigma' = DynamicControllerSigma(nextRound),
controllerStatus' = ControllerStatusFor(HashParticipationPct.get(nextRound)),
dynamicAction' = "DerivedForkRaiseSigma",
}
}
action AdvanceHashParticipationSignal = {
val nextRound = dynRound + 1
all {
nextRound.in(Rounds),
DerivedReorgDepth(nextRound) < BaseSigma,
HashParticipationPct.get(nextRound) < TargetHashParticipationPct,
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
firedAction' = firedAction,
dynRound' = nextRound,
dynSigma' = DynamicControllerSigma(nextRound),
controllerStatus' = ControllerStatusFor(HashParticipationPct.get(nextRound)),
dynamicAction' =
if (HashParticipationPct.get(nextRound) < CriticalHashParticipationPct) {
"CriticalHashParticipationRaiseSigma"
} else {
"LowHashParticipationRaiseSigma"
},
}
}
action DynamicResamplingStutter = all {
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
firedAction' = firedAction,
DynamicStateUnchanged,
}
action DynamicSeedValidNil(p: Proc_t): bool =
all { SeedValidNilPrecommitState(p), DynamicStateUnchanged }
action DynamicSeedFaultyNil(p: Proc_t): bool =
all { SeedFaultyNilPrecommit(p), DynamicStateUnchanged }
action DynamicSeedSameRoundLock(p: Proc_t): bool =
all { SeedSameRoundValueLock(p), DynamicStateUnchanged }
action DynamicStartNextRound(p: Proc_t): bool =
all { StartNextRoundAfterPrecommitQuorum(p), DynamicStateUnchanged }
action DynamicInsertProposal(p: Proc_t): bool =
all { InsertProposal(p), DynamicStateUnchanged }
action DynamicPrevote(p: Proc_t, v: Snapshot_t): bool =
all { UponProposalPrevote(p, v), DynamicStateUnchanged }
action DynamicPrecommitValue(p: Proc_t, v: Snapshot_t): bool =
all { UponValuePrevoteQuorum(p, v), DynamicStateUnchanged }
action DynamicDecide(p: Proc_t, r: Round_t, v: Snapshot_t): bool =
all { Decide(p, r, v), DynamicStateUnchanged }
action DynamicResamplingNext =
any {
AdvanceDerivedForkSignal,
AdvanceHashParticipationSignal,
nondet p = oneOf(Corr)
DynamicStartNextRound(p),
nondet p = oneOf(Corr)
DynamicInsertProposal(p),
nondet p = oneOf(Corr)
nondet v = oneOf(Snapshots)
DynamicPrevote(p, v),
nondet p = oneOf(Corr)
nondet v = oneOf(Snapshots)
DynamicPrecommitValue(p, v),
nondet p = oneOf(Corr)
nondet r = oneOf(Rounds)
nondet v = oneOf(Snapshots)
DynamicDecide(p, r, v),
DynamicResamplingStutter,
}
val DynamicSigmaWithinBounds =
BaseSigma <= dynSigma and dynSigma <= MaxSigma
val DynamicSigmaRespectsDerivedReorgFloor =
dynSigma >= ReorgSigmaFloor(DerivedReorgDepth(dynRound))
val DynamicSigmaRespectsHashParticipationFloor =
dynSigma >= HashParticipationSigmaFloor(HashParticipationPct.get(dynRound))
val HashParticipationFloorIsMonotone =
Percentages.forall(lower =>
Percentages.forall(higher =>
(lower <= higher) implies
HashParticipationSigmaFloor(lower) >= HashParticipationSigmaFloor(higher)
)
)
val DynamicControllerStatusMatchesHashParticipation =
controllerStatus == ControllerStatusFor(HashParticipationPct.get(dynRound))
val CurrentDynamicBestTipMatchesGeneratedCompetition =
IsBestTipAt(dynRound, BestTip.get(dynRound))
val DynamicResamplingSafety =
Safety and
DynamicSigmaWithinBounds and
DynamicSigmaRespectsDerivedReorgFloor and
DynamicSigmaRespectsHashParticipationFloor and
HashParticipationFloorIsMonotone and
DynamicControllerStatusMatchesHashParticipation and
CurrentDynamicBestTipMatchesGeneratedCompetition
}
module CrosslinkDynamicSigmaResamplingTest {
import CrosslinkDynamicSigmaResampling.* from "./CrosslinkDynamicSigmaResampling"
export CrosslinkDynamicSigmaResampling.*
run derivedForkSignalRaisesSigmaBeforeResamplingDecisionTest = {
DynamicResamplingInit
.then(AdvanceDerivedForkSignal)
.then(all {
assert(dynSigma == RaisedSigma),
assert(controllerStatus == "healthy-hash-participation"),
assert(dynamicAction == "DerivedForkRaiseSigma"),
assert(DynamicResamplingSafety),
DynamicResamplingStutter,
})
}
run derivedForkSignalThenNilResamplingDecidesFreshValueTest = {
DynamicResamplingInit
.then(DynamicSeedValidNil("p1"))
.then(DynamicSeedValidNil("p2"))
.then(DynamicSeedFaultyNil("p4"))
.then(DynamicSeedSameRoundLock("p3"))
.then(AdvanceDerivedForkSignal)
.then(all {
assert(dynSigma == RaisedSigma),
assert(controllerStatus == "healthy-hash-participation"),
assert(dynamicAction == "DerivedForkRaiseSigma"),
assert(DynamicResamplingSafety),
DynamicResamplingStutter,
})
.then(DynamicStartNextRound("p1"))
.then(DynamicStartNextRound("p2"))
.then(DynamicStartNextRound("p3"))
.then(DynamicInsertProposal("p2"))
.then(DynamicPrevote("p1", "s1"))
.then(DynamicPrevote("p2", "s1"))
.then(DynamicPrevote("p3", "s1"))
.then(DynamicPrecommitValue("p1", "s1"))
.then(DynamicPrecommitValue("p2", "s1"))
.then(DynamicPrecommitValue("p3", "s1"))
.then(DynamicDecide("p1", 1, "s1"))
.then(all {
assert(decision.get("p1") == "s1"),
assert(dynSigma == RaisedSigma),
assert(DynamicResamplingSafety),
DynamicResamplingStutter,
})
}
run criticalHashParticipationRaisesSigmaWithoutNewForkSwitchTest = {
DynamicResamplingInit
.then(AdvanceDerivedForkSignal)
.then(AdvanceHashParticipationSignal)
.then(all {
assert(dynRound == 2),
assert(DerivedReorgDepth(dynRound) == 0),
assert(dynSigma == MaxSigma),
assert(controllerStatus == "critical-hash-participation"),
assert(dynamicAction == "CriticalHashParticipationRaiseSigma"),
assert(DynamicResamplingSafety),
DynamicResamplingStutter,
})
}
run generatedCompetitionBacksResamplingForkSignalTest = all {
assert(IsBestTipAt(0, "s0")),
assert(not(CandidateAt(0, "s1"))),
assert(IsBestTipAt(1, "s1")),
assert(DerivedReorgDepth(1) == 1),
}
}
module CrosslinkDynamicSigmaResamplingModel {
import CrosslinkDynamicSigmaResamplingTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 2,
Proposer = Map(0 -> "p1", 1 -> "p2", 2 -> "p3"),
Stream = Map(0 -> "s0", 1 -> "s1", 2 -> "s2"),
Snapshots = Set("s0", "s1", "s2"),
ResampleOnNilPrecommit = true,
BaseSigma = 1,
RaisedSigma = 3,
MaxSigma = 6,
MaxWork = 4,
TargetHashParticipationPct = 67,
CriticalHashParticipationPct = 50,
HashParticipationPct = Map(
0 -> 90,
1 -> 90,
2 -> 45,
),
BestTip = Map(
0 -> "s0",
1 -> "s1",
2 -> "s2",
),
PublishedAt = Map(
0 -> Map("s0" -> true, "s1" -> false, "s2" -> false),
1 -> Map("s0" -> true, "s1" -> true, "s2" -> false),
2 -> Map("s0" -> true, "s1" -> true, "s2" -> true),
),
HonestWorkAt = Map(
0 -> Map("s0" -> 1, "s1" -> 0, "s2" -> 0),
1 -> Map("s0" -> 1, "s1" -> 0, "s2" -> 0),
2 -> Map("s0" -> 1, "s1" -> 0, "s2" -> 0),
),
AdversarialWorkAt = Map(
0 -> Map("s0" -> 0, "s1" -> 3, "s2" -> 0),
1 -> Map("s0" -> 0, "s1" -> 3, "s2" -> 0),
2 -> Map("s0" -> 0, "s1" -> 3, "s2" -> 4),
),
TieBreakRank = Map(
"s0" -> 0,
"s1" -> 1,
"s2" -> 2,
),
HeightOf = Map(
"s0" -> 1,
"s1" -> 2,
"s2" -> 3,
),
LcaHeight = Map(
"s0" -> Map("s1" -> 0, "s2" -> 0),
"s1" -> Map("s1" -> 2, "s2" -> 2),
"s2" -> Map("s1" -> 2, "s2" -> 3),
),
).*
}
// -*- mode: Bluespec; -*-
module CrosslinkDynamicSigmaTelemetry {
/*
Production-shaped telemetry contract for the dynamic-sigma controller.
The calibration harness classifies bounded measurement windows from supplied
percentages. This model adds the contract a deployed controller should
satisfy before those percentages are trusted:
- hash-power participation is derived from Crosslink-participating work over
total observed PoW work
- rate estimates conservatively upper-bound raw coverage and round-failure
samples
- block interval variance is derived from adjacent header timestamps
- selected sigma meets explicit rollback-probability and expected-loss
targets whenever the bounded sigma ladder can meet them
*/
type Observation_t = int
type Pct_t = int
type Sigma_t = int
const MaxObservation: Observation_t
const BaseSigma: Sigma_t
const RaisedSigma: Sigma_t
const MaxSigma: Sigma_t
const TargetHashParticipationPct: Pct_t
const CriticalHashParticipationPct: Pct_t
const VerifiedHashWorkSample: Observation_t -> int
const UnverifiedHashWorkSample: Observation_t -> int
const TotalHashWork: Observation_t -> int
const CrosslinkParticipatingHashWork: Observation_t -> int
const TotalTenderlinkRounds: Observation_t -> int
const FailedTenderlinkRounds: Observation_t -> int
const NilPrecommitRecoveryRounds: Observation_t -> int
const StaleProposalRounds: Observation_t -> int
const TimeoutRounds: Observation_t -> int
const InvalidProposalRounds: Observation_t -> int
const MixedEvidenceRounds: Observation_t -> int
const DecidedTenderlinkRounds: Observation_t -> int
const EstimatedCoverageRiskPct: Observation_t -> Pct_t
const EstimatedRoundFailureRatePct: Observation_t -> Pct_t
const MeasuredBlockIntervalVariancePct: Observation_t -> Pct_t
const TargetBlockSpacingSeconds: int
const FirstHeaderTimestamp: Observation_t -> int
const SecondHeaderTimestamp: Observation_t -> int
const ThirdHeaderTimestamp: Observation_t -> int
const MeasuredObservedReorgDepth: Observation_t -> int
const PreviousBestTipHeight: Observation_t -> int
const NewBestTipHeight: Observation_t -> int
const CommonAncestorHeight: Observation_t -> int
const RollbackRiskPpmAtSigma: Observation_t -> Sigma_t -> int
const MaxAcceptableRollbackRiskPpm: int
const ValueAtRiskUnits: Observation_t -> int
const MaxAcceptableExpectedLossUnits: Observation_t -> int
const ExpectedTelemetrySigmaFloor: Observation_t -> Sigma_t
const CoverageRiskWeight: int
const RoundFailureRiskWeight: int
const BlockVarianceRiskWeight: int
const ReorgDepthRiskWeight: int
const RiskScoreRaisedThreshold: int
const RiskScoreMaxThreshold: int
pure val Observations = 0.to(MaxObservation)
pure val Percentages = 0.to(100)
pure val SigmaLadder = Set(BaseSigma, RaisedSigma, MaxSigma)
pure val PpmDenominator = 1000000
var observation: Observation_t
assume max_observation_non_negative =
MaxObservation >= 0
assume sigma_ladder_is_ordered =
1 <= BaseSigma and BaseSigma < RaisedSigma and RaisedSigma < MaxSigma
assume participation_thresholds_are_ordered =
0 <= CriticalHashParticipationPct and
CriticalHashParticipationPct < TargetHashParticipationPct and
TargetHashParticipationPct <= 100
assume hash_work_source_samples_are_well_formed =
Observations.forall(o =>
VerifiedHashWorkSample.get(o) >= 0 and UnverifiedHashWorkSample.get(o) >= 0 and
VerifiedHashWorkSample.get(o) + UnverifiedHashWorkSample.get(o) > 0
)
assume work_samples_are_well_formed =
Observations.forall(o =>
TotalHashWork.get(o) > 0 and
0 <= CrosslinkParticipatingHashWork.get(o) and
CrosslinkParticipatingHashWork.get(o) <= TotalHashWork.get(o)
)
assume round_samples_are_well_formed =
Observations.forall(o =>
TotalTenderlinkRounds.get(o) > 0 and
0 <= FailedTenderlinkRounds.get(o) and
0 <= DecidedTenderlinkRounds.get(o) and
FailedTenderlinkRounds.get(o) + DecidedTenderlinkRounds.get(o) <=
TotalTenderlinkRounds.get(o) and
0 <= NilPrecommitRecoveryRounds.get(o) and
0 <= StaleProposalRounds.get(o) and
0 <= TimeoutRounds.get(o) and
0 <= InvalidProposalRounds.get(o) and
0 <= MixedEvidenceRounds.get(o)
)
assume best_tip_transition_samples_are_well_formed =
Observations.forall(o =>
0 <= CommonAncestorHeight.get(o) and
CommonAncestorHeight.get(o) <= PreviousBestTipHeight.get(o) and
CommonAncestorHeight.get(o) <= NewBestTipHeight.get(o)
)
assume estimated_percentages_are_percentages =
Observations.forall(o =>
0 <= EstimatedCoverageRiskPct.get(o) and EstimatedCoverageRiskPct.get(o) <= 100 and
0 <= EstimatedRoundFailureRatePct.get(o) and EstimatedRoundFailureRatePct.get(o) <= 100 and
0 <= MeasuredBlockIntervalVariancePct.get(o) and
MeasuredBlockIntervalVariancePct.get(o) <= 100
)
assume header_timestamp_samples_are_well_formed =
TargetBlockSpacingSeconds > 0 and
Observations.forall(o =>
FirstHeaderTimestamp.get(o) < SecondHeaderTimestamp.get(o) and
SecondHeaderTimestamp.get(o) < ThirdHeaderTimestamp.get(o)
)
assume measured_reorg_depth_is_bounded =
Observations.forall(o =>
0 <= MeasuredObservedReorgDepth.get(o) and
MeasuredObservedReorgDepth.get(o) <= MaxSigma
)
assume rollback_risk_ppm_is_non_negative =
Observations.forall(o =>
SigmaLadder.forall(s => RollbackRiskPpmAtSigma.get(o).get(s) >= 0)
)
assume max_acceptable_risk_is_non_negative =
MaxAcceptableRollbackRiskPpm >= 0
assume economic_exposure_is_well_formed =
Observations.forall(o =>
ValueAtRiskUnits.get(o) >= 0 and MaxAcceptableExpectedLossUnits.get(o) >= 0
)
assume expected_floors_are_in_ladder =
Observations.forall(o =>
SigmaLadder.contains(ExpectedTelemetrySigmaFloor.get(o))
)
assume risk_weights_are_material =
CoverageRiskWeight > 0 and
RoundFailureRiskWeight > 0 and
BlockVarianceRiskWeight > 0 and
ReorgDepthRiskWeight > 0
assume risk_score_thresholds_are_ordered =
0 <= RiskScoreRaisedThreshold and RiskScoreRaisedThreshold < RiskScoreMaxThreshold
pure def MaxInt(a: int, b: int): int =
if (a >= b) { a } else { b }
pure def AbsDiff(a: int, b: int): int =
if (a >= b) { a - b } else { b - a }
pure def SourceTotalHashWork(o: Observation_t): int =
VerifiedHashWorkSample.get(o) + UnverifiedHashWorkSample.get(o)
pure def SourceCrosslinkParticipatingHashWork(o: Observation_t): int =
VerifiedHashWorkSample.get(o)
pure def FailureReasonRounds(o: Observation_t): int =
NilPrecommitRecoveryRounds.get(o) +
StaleProposalRounds.get(o) +
TimeoutRounds.get(o) +
InvalidProposalRounds.get(o) +
MixedEvidenceRounds.get(o)
pure def SourceRollbackDepth(o: Observation_t): int =
PreviousBestTipHeight.get(o) - CommonAncestorHeight.get(o)
pure def FirstHeaderIntervalSeconds(o: Observation_t): int =
SecondHeaderTimestamp.get(o) - FirstHeaderTimestamp.get(o)
pure def SecondHeaderIntervalSeconds(o: Observation_t): int =
ThirdHeaderTimestamp.get(o) - SecondHeaderTimestamp.get(o)
pure def SourceMaxBlockIntervalDeviationSeconds(o: Observation_t): int =
MaxInt(
AbsDiff(FirstHeaderIntervalSeconds(o), TargetBlockSpacingSeconds),
AbsDiff(SecondHeaderIntervalSeconds(o), TargetBlockSpacingSeconds)
)
pure def WorkCoverageAtLeast(o: Observation_t, pct: Pct_t): bool =
CrosslinkParticipatingHashWork.get(o) * 100 >= TotalHashWork.get(o) * pct
pure def HashParticipationSigmaFloorForObservation(o: Observation_t): Sigma_t =
if (not(WorkCoverageAtLeast(o, CriticalHashParticipationPct))) {
MaxSigma
} else if (not(WorkCoverageAtLeast(o, TargetHashParticipationPct))) {
RaisedSigma
} else {
BaseSigma
}
pure def ReorgSigmaFloor(depth: int): Sigma_t =
if (depth >= RaisedSigma) {
MaxSigma
} else if (depth >= BaseSigma) {
RaisedSigma
} else {
BaseSigma
}
pure def CalibratedRiskScoreForObservation(o: Observation_t): int =
CoverageRiskWeight * EstimatedCoverageRiskPct.get(o) +
RoundFailureRiskWeight * EstimatedRoundFailureRatePct.get(o) +
BlockVarianceRiskWeight * MeasuredBlockIntervalVariancePct.get(o) +
ReorgDepthRiskWeight * MeasuredObservedReorgDepth.get(o)
pure def RiskScoreSigmaFloor(score: int): Sigma_t =
if (score >= RiskScoreMaxThreshold) {
MaxSigma
} else if (score >= RiskScoreRaisedThreshold) {
RaisedSigma
} else {
BaseSigma
}
pure def EconomicTargetSatisfiedAtSigma(o: Observation_t, s: Sigma_t): bool =
RollbackRiskPpmAtSigma.get(o).get(s) <= MaxAcceptableRollbackRiskPpm and
RollbackRiskPpmAtSigma.get(o).get(s) * ValueAtRiskUnits.get(o) <=
MaxAcceptableExpectedLossUnits.get(o) * PpmDenominator
pure def EconomicSigmaFloorForObservation(o: Observation_t): Sigma_t =
if (EconomicTargetSatisfiedAtSigma(o, BaseSigma)) {
BaseSigma
} else if (EconomicTargetSatisfiedAtSigma(o, RaisedSigma)) {
RaisedSigma
} else {
MaxSigma
}
pure def TelemetrySigmaFloorForObservation(o: Observation_t): Sigma_t =
MaxInt(
EconomicSigmaFloorForObservation(o),
MaxInt(
RiskScoreSigmaFloor(CalibratedRiskScoreForObservation(o)),
MaxInt(
HashParticipationSigmaFloorForObservation(o),
ReorgSigmaFloor(MeasuredObservedReorgDepth.get(o))
)
)
)
pure def EconomicTargetStatusForObservation(o: Observation_t): str =
if (EconomicTargetSatisfiedAtSigma(o, TelemetrySigmaFloorForObservation(o))) {
"target-satisfied"
} else {
"target-unreachable-at-max"
}
val SourceHashWorkMatchesTelemetryComponents =
Observations.forall(o =>
TotalHashWork.get(o) == SourceTotalHashWork(o) and
CrosslinkParticipatingHashWork.get(o) == SourceCrosslinkParticipatingHashWork(o)
)
val RoundCountersAreInternallyConsistent =
Observations.forall(o =>
FailureReasonRounds(o) <= FailedTenderlinkRounds.get(o) and
FailedTenderlinkRounds.get(o) + DecidedTenderlinkRounds.get(o) <=
TotalTenderlinkRounds.get(o)
)
val SourceRollbackDepthMatchesTelemetryComponents =
Observations.forall(o =>
MeasuredObservedReorgDepth.get(o) == SourceRollbackDepth(o)
)
val SourceBlockIntervalVarianceMatchesTelemetryComponents =
Observations.forall(o =>
TargetBlockSpacingSeconds * MeasuredBlockIntervalVariancePct.get(o) >=
SourceMaxBlockIntervalDeviationSeconds(o) * 100 and
if (MeasuredBlockIntervalVariancePct.get(o) == 0) {
SourceMaxBlockIntervalDeviationSeconds(o) == 0
} else if (MeasuredBlockIntervalVariancePct.get(o) < 100) {
TargetBlockSpacingSeconds * (MeasuredBlockIntervalVariancePct.get(o) - 1) <
SourceMaxBlockIntervalDeviationSeconds(o) * 100
} else {
true
}
)
val CoverageEstimateUpperBoundsRawWorkGap =
Observations.forall(o =>
TotalHashWork.get(o) * EstimatedCoverageRiskPct.get(o) >=
(TotalHashWork.get(o) - CrosslinkParticipatingHashWork.get(o)) * 100
)
val RoundFailureEstimateUpperBoundsRawFailures =
Observations.forall(o =>
TotalTenderlinkRounds.get(o) * EstimatedRoundFailureRatePct.get(o) >=
FailedTenderlinkRounds.get(o) * 100
)
val RollbackRiskCurveIsMonotoneAcrossSigma =
Observations.forall(o =>
RollbackRiskPpmAtSigma.get(o).get(BaseSigma) >=
RollbackRiskPpmAtSigma.get(o).get(RaisedSigma) and
RollbackRiskPpmAtSigma.get(o).get(RaisedSigma) >=
RollbackRiskPpmAtSigma.get(o).get(MaxSigma)
)
val EconomicTargetIsSatisfiedWhenReachable =
Observations.forall(o =>
if (EconomicTargetSatisfiedAtSigma(o, MaxSigma)) {
EconomicTargetSatisfiedAtSigma(o, TelemetrySigmaFloorForObservation(o))
} else {
TelemetrySigmaFloorForObservation(o) == MaxSigma and
EconomicTargetStatusForObservation(o) == "target-unreachable-at-max"
}
)
val HashParticipationFloorIsMonotoneInWorkCoverage =
Observations.forall(o =>
if (WorkCoverageAtLeast(o, TargetHashParticipationPct)) {
HashParticipationSigmaFloorForObservation(o) == BaseSigma
} else if (WorkCoverageAtLeast(o, CriticalHashParticipationPct)) {
HashParticipationSigmaFloorForObservation(o) == RaisedSigma
} else {
HashParticipationSigmaFloorForObservation(o) == MaxSigma
}
)
val TelemetryMatchesExpectedSigmaLabels =
Observations.forall(o =>
TelemetrySigmaFloorForObservation(o) == ExpectedTelemetrySigmaFloor.get(o)
)
val TelemetryCalibrationSafety =
SourceHashWorkMatchesTelemetryComponents and
RoundCountersAreInternallyConsistent and
SourceRollbackDepthMatchesTelemetryComponents and
SourceBlockIntervalVarianceMatchesTelemetryComponents and
CoverageEstimateUpperBoundsRawWorkGap and
RoundFailureEstimateUpperBoundsRawFailures and
RollbackRiskCurveIsMonotoneAcrossSigma and
EconomicTargetIsSatisfiedWhenReachable and
HashParticipationFloorIsMonotoneInWorkCoverage and
TelemetryMatchesExpectedSigmaLabels
val CurrentObservationMatchesTelemetry =
TelemetrySigmaFloorForObservation(observation) == ExpectedTelemetrySigmaFloor.get(observation)
action Init = all {
observation' = 0,
}
action AdvanceObservation = {
val nextObservation = observation + 1
all {
nextObservation.in(Observations),
observation' = nextObservation,
}
}
action Stutter = all {
observation' = observation,
}
action Next =
any {
AdvanceObservation,
Stutter,
}
val Safety =
observation.in(Observations) and
TelemetryCalibrationSafety and
CurrentObservationMatchesTelemetry
}
module CrosslinkDynamicSigmaTelemetryTest {
import CrosslinkDynamicSigmaTelemetry.* from "./CrosslinkDynamicSigmaTelemetry"
export CrosslinkDynamicSigmaTelemetry.*
run healthyTelemetryWindowKeepsBaseSigmaTest = all {
assert(TelemetrySigmaFloorForObservation(0) == BaseSigma),
assert(EconomicTargetStatusForObservation(0) == "target-satisfied"),
}
run sourceHashWorkDerivesTelemetryComponentsTest = all {
assert(SourceHashWorkMatchesTelemetryComponents),
assert(SourceTotalHashWork(1) == 100),
assert(SourceCrosslinkParticipatingHashWork(1) == 63),
assert(TelemetrySigmaFloorForObservation(1) == RaisedSigma),
}
run sourceRoundCountersAreConsistentTest = all {
assert(RoundCountersAreInternallyConsistent),
assert(FailureReasonRounds(2) == FailedTenderlinkRounds.get(2)),
}
run sourceRollbackDepthDerivesTelemetryComponentsTest = all {
assert(SourceRollbackDepthMatchesTelemetryComponents),
assert(SourceRollbackDepth(7) == 3),
assert(TelemetrySigmaFloorForObservation(7) == MaxSigma),
}
run sourceBlockIntervalVarianceDerivesTelemetryComponentsTest = all {
assert(SourceBlockIntervalVarianceMatchesTelemetryComponents),
assert(SourceMaxBlockIntervalDeviationSeconds(2) == 15),
assert(MeasuredBlockIntervalVariancePct.get(2) == 15),
assert(CalibratedRiskScoreForObservation(2) >= RiskScoreRaisedThreshold),
assert(TelemetrySigmaFloorForObservation(2) == RaisedSigma),
}
run hashWorkParticipationRaisesSigmaTest = all {
assert(WorkCoverageAtLeast(1, CriticalHashParticipationPct)),
assert(not(WorkCoverageAtLeast(1, TargetHashParticipationPct))),
assert(HashParticipationSigmaFloorForObservation(1) == RaisedSigma),
assert(TelemetrySigmaFloorForObservation(1) == RaisedSigma),
}
run combinedTelemetryRiskRaisesSigmaTest = all {
assert(WorkCoverageAtLeast(2, TargetHashParticipationPct)),
assert(CalibratedRiskScoreForObservation(2) >= RiskScoreRaisedThreshold),
assert(CalibratedRiskScoreForObservation(2) < RiskScoreMaxThreshold),
assert(TelemetrySigmaFloorForObservation(2) == RaisedSigma),
}
run economicTargetRaisesSigmaAboveSignalFloorTest = all {
assert(HashParticipationSigmaFloorForObservation(3) == BaseSigma),
assert(RiskScoreSigmaFloor(CalibratedRiskScoreForObservation(3)) == BaseSigma),
assert(EconomicSigmaFloorForObservation(3) == RaisedSigma),
assert(TelemetrySigmaFloorForObservation(3) == RaisedSigma),
}
run criticalParticipationForcesMaxSigmaTest = all {
assert(not(WorkCoverageAtLeast(4, CriticalHashParticipationPct))),
assert(TelemetrySigmaFloorForObservation(4) == MaxSigma),
}
run economicTargetCanForceMaxSigmaTest = all {
assert(EconomicSigmaFloorForObservation(5) == MaxSigma),
assert(EconomicTargetStatusForObservation(5) == "target-satisfied"),
assert(TelemetrySigmaFloorForObservation(5) == MaxSigma),
}
run unreachableEconomicTargetFallsBackToMaxSigmaTest = all {
assert(EconomicSigmaFloorForObservation(6) == MaxSigma),
assert(EconomicTargetStatusForObservation(6) == "target-unreachable-at-max"),
assert(TelemetrySigmaFloorForObservation(6) == MaxSigma),
}
run deepReorgTelemetryWindowForcesMaxSigmaTest = all {
assert(MeasuredObservedReorgDepth.get(7) >= RaisedSigma),
assert(TelemetrySigmaFloorForObservation(7) == MaxSigma),
}
run expectedLossBudgetRaisesSigmaEvenWithinPpmTargetTest = all {
assert(RollbackRiskPpmAtSigma.get(8).get(BaseSigma) <= MaxAcceptableRollbackRiskPpm),
assert(not(EconomicTargetSatisfiedAtSigma(8, BaseSigma))),
assert(EconomicTargetSatisfiedAtSigma(8, RaisedSigma)),
assert(EconomicSigmaFloorForObservation(8) == RaisedSigma),
assert(TelemetrySigmaFloorForObservation(8) == RaisedSigma),
}
run telemetryMatchesAllExpectedWindowsTest = {
assert(TelemetryCalibrationSafety)
}
}
module CrosslinkDynamicSigmaTelemetryModel {
import CrosslinkDynamicSigmaTelemetryTest(
MaxObservation = 8,
BaseSigma = 1,
RaisedSigma = 3,
MaxSigma = 6,
TargetHashParticipationPct = 67,
CriticalHashParticipationPct = 50,
VerifiedHashWorkSample = Map(
0 -> 90,
1 -> 63,
2 -> 75,
3 -> 90,
4 -> 45,
5 -> 90,
6 -> 90,
7 -> 90,
8 -> 90,
),
UnverifiedHashWorkSample = Map(
0 -> 10,
1 -> 37,
2 -> 25,
3 -> 10,
4 -> 55,
5 -> 10,
6 -> 10,
7 -> 10,
8 -> 10,
),
TotalHashWork = Map(
0 -> 100,
1 -> 100,
2 -> 100,
3 -> 100,
4 -> 100,
5 -> 100,
6 -> 100,
7 -> 100,
8 -> 100,
),
CrosslinkParticipatingHashWork = Map(
0 -> 90,
1 -> 63,
2 -> 75,
3 -> 90,
4 -> 45,
5 -> 90,
6 -> 90,
7 -> 90,
8 -> 90,
),
TotalTenderlinkRounds = Map(
0 -> 100,
1 -> 100,
2 -> 100,
3 -> 100,
4 -> 100,
5 -> 100,
6 -> 100,
7 -> 100,
8 -> 100,
),
FailedTenderlinkRounds = Map(
0 -> 0,
1 -> 0,
2 -> 15,
3 -> 0,
4 -> 0,
5 -> 0,
6 -> 0,
7 -> 0,
8 -> 0,
),
NilPrecommitRecoveryRounds = Map(
0 -> 0,
1 -> 0,
2 -> 5,
3 -> 0,
4 -> 0,
5 -> 0,
6 -> 0,
7 -> 0,
8 -> 0,
),
StaleProposalRounds = Map(
0 -> 0,
1 -> 0,
2 -> 10,
3 -> 0,
4 -> 0,
5 -> 0,
6 -> 0,
7 -> 0,
8 -> 0,
),
TimeoutRounds = Map(
0 -> 0,
1 -> 0,
2 -> 0,
3 -> 0,
4 -> 0,
5 -> 0,
6 -> 0,
7 -> 0,
8 -> 0,
),
InvalidProposalRounds = Map(
0 -> 0,
1 -> 0,
2 -> 0,
3 -> 0,
4 -> 0,
5 -> 0,
6 -> 0,
7 -> 0,
8 -> 0,
),
MixedEvidenceRounds = Map(
0 -> 0,
1 -> 0,
2 -> 0,
3 -> 0,
4 -> 0,
5 -> 0,
6 -> 0,
7 -> 0,
8 -> 0,
),
DecidedTenderlinkRounds = Map(
0 -> 100,
1 -> 100,
2 -> 85,
3 -> 100,
4 -> 100,
5 -> 100,
6 -> 100,
7 -> 100,
8 -> 100,
),
EstimatedCoverageRiskPct = Map(
0 -> 10,
1 -> 37,
2 -> 25,
3 -> 10,
4 -> 55,
5 -> 10,
6 -> 10,
7 -> 10,
8 -> 10,
),
EstimatedRoundFailureRatePct = Map(
0 -> 0,
1 -> 0,
2 -> 15,
3 -> 0,
4 -> 0,
5 -> 0,
6 -> 0,
7 -> 0,
8 -> 0,
),
TargetBlockSpacingSeconds = 100,
FirstHeaderTimestamp = Map(
0 -> 0,
1 -> 0,
2 -> 0,
3 -> 0,
4 -> 0,
5 -> 0,
6 -> 0,
7 -> 0,
8 -> 0,
),
SecondHeaderTimestamp = Map(
0 -> 100,
1 -> 100,
2 -> 100,
3 -> 100,
4 -> 100,
5 -> 100,
6 -> 100,
7 -> 100,
8 -> 100,
),
ThirdHeaderTimestamp = Map(
0 -> 205,
1 -> 210,
2 -> 215,
3 -> 205,
4 -> 200,
5 -> 205,
6 -> 205,
7 -> 205,
8 -> 205,
),
MeasuredBlockIntervalVariancePct = Map(
0 -> 5,
1 -> 10,
2 -> 15,
3 -> 5,
4 -> 0,
5 -> 5,
6 -> 5,
7 -> 5,
8 -> 5,
),
MeasuredObservedReorgDepth = Map(
0 -> 0,
1 -> 0,
2 -> 0,
3 -> 0,
4 -> 0,
5 -> 0,
6 -> 0,
7 -> 3,
8 -> 0,
),
PreviousBestTipHeight = Map(
0 -> 100,
1 -> 100,
2 -> 100,
3 -> 100,
4 -> 100,
5 -> 100,
6 -> 100,
7 -> 105,
8 -> 100,
),
NewBestTipHeight = Map(
0 -> 101,
1 -> 101,
2 -> 101,
3 -> 101,
4 -> 101,
5 -> 101,
6 -> 101,
7 -> 107,
8 -> 101,
),
CommonAncestorHeight = Map(
0 -> 100,
1 -> 100,
2 -> 100,
3 -> 100,
4 -> 100,
5 -> 100,
6 -> 100,
7 -> 102,
8 -> 100,
),
RollbackRiskPpmAtSigma = Map(
0 -> Map(1 -> 40, 3 -> 10, 6 -> 1),
1 -> Map(1 -> 80, 3 -> 20, 6 -> 2),
2 -> Map(1 -> 80, 3 -> 25, 6 -> 3),
3 -> Map(1 -> 250, 3 -> 80, 6 -> 10),
4 -> Map(1 -> 500, 3 -> 250, 6 -> 70),
5 -> Map(1 -> 500, 3 -> 180, 6 -> 70),
6 -> Map(1 -> 500, 3 -> 300, 6 -> 120),
7 -> Map(1 -> 400, 3 -> 180, 6 -> 60),
8 -> Map(1 -> 80, 3 -> 5, 6 -> 1),
),
MaxAcceptableRollbackRiskPpm = 100,
ValueAtRiskUnits = Map(
0 -> 1000,
1 -> 1000,
2 -> 1000,
3 -> 1000,
4 -> 1000,
5 -> 1000,
6 -> 1000,
7 -> 1000,
8 -> 100000,
),
MaxAcceptableExpectedLossUnits = Map(
0 -> 100,
1 -> 100,
2 -> 100,
3 -> 100,
4 -> 100,
5 -> 100,
6 -> 100,
7 -> 100,
8 -> 1,
),
ExpectedTelemetrySigmaFloor = Map(
0 -> 1,
1 -> 3,
2 -> 3,
3 -> 3,
4 -> 6,
5 -> 6,
6 -> 6,
7 -> 6,
8 -> 3,
),
CoverageRiskWeight = 2,
RoundFailureRiskWeight = 3,
BlockVarianceRiskWeight = 2,
ReorgDepthRiskWeight = 60,
RiskScoreRaisedThreshold = 100,
RiskScoreMaxThreshold = 220,
).*
}
module CrosslinkForkFinality {
/*
A small value-semantics model for Crosslink finality.
This module is intentionally independent of the Tenderlink round machine.
It models what a BFT decision is allowed to finalize: a tail-confirmed PoW
snapshot that extends the current finalized prefix. This lets the protocol
skip PoW heights on a single branch, while rejecting rollback to a fork after
a block is final.
*/
type Snapshot_t = str
type Block_t = str
type Height_t = int
const Snapshots: Set[Snapshot_t]
const MaxHeight: Height_t
const Sigma: int
const InitialFinalized: Snapshot_t
const HeightOf: Snapshot_t -> Height_t
const AncestorAt: Snapshot_t -> Height_t -> Block_t
assume initial_is_snapshot = Snapshots.contains(InitialFinalized)
assume sigma_is_positive = Sigma >= 1
pure val Heights = 0.to(MaxHeight)
var latestFinal: Snapshot_t
var finalized: Set[Snapshot_t]
var firedAction: str
pure def IsSnapshot(v: Snapshot_t): bool =
Snapshots.contains(v)
pure def Height(v: Snapshot_t): Height_t =
HeightOf.get(v)
pure def BlockAt(v: Snapshot_t, h: Height_t): Block_t =
AncestorAt.get(v).get(h)
def Extends(newer: Snapshot_t, older: Snapshot_t): bool =
IsSnapshot(newer) and
IsSnapshot(older) and
Height(newer) >= Height(older) and
Heights.forall(h =>
h > Height(older) or BlockAt(newer, h) == BlockAt(older, h)
)
def Agrees(a: Snapshot_t, b: Snapshot_t): bool =
Extends(a, b) or Extends(b, a)
def TailConfirms(tip: Snapshot_t, candidate: Snapshot_t): bool =
IsSnapshot(tip) and
IsSnapshot(candidate) and
Height(tip) >= Height(candidate) + Sigma and
BlockAt(tip, Height(candidate)) == BlockAt(candidate, Height(candidate))
def ValidFinalityCandidate(candidate: Snapshot_t, tip: Snapshot_t, prevFinal: Snapshot_t): bool =
Extends(candidate, prevFinal) and
Height(candidate) > Height(prevFinal) and
TailConfirms(tip, candidate)
action Init = all {
latestFinal' = InitialFinalized,
finalized' = Set(InitialFinalized),
firedAction' = "Init",
}
action FinalizeCandidate(candidate: Snapshot_t, tip: Snapshot_t): bool = all {
ValidFinalityCandidate(candidate, tip, latestFinal),
latestFinal' = candidate,
finalized' = finalized.union(Set(candidate)),
firedAction' = "FinalizeCandidate",
}
action Stutter = all {
latestFinal' = latestFinal,
finalized' = finalized,
firedAction' = "Stutter",
}
action Next =
any {
nondet candidate = oneOf(Snapshots)
nondet tip = oneOf(Snapshots)
FinalizeCandidate(candidate, tip),
Stutter,
}
val FinalizedPrefixLinear =
tuples(finalized, finalized).forall(((a, b)) =>
Agrees(a, b)
)
val LatestFinalExtendsAllFinalized =
finalized.forall(v => Extends(latestFinal, v))
val InitialFinalizedRemainsFinalized =
finalized.contains(InitialFinalized)
val Safety =
FinalizedPrefixLinear and
LatestFinalExtendsAllFinalized and
InitialFinalizedRemainsFinalized
}
module CrosslinkForkFinalityTest {
import CrosslinkForkFinality.* from "./CrosslinkForkFinality"
export CrosslinkForkFinality.*
action unchangedAll = all {
latestFinal' = latestFinal,
finalized' = finalized,
firedAction' = firedAction,
}
run canSkipHeightsOnSamePowBranchTest = {
Init
.then(FinalizeCandidate("a2", "a3"))
.then(all {
assert(latestFinal == "a2"),
assert(finalized.contains("a2")),
unchangedAll,
})
}
run extendsFinalizedPrefixTest = {
Init
.then(FinalizeCandidate("a1", "a2"))
.then(FinalizeCandidate("a2", "a3"))
.then(all {
assert(latestFinal == "a2"),
assert(FinalizedPrefixLinear),
unchangedAll,
})
}
run rejectsFinalizingForkAfterFinalBlockTest = {
Init
.then(FinalizeCandidate("a1", "a2"))
.then(FinalizeCandidate("b2", "b3"))
.fail()
}
run rejectsUnconfirmedTailTest = {
Init
.then(FinalizeCandidate("a2", "a2"))
.fail()
}
}
module CrosslinkForkFinalityModel {
import CrosslinkForkFinalityTest(
Snapshots = Set("g", "a1", "a2", "a3", "b1", "b2", "b3"),
MaxHeight = 3,
Sigma = 1,
InitialFinalized = "g",
HeightOf = Map(
"g" -> 0,
"a1" -> 1,
"a2" -> 2,
"a3" -> 3,
"b1" -> 1,
"b2" -> 2,
"b3" -> 3,
),
AncestorAt = Map(
"g" -> Map(0 -> "g", 1 -> "None", 2 -> "None", 3 -> "None"),
"a1" -> Map(0 -> "g", 1 -> "a1", 2 -> "None", 3 -> "None"),
"a2" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "None"),
"a3" -> Map(0 -> "g", 1 -> "a1", 2 -> "a2", 3 -> "a3"),
"b1" -> Map(0 -> "g", 1 -> "b1", 2 -> "None", 3 -> "None"),
"b2" -> Map(0 -> "g", 1 -> "b1", 2 -> "b2", 3 -> "None"),
"b3" -> Map(0 -> "g", 1 -> "b1", 2 -> "b2", 3 -> "b3"),
),
).*
}
// -*- mode: Bluespec; -*-
module CrosslinkPowBranchCompetition {
/*
A bounded PoW branch-competition model for Crosslink.
CrosslinkPowForkSchedule constrains a supplied BestTip map. This model adds
the missing competition layer: each round has published tips, honest work,
and adversarial work. The selected best tip must be a published candidate
with maximal total work, using a fixture rank only to break exact ties.
*/
type Round_t = int
type Height_t = int
type Work_t = int
type Snapshot_t = str
const MaxRound: Round_t
const MaxHeight: Height_t
const MaxWork: Work_t
const Sigma: int
const Snapshots: Set[Snapshot_t]
const BestTip: Round_t -> Snapshot_t
const PublishedAt: Round_t -> Snapshot_t -> bool
const HonestWorkAt: Round_t -> Snapshot_t -> Work_t
const AdversarialWorkAt: Round_t -> Snapshot_t -> Work_t
const TieBreakRank: Snapshot_t -> int
const HeightOf: Snapshot_t -> Height_t
const LcaHeight: Snapshot_t -> Snapshot_t -> Height_t
pure val Rounds = 0.to(MaxRound)
pure def Height(v: Snapshot_t): Height_t =
HeightOf.get(v)
pure def TotalWorkAt(r: Round_t, v: Snapshot_t): Work_t =
HonestWorkAt.get(r).get(v) + AdversarialWorkAt.get(r).get(v)
pure def CandidateAt(r: Round_t, v: Snapshot_t): bool =
Snapshots.contains(v) and PublishedAt.get(r).get(v)
pure def BetterOrEqualAt(r: Round_t, a: Snapshot_t, b: Snapshot_t): bool =
TotalWorkAt(r, a) > TotalWorkAt(r, b) or
(
TotalWorkAt(r, a) == TotalWorkAt(r, b) and
TieBreakRank.get(a) >= TieBreakRank.get(b)
)
pure def IsBestTipAt(r: Round_t, v: Snapshot_t): bool =
CandidateAt(r, v) and
Snapshots.forall(w =>
not(CandidateAt(r, w)) or BetterOrEqualAt(r, v, w)
)
pure def BestTipAt(r: Round_t): Snapshot_t =
BestTip.get(r)
pure def CommonAncestorHeight(previousTip: Snapshot_t, nextTip: Snapshot_t): Height_t =
LcaHeight.get(previousTip).get(nextTip)
pure def RollbackDepth(previousTip: Snapshot_t, nextTip: Snapshot_t): int =
Height(previousTip) - CommonAncestorHeight(previousTip, nextTip)
pure def RoundRollbackDepth(r: Round_t): int =
if (r == 0) {
0
} else {
RollbackDepth(BestTipAt(r - 1), BestTipAt(r))
}
pure def SampleSurvivesRollback(previousTip: Snapshot_t, nextTip: Snapshot_t, sigma: int): bool =
RollbackDepth(previousTip, nextTip) < sigma
assume max_round_non_negative =
MaxRound >= 0
assume max_height_non_negative =
MaxHeight >= 0
assume max_work_non_negative =
MaxWork >= 0
assume sigma_is_positive =
Sigma >= 1
assume best_tips_are_snapshots =
Rounds.forall(r => Snapshots.contains(BestTipAt(r)))
assume generated_best_tips_match_work_competition =
Rounds.forall(r => IsBestTipAt(r, BestTipAt(r)))
assume published_work_is_bounded =
Rounds.forall(r =>
Snapshots.forall(v =>
0 <= HonestWorkAt.get(r).get(v) and
0 <= AdversarialWorkAt.get(r).get(v) and
TotalWorkAt(r, v) <= MaxWork
)
)
assume best_tip_heights_are_bounded =
Rounds.forall(r =>
0 <= Height(BestTipAt(r)) and Height(BestTipAt(r)) <= MaxHeight
)
assume adjacent_lca_heights_are_bounded =
Rounds.forall(r =>
if (r == 0) {
true
} else {
val lca = CommonAncestorHeight(BestTipAt(r - 1), BestTipAt(r))
all {
0 <= lca,
lca <= Height(BestTipAt(r - 1)),
lca <= Height(BestTipAt(r)),
}
}
)
var round: Round_t
var bestTip: Snapshot_t
var bestTipWork: Work_t
var observedRollbackDepth: int
var firedAction: str
action Init = all {
round' = 0,
bestTip' = BestTipAt(0),
bestTipWork' = TotalWorkAt(0, BestTipAt(0)),
observedRollbackDepth' = 0,
firedAction' = "Init",
}
action AdvanceRound = {
val nextRound = round + 1
all {
nextRound.in(Rounds),
round' = nextRound,
bestTip' = BestTipAt(nextRound),
bestTipWork' = TotalWorkAt(nextRound, BestTipAt(nextRound)),
observedRollbackDepth' = RoundRollbackDepth(nextRound),
firedAction' =
if (RoundRollbackDepth(nextRound) == 0) {
"AdvanceSameBranch"
} else {
"AdvanceAdversarialForkSwitch"
},
}
}
action Stutter = all {
round' = round,
bestTip' = bestTip,
bestTipWork' = bestTipWork,
observedRollbackDepth' = observedRollbackDepth,
firedAction' = firedAction,
}
action Next =
any {
AdvanceRound,
Stutter,
}
val CurrentTipMatchesGeneratedCompetition =
bestTip == BestTipAt(round) and IsBestTipAt(round, bestTip)
val CurrentWorkMatchesGeneratedCompetition =
bestTipWork == TotalWorkAt(round, bestTip)
val CurrentRollbackDepthMatchesGeneratedCompetition =
observedRollbackDepth == RoundRollbackDepth(round)
val CurrentRollbackDepthIsBounded =
0 <= observedRollbackDepth and observedRollbackDepth <= MaxHeight
val Safety =
CurrentTipMatchesGeneratedCompetition and
CurrentWorkMatchesGeneratedCompetition and
CurrentRollbackDepthMatchesGeneratedCompetition and
CurrentRollbackDepthIsBounded
}
module CrosslinkPowBranchCompetitionTest {
import CrosslinkPowBranchCompetition.* from "./CrosslinkPowBranchCompetition"
export CrosslinkPowBranchCompetition.*
run hiddenAdversarialWorkDoesNotWinUntilPublishedTest = all {
assert(not(CandidateAt(1, "b4"))),
assert(IsBestTipAt(1, "a4")),
assert(CandidateAt(2, "b4")),
assert(IsBestTipAt(2, "b4")),
}
run releasedAdversarialBranchOutworksHonestTipTest = {
assert(TotalWorkAt(2, "b4") > TotalWorkAt(2, "a4"))
}
run generatedCompetitionDerivesRollbackDepthTest = {
assert(RollbackDepth(BestTipAt(1), BestTipAt(2)) == 2)
}
run raisedSigmaSurvivesGeneratedAdversarialSwitchTest = all {
assert(not(SampleSurvivesRollback(BestTipAt(1), BestTipAt(2), 1))),
assert(SampleSurvivesRollback(BestTipAt(1), BestTipAt(2), Sigma)),
}
run adversarialCatchupProducesForkSwitchTest = {
Init
.then(AdvanceRound)
.then(all {
assert(bestTip == "a4"),
assert(bestTipWork == 4),
assert(observedRollbackDepth == 0),
assert(firedAction == "AdvanceSameBranch"),
assert(Safety),
Stutter,
})
.then(AdvanceRound)
.then(all {
assert(bestTip == "b4"),
assert(bestTipWork == 5),
assert(observedRollbackDepth == 2),
assert(firedAction == "AdvanceAdversarialForkSwitch"),
assert(Safety),
Stutter,
})
}
}
module CrosslinkPowBranchCompetitionModel {
import CrosslinkPowBranchCompetitionTest(
MaxRound = 2,
MaxHeight = 4,
MaxWork = 5,
Sigma = 3,
Snapshots = Set("g", "a3", "a4", "b4"),
BestTip = Map(
0 -> "a3",
1 -> "a4",
2 -> "b4",
),
PublishedAt = Map(
0 -> Map("g" -> true, "a3" -> true, "a4" -> false, "b4" -> false),
1 -> Map("g" -> true, "a3" -> true, "a4" -> true, "b4" -> false),
2 -> Map("g" -> true, "a3" -> true, "a4" -> true, "b4" -> true),
),
HonestWorkAt = Map(
0 -> Map("g" -> 0, "a3" -> 3, "a4" -> 0, "b4" -> 0),
1 -> Map("g" -> 0, "a3" -> 3, "a4" -> 4, "b4" -> 0),
2 -> Map("g" -> 0, "a3" -> 3, "a4" -> 4, "b4" -> 0),
),
AdversarialWorkAt = Map(
0 -> Map("g" -> 0, "a3" -> 0, "a4" -> 0, "b4" -> 4),
1 -> Map("g" -> 0, "a3" -> 0, "a4" -> 0, "b4" -> 4),
2 -> Map("g" -> 0, "a3" -> 0, "a4" -> 0, "b4" -> 5),
),
TieBreakRank = Map(
"g" -> 0,
"a3" -> 1,
"a4" -> 2,
"b4" -> 3,
),
HeightOf = Map(
"g" -> 0,
"a3" -> 3,
"a4" -> 4,
"b4" -> 4,
),
LcaHeight = Map(
"g" -> Map("g" -> 0, "a3" -> 0, "a4" -> 0, "b4" -> 0),
"a3" -> Map("g" -> 0, "a3" -> 3, "a4" -> 3, "b4" -> 2),
"a4" -> Map("g" -> 0, "a3" -> 3, "a4" -> 4, "b4" -> 2),
"b4" -> Map("g" -> 0, "a3" -> 2, "a4" -> 2, "b4" -> 4),
),
).*
}
// -*- mode: Bluespec; -*-
module CrosslinkPowForkSchedule {
/*
A focused PoW fork-schedule model for dynamic sigma.
CrosslinkDynamicSigma currently accepts an observed reorg-depth schedule as a
controller input. This model derives that depth from a bounded sequence of PoW
best-tip changes, so the next composition step can feed a generated rollback
signal into the dynamic-sigma controller.
*/
type Round_t = int
type Height_t = int
type Snapshot_t = str
const MaxRound: Round_t
const MaxHeight: Height_t
const Sigma: int
const Snapshots: Set[Snapshot_t]
const BestTip: Round_t -> Snapshot_t
const HeightOf: Snapshot_t -> Height_t
const LcaHeight: Snapshot_t -> Snapshot_t -> Height_t
pure val Rounds = 0.to(MaxRound)
assume max_round_non_negative =
MaxRound >= 0
assume max_height_non_negative =
MaxHeight >= 0
assume sigma_is_positive =
Sigma >= 1
assume best_tips_are_snapshots =
Rounds.forall(r => Snapshots.contains(BestTip.get(r)))
assume best_tip_heights_are_bounded =
Rounds.forall(r =>
0 <= HeightOf.get(BestTip.get(r)) and
HeightOf.get(BestTip.get(r)) <= MaxHeight
)
assume adjacent_lca_heights_are_bounded =
Rounds.forall(r =>
if (r == 0) {
true
} else {
val lca = LcaHeight.get(BestTip.get(r - 1)).get(BestTip.get(r))
all {
0 <= lca,
lca <= HeightOf.get(BestTip.get(r - 1)),
lca <= HeightOf.get(BestTip.get(r)),
}
}
)
var round: Round_t
var bestTip: Snapshot_t
var observedRollbackDepth: int
var firedAction: str
pure def Height(v: Snapshot_t): Height_t =
HeightOf.get(v)
pure def CommonAncestorHeight(previousTip: Snapshot_t, nextTip: Snapshot_t): Height_t =
LcaHeight.get(previousTip).get(nextTip)
pure def RollbackDepth(previousTip: Snapshot_t, nextTip: Snapshot_t): int =
Height(previousTip) - CommonAncestorHeight(previousTip, nextTip)
pure def RoundRollbackDepth(r: Round_t): int =
if (r == 0) {
0
} else {
RollbackDepth(BestTip.get(r - 1), BestTip.get(r))
}
pure def SampleSurvivesRollback(previousTip: Snapshot_t, nextTip: Snapshot_t, sigma: int): bool =
RollbackDepth(previousTip, nextTip) < sigma
action Init = all {
round' = 0,
bestTip' = BestTip.get(0),
observedRollbackDepth' = 0,
firedAction' = "Init",
}
action AdvanceRound = {
val nextRound = round + 1
all {
nextRound.in(Rounds),
round' = nextRound,
bestTip' = BestTip.get(nextRound),
observedRollbackDepth' = RoundRollbackDepth(nextRound),
firedAction' =
if (RoundRollbackDepth(nextRound) == 0) {
"AdvanceSameBranch"
} else {
"AdvanceForkSwitch"
},
}
}
action Stutter = all {
round' = round,
bestTip' = bestTip,
observedRollbackDepth' = observedRollbackDepth,
firedAction' = firedAction,
}
action Next =
any {
AdvanceRound,
Stutter,
}
val CurrentTipMatchesSchedule =
bestTip == BestTip.get(round)
val CurrentRollbackDepthMatchesSchedule =
observedRollbackDepth == RoundRollbackDepth(round)
val CurrentRollbackDepthIsBounded =
0 <= observedRollbackDepth and observedRollbackDepth <= MaxHeight
val Safety =
CurrentTipMatchesSchedule and
CurrentRollbackDepthMatchesSchedule and
CurrentRollbackDepthIsBounded
}
module CrosslinkPowForkScheduleTest {
import CrosslinkPowForkSchedule.* from "./CrosslinkPowForkSchedule"
export CrosslinkPowForkSchedule.*
run forkSwitchDerivesRollbackDepthTest = {
assert(RollbackDepth("a4", "b4") == 2)
}
run sameBranchExtensionHasZeroRollbackDepthTest = {
assert(RollbackDepth("a3", "a4") == 0)
}
run raisedSigmaSurvivesForkSwitchThatBaseSigmaDoesNotTest = all {
assert(not(SampleSurvivesRollback("a4", "b4", 1))),
assert(SampleSurvivesRollback("a4", "b4", Sigma)),
}
run scheduleDerivesRollbackDepthAcrossRoundsTest = {
Init
.then(AdvanceRound)
.then(all {
assert(bestTip == "a4"),
assert(observedRollbackDepth == 0),
assert(firedAction == "AdvanceSameBranch"),
assert(Safety),
Stutter,
})
.then(AdvanceRound)
.then(all {
assert(bestTip == "b4"),
assert(observedRollbackDepth == 2),
assert(firedAction == "AdvanceForkSwitch"),
assert(Safety),
Stutter,
})
}
}
module CrosslinkPowForkScheduleModel {
import CrosslinkPowForkScheduleTest(
MaxRound = 2,
MaxHeight = 4,
Sigma = 3,
Snapshots = Set("g", "a1", "a2", "a3", "a4", "b3", "b4"),
BestTip = Map(
0 -> "a3",
1 -> "a4",
2 -> "b4",
),
HeightOf = Map(
"g" -> 0,
"a1" -> 1,
"a2" -> 2,
"a3" -> 3,
"a4" -> 4,
"b3" -> 3,
"b4" -> 4,
),
LcaHeight = Map(
"a3" -> Map("a4" -> 3, "b4" -> 2),
"a4" -> Map("a4" -> 4, "b4" -> 2),
"b4" -> Map("a4" -> 2, "b4" -> 4),
),
).*
}
// -*- mode: Bluespec; -*-
module CrosslinkResampling {
/*
A small Crosslink/Tenderlink model derived from the current Quint
Tendermint examples, focused on one question:
What changes when a 2f+1 PRECOMMIT nil certificate lets the next
round resample Crosslink's moving PoW stream?
This is a Crosslink-focused Tenderlink model. It intentionally isolates the
Crosslink-specific moving-value part:
- BFT values are PoW snapshots sampled from Stream(round), modelling the
implementation's current "head - sigma" proposal.
- A nil-precommit quorum for round r is a round-abandon certificate.
- In the resampling variant, that certificate clears Crosslink's proposal
cache and same-round Tendermint valid/lock state for r, allowing the next
proposer to sample Stream(r + 1).
- In the current/sticky variant, the proposal cache continues to carry the
stale sample into the next round, and same-round valid/lock state blocks
fresh voting.
- Older Tendermint validValue/lockedValue state is preserved; the nil
precommit certificate is only an unlock certificate for its own round.
- The unlock rule keeps Tendermint's value-lock safety argument: retained
locks are backed by value precommits, and a nil certificate can coexist
with at most f correct same-round value locks, not a commit-capable quorum.
*/
type Proc_t = str
type Snapshot_t = str
type Round_t = int
type Step_t = str
const Corr: Set[Proc_t]
const Faulty: Set[Proc_t]
const N: int
const T: int
const MaxRound: Round_t
const Proposer: Round_t -> Proc_t
const Stream: Round_t -> Snapshot_t
const Snapshots: Set[Snapshot_t]
const ResampleOnNilPrecommit: bool
pure val Rounds = 0.to(MaxRound)
pure val NilRound = -1
pure val RoundsOrNil = Set(NilRound).union(Rounds)
pure val NilSnapshot = "None"
pure val SnapshotsOrNil = Set(NilSnapshot).union(Snapshots)
pure val AllProcs = Corr.union(Faulty)
pure val THRESHOLD1 = T + 1
pure val THRESHOLD2 = 2 * T + 1
assume corr_and_faulty_disjoint = Corr.intersect(Faulty) == Set()
assume corr_and_faulty_make_n = N == size(Corr.union(Faulty))
assume faulty_at_threshold = size(Faulty) <= T
assume quorum_intersection_has_correct =
2 * THRESHOLD2 - N > T
assume nil_is_not_snapshot = not(Snapshots.contains(NilSnapshot))
assume streams_are_snapshots =
Rounds.forall(r => Snapshots.contains(Stream.get(r)))
type Propose_t = {
src: Proc_t,
round: Round_t,
proposal: Snapshot_t,
validRound: Round_t,
}
type Vote_t = {
src: Proc_t,
round: Round_t,
id: Snapshot_t,
}
var round: Proc_t -> Round_t
var step: Proc_t -> Step_t
var decision: Proc_t -> Snapshot_t
var lockedValue: Proc_t -> Snapshot_t
var lockedRound: Proc_t -> Round_t
var validValue: Proc_t -> Snapshot_t
var validRound: Proc_t -> Round_t
var cachedProposal: Proc_t -> Snapshot_t
var cachedProposalRound: Proc_t -> Round_t
var msgsPropose: Round_t -> Set[Propose_t]
var msgsPrevote: Round_t -> Set[Vote_t]
var msgsPrecommit: Round_t -> Set[Vote_t]
var evidencePropose: Set[Propose_t]
var evidencePrevote: Set[Vote_t]
var evidencePrecommit: Set[Vote_t]
var firedAction: str
pure def IsValidSnapshot(v: Snapshot_t): bool =
Snapshots.contains(v)
pure def IsFreshForRound(r: Round_t, v: Snapshot_t): bool =
Stream.get(r) == v
pure def StreamChangedAfter(r: Round_t): bool =
(r + 1).in(Rounds) and Stream.get(r + 1) != Stream.get(r)
pure def Voters(votes: Set[Vote_t]): Set[Proc_t] =
votes.map(m => m.src)
def Proposers(r: Round_t): Set[Proc_t] =
msgsPropose.get(r).map(m => m.src)
def RoundActivityVoters(r: Round_t): Set[Proc_t] =
Proposers(r)
.union(Voters(msgsPrevote.get(r)))
.union(Voters(msgsPrecommit.get(r)))
def PrevoteQuorum(r: Round_t, v: Snapshot_t): bool =
size(Voters(msgsPrevote.get(r).filter(m => m.id == v))) >= THRESHOLD2
def PrecommitQuorum(r: Round_t, v: Snapshot_t): bool =
size(Voters(msgsPrecommit.get(r).filter(m => m.id == v))) >= THRESHOLD2
def NilPrecommitCert(r: Round_t): bool =
PrecommitQuorum(r, NilSnapshot)
def RoundCatchupEvidence(r: Round_t): bool =
size(RoundActivityVoters(r)) >= THRESHOLD1
def HasPrevoteFrom(p: Proc_t, r: Round_t): bool =
msgsPrevote.get(r).exists(m => m.src == p)
def HasPrecommitFrom(p: Proc_t, r: Round_t): bool =
msgsPrecommit.get(r).exists(m => m.src == p)
def HasValuePrecommitFrom(p: Proc_t, r: Round_t, v: Snapshot_t): bool =
msgsPrecommit.get(r).exists(m => m.src == p and m.id == v)
def HasNilPrecommitFrom(p: Proc_t, r: Round_t): bool =
msgsPrecommit.get(r).exists(m => m.src == p and m.id == NilSnapshot)
def CorrectValueLocks(r: Round_t, v: Snapshot_t): Set[Proc_t] =
Corr.filter(p => lockedRound.get(p) == r and lockedValue.get(p) == v)
def HasProposal(r: Round_t, v: Snapshot_t): bool =
msgsPropose.get(r).exists(m =>
m.src == Proposer.get(r) and m.round == r and m.proposal == v
)
def HasNilValidRoundProposal(r: Round_t, v: Snapshot_t): bool =
msgsPropose.get(r).exists(m =>
m.src == Proposer.get(r) and
m.round == r and
m.proposal == v and
m.validRound == NilRound
)
def HasConcreteValidRoundProposal(
r: Round_t,
v: Snapshot_t,
vr: Round_t,
): bool =
msgsPropose.get(r).exists(m =>
m.src == Proposer.get(r) and
m.round == r and
m.proposal == v and
m.validRound == vr and
vr.in(Rounds) and
vr < r and
PrevoteQuorum(vr, v)
)
def HasPrevoteJustifiedProposal(r: Round_t, v: Snapshot_t): bool =
msgsPropose.get(r).exists(m =>
m.src == Proposer.get(r) and
m.round == r and
m.proposal == v and
(
m.validRound == NilRound or (
m.validRound.in(Rounds) and
m.validRound < r and
PrevoteQuorum(m.validRound, v)
)
)
)
val EvidencePropose: Set[Propose_t] =
evidencePropose
val EvidencePrevote: Set[Vote_t] =
evidencePrevote
val EvidencePrecommit: Set[Vote_t] =
evidencePrecommit
def FaultyProposals(r: Round_t): Set[Propose_t] =
tuples(Faulty, Snapshots, RoundsOrNil)
.map(((p, v, vr)) => {
src: p,
round: r,
proposal: v,
validRound: vr,
})
val AllFaultyProposals =
Rounds.map(r => FaultyProposals(r)).flatten()
def FaultyPrevotes(r: Round_t): Set[Vote_t] =
tuples(Faulty, SnapshotsOrNil)
.map(((p, v)) => {
src: p,
round: r,
id: v,
})
val AllFaultyPrevotes =
Rounds.map(r => FaultyPrevotes(r)).flatten()
def FaultyPrecommits(r: Round_t): Set[Vote_t] =
tuples(Faulty, SnapshotsOrNil)
.map(((p, v)) => {
src: p,
round: r,
id: v,
})
val AllFaultyPrecommits =
Rounds.map(r => FaultyPrecommits(r)).flatten()
def StickyOrStreamProposal(p: Proc_t): Snapshot_t =
if (validValue.get(p) == NilSnapshot) {
if (cachedProposal.get(p) == NilSnapshot) {
Stream.get(round.get(p))
} else {
cachedProposal.get(p)
}
} else {
validValue.get(p)
}
action Init = all {
round' = Corr.mapBy(_ => 0),
step' = Corr.mapBy(_ => "propose"),
decision' = Corr.mapBy(_ => NilSnapshot),
lockedValue' = Corr.mapBy(_ => NilSnapshot),
lockedRound' = Corr.mapBy(_ => NilRound),
validValue' = Corr.mapBy(_ => NilSnapshot),
validRound' = Corr.mapBy(_ => NilRound),
cachedProposal' = Corr.mapBy(_ => NilSnapshot),
cachedProposalRound' = Corr.mapBy(_ => NilRound),
msgsPropose' = Rounds.mapBy(_ => Set()),
msgsPrevote' = Rounds.mapBy(_ => Set()),
msgsPrecommit' = Rounds.mapBy(_ => Set()),
evidencePropose' = Set(),
evidencePrevote' = Set(),
evidencePrecommit' = Set(),
firedAction' = "Init",
}
action InitWithFaultyEvidence =
nondet faultyProposals = AllFaultyProposals.powerset().oneOf()
nondet faultyPrevotes = AllFaultyPrevotes.powerset().oneOf()
nondet faultyPrecommits = AllFaultyPrecommits.powerset().oneOf()
all {
round' = Corr.mapBy(_ => 0),
step' = Corr.mapBy(_ => "propose"),
decision' = Corr.mapBy(_ => NilSnapshot),
lockedValue' = Corr.mapBy(_ => NilSnapshot),
lockedRound' = Corr.mapBy(_ => NilRound),
validValue' = Corr.mapBy(_ => NilSnapshot),
validRound' = Corr.mapBy(_ => NilRound),
cachedProposal' = Corr.mapBy(_ => NilSnapshot),
cachedProposalRound' = Corr.mapBy(_ => NilRound),
msgsPropose' = Rounds.mapBy(r =>
faultyProposals.filter(m => m.round == r)
),
msgsPrevote' = Rounds.mapBy(r =>
faultyPrevotes.filter(m => m.round == r)
),
msgsPrecommit' = Rounds.mapBy(r =>
faultyPrecommits.filter(m => m.round == r)
),
evidencePropose' = faultyProposals,
evidencePrevote' = faultyPrevotes,
evidencePrecommit' = faultyPrecommits,
firedAction' = "InitWithFaultyEvidence",
}
action StartRound(p: Proc_t, targetRound: Round_t): bool =
all {
targetRound.in(Rounds),
step.get(p) != "decided",
round' = round.set(p, targetRound),
step' = step.set(p, "propose"),
firedAction' = "StartRound",
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
}
action BroadcastProposal(p: Proc_t, r: Round_t, v: Snapshot_t, vr: Round_t): bool = {
val m = {
src: p,
round: r,
proposal: v,
validRound: vr,
}
all {
msgsPropose' = msgsPropose.setBy(r, old => old.union(Set(m))),
evidencePropose' = evidencePropose.union(Set(m)),
}
}
action BroadcastPrevote(p: Proc_t, r: Round_t, v: Snapshot_t): bool = {
val m = {
src: p,
round: r,
id: v,
}
all {
msgsPrevote' = msgsPrevote.setBy(r, old => old.union(Set(m))),
evidencePrevote' = evidencePrevote.union(Set(m)),
}
}
action BroadcastPrecommit(p: Proc_t, r: Round_t, v: Snapshot_t): bool = {
val m = {
src: p,
round: r,
id: v,
}
all {
msgsPrecommit' = msgsPrecommit.setBy(r, old => old.union(Set(m))),
evidencePrecommit' = evidencePrecommit.union(Set(m)),
}
}
action InsertProposal(p: Proc_t): bool = {
val r = round.get(p)
val proposal = StickyOrStreamProposal(p)
all {
p == Proposer.get(r),
step.get(p) == "propose",
msgsPropose.get(r).forall(m => m.src != p),
BroadcastProposal(p, r, proposal, validRound.get(p)),
firedAction' = "InsertProposal",
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
}
}
action UponProposalPrevote(p: Proc_t, v: Snapshot_t): bool = {
val r = round.get(p)
val vote = if (
IsValidSnapshot(v) and
IsFreshForRound(r, v) and
(lockedRound.get(p) == NilRound or lockedValue.get(p) == v)
) {
v
} else {
NilSnapshot
}
all {
step.get(p) == "propose",
HasPrevoteJustifiedProposal(r, v),
not(HasPrevoteFrom(p, r)),
BroadcastPrevote(p, r, vote),
step' = step.set(p, "prevote"),
firedAction' = "UponProposalPrevote",
round' = round,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrecommit' = evidencePrecommit,
}
}
action UponProposalInPropose(p: Proc_t, v: Snapshot_t): bool = {
val r = round.get(p)
all {
HasNilValidRoundProposal(r, v),
UponProposalPrevote(p, v),
}
}
action UponProposalInProposeAndPrevote(
p: Proc_t,
v: Snapshot_t,
vr: Round_t,
): bool = {
val r = round.get(p)
all {
HasConcreteValidRoundProposal(r, v, vr),
UponProposalPrevote(p, v),
}
}
action TimeoutProposePrevoteNil(p: Proc_t): bool = {
val r = round.get(p)
all {
step.get(p) == "propose",
not(HasPrevoteFrom(p, r)),
BroadcastPrevote(p, r, NilSnapshot),
step' = step.set(p, "prevote"),
firedAction' = "TimeoutProposePrevoteNil",
round' = round,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrecommit' = evidencePrecommit,
}
}
action UponValuePrevoteQuorum(p: Proc_t, v: Snapshot_t): bool = {
val r = round.get(p)
all {
step.get(p).in(Set("prevote", "precommit")),
HasProposal(r, v),
IsValidSnapshot(v),
IsFreshForRound(r, v),
PrevoteQuorum(r, v),
not(HasPrecommitFrom(p, r)),
BroadcastPrecommit(p, r, v),
step' = step.set(p, "precommit"),
lockedValue' = lockedValue.set(p, v),
lockedRound' = lockedRound.set(p, r),
validValue' = validValue.set(p, v),
validRound' = validRound.set(p, r),
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
firedAction' = "UponValuePrevoteQuorum",
round' = round,
decision' = decision,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
}
}
action TimeoutPrevotePrecommitNil(p: Proc_t): bool = {
val r = round.get(p)
all {
step.get(p) == "prevote",
not(HasPrecommitFrom(p, r)),
BroadcastPrecommit(p, r, NilSnapshot),
step' = step.set(p, "precommit"),
firedAction' = "TimeoutPrevotePrecommitNil",
round' = round,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
}
}
action UponNilPrevoteQuorum(p: Proc_t): bool = {
val r = round.get(p)
all {
step.get(p).in(Set("prevote", "precommit")),
PrevoteQuorum(r, NilSnapshot),
not(HasPrecommitFrom(p, r)),
BroadcastPrecommit(p, r, NilSnapshot),
step' = step.set(p, "precommit"),
firedAction' = "UponNilPrevoteQuorum",
round' = round,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
}
}
action UponStreamChangePrecommitNil(p: Proc_t): bool = {
val r = round.get(p)
all {
step.get(p).in(Set("prevote", "precommit")),
StreamChangedAfter(r),
not(HasPrecommitFrom(p, r)),
BroadcastPrecommit(p, r, NilSnapshot),
step' = step.set(p, "precommit"),
firedAction' = "UponStreamChangePrecommitNil",
round' = round,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
}
}
action StartNextRoundAfterPrecommitQuorum(p: Proc_t): bool = {
val oldRound = round.get(p)
val nextRound = oldRound + 1
val clearSameRoundCache = ResampleOnNilPrecommit and NilPrecommitCert(oldRound)
all {
nextRound.in(Rounds),
size(Voters(msgsPrecommit.get(oldRound))) >= THRESHOLD2,
round' = round.set(p, nextRound),
step' = step.set(p, "propose"),
validValue' =
if (clearSameRoundCache and validRound.get(p) == oldRound) {
validValue.set(p, NilSnapshot)
} else {
validValue
},
validRound' =
if (clearSameRoundCache and validRound.get(p) == oldRound) {
validRound.set(p, NilRound)
} else {
validRound
},
lockedValue' =
if (clearSameRoundCache and lockedRound.get(p) == oldRound) {
lockedValue.set(p, NilSnapshot)
} else {
lockedValue
},
lockedRound' =
if (clearSameRoundCache and lockedRound.get(p) == oldRound) {
lockedRound.set(p, NilRound)
} else {
lockedRound
},
cachedProposal' =
if (clearSameRoundCache and cachedProposalRound.get(p) == oldRound) {
cachedProposal.set(p, NilSnapshot)
} else {
cachedProposal
},
cachedProposalRound' =
if (clearSameRoundCache and cachedProposalRound.get(p) == oldRound) {
cachedProposalRound.set(p, NilRound)
} else {
cachedProposalRound
},
firedAction' = "StartNextRoundAfterPrecommitQuorum",
decision' = decision,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
}
}
action TimeoutPrecommitStartNextRound(p: Proc_t): bool = {
val oldRound = round.get(p)
val nextRound = oldRound + 1
val clearSameRoundCache = ResampleOnNilPrecommit and NilPrecommitCert(oldRound)
all {
nextRound.in(Rounds),
step.get(p) == "precommit",
round' = round.set(p, nextRound),
step' = step.set(p, "propose"),
validValue' =
if (clearSameRoundCache and validRound.get(p) == oldRound) {
validValue.set(p, NilSnapshot)
} else {
validValue
},
validRound' =
if (clearSameRoundCache and validRound.get(p) == oldRound) {
validRound.set(p, NilRound)
} else {
validRound
},
lockedValue' =
if (clearSameRoundCache and lockedRound.get(p) == oldRound) {
lockedValue.set(p, NilSnapshot)
} else {
lockedValue
},
lockedRound' =
if (clearSameRoundCache and lockedRound.get(p) == oldRound) {
lockedRound.set(p, NilRound)
} else {
lockedRound
},
cachedProposal' =
if (clearSameRoundCache and cachedProposalRound.get(p) == oldRound) {
cachedProposal.set(p, NilSnapshot)
} else {
cachedProposal
},
cachedProposalRound' =
if (clearSameRoundCache and cachedProposalRound.get(p) == oldRound) {
cachedProposalRound.set(p, NilRound)
} else {
cachedProposalRound
},
firedAction' = "TimeoutPrecommitStartNextRound",
decision' = decision,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
}
}
action CatchUpToRound(p: Proc_t, targetRound: Round_t): bool =
all {
targetRound.in(Rounds),
targetRound > round.get(p),
RoundCatchupEvidence(targetRound),
round' = round.set(p, targetRound),
step' = step.set(p, "propose"),
firedAction' = "CatchUpToRound",
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
}
action ApplyLateNilPrecommitCertificate(p: Proc_t, certRound: Round_t): bool = {
all {
certRound.in(Rounds),
certRound < round.get(p),
ResampleOnNilPrecommit,
NilPrecommitCert(certRound),
round' = round,
step' = step,
validValue' =
if (validRound.get(p) == certRound) {
validValue.set(p, NilSnapshot)
} else {
validValue
},
validRound' =
if (validRound.get(p) == certRound) {
validRound.set(p, NilRound)
} else {
validRound
},
lockedValue' =
if (lockedRound.get(p) == certRound) {
lockedValue.set(p, NilSnapshot)
} else {
lockedValue
},
lockedRound' =
if (lockedRound.get(p) == certRound) {
lockedRound.set(p, NilRound)
} else {
lockedRound
},
cachedProposal' =
if (cachedProposalRound.get(p) == certRound) {
cachedProposal.set(p, NilSnapshot)
} else {
cachedProposal
},
cachedProposalRound' =
if (cachedProposalRound.get(p) == certRound) {
cachedProposalRound.set(p, NilRound)
} else {
cachedProposalRound
},
firedAction' = "ApplyLateNilPrecommitCertificate",
decision' = decision,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
}
}
action Decide(p: Proc_t, r: Round_t, v: Snapshot_t): bool = all {
decision.get(p) == NilSnapshot,
HasProposal(r, v),
IsValidSnapshot(v),
IsFreshForRound(r, v),
PrecommitQuorum(r, v),
decision' = decision.set(p, v),
step' = step.set(p, "decided"),
firedAction' = "Decide",
round' = round,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
}
action SeedAbandonedRoundState(p: Proc_t): bool = {
val r = 0
val stale = Stream.get(r)
all {
r.in(Rounds),
p.in(Corr),
round' = round.set(p, r),
step' = step.set(p, "precommit"),
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal.set(p, stale),
cachedProposalRound' = cachedProposalRound.set(p, r),
BroadcastPrecommit(p, r, NilSnapshot),
firedAction' = "SeedAbandonedRoundState",
decision' = decision,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
}
}
action SeedValidNilPrecommitState(p: Proc_t): bool = {
val r = 0
val stale = Stream.get(r)
all {
r.in(Rounds),
p.in(Corr),
round' = round.set(p, r),
step' = step.set(p, "precommit"),
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue.set(p, stale),
validRound' = validRound.set(p, r),
cachedProposal' = cachedProposal.set(p, stale),
cachedProposalRound' = cachedProposalRound.set(p, r),
BroadcastPrecommit(p, r, NilSnapshot),
firedAction' = "SeedValidNilPrecommitState",
decision' = decision,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
}
}
action SeedSameRoundValueLock(p: Proc_t): bool = {
val r = 0
val locked = Stream.get(r)
all {
r.in(Rounds),
p.in(Corr),
round' = round.set(p, r),
step' = step.set(p, "precommit"),
lockedValue' = lockedValue.set(p, locked),
lockedRound' = lockedRound.set(p, r),
validValue' = validValue.set(p, locked),
validRound' = validRound.set(p, r),
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
BroadcastPrecommit(p, r, locked),
firedAction' = "SeedSameRoundValueLock",
decision' = decision,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
}
}
action SeedTimedOutSameRoundValueLock(p: Proc_t): bool = {
val r = 0
val nextRound = 1
val locked = Stream.get(r)
all {
r.in(Rounds),
nextRound.in(Rounds),
p.in(Corr),
round' = round.set(p, nextRound),
step' = step.set(p, "propose"),
lockedValue' = lockedValue.set(p, locked),
lockedRound' = lockedRound.set(p, r),
validValue' = validValue.set(p, locked),
validRound' = validRound.set(p, r),
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
BroadcastPrecommit(p, r, locked),
firedAction' = "SeedTimedOutSameRoundValueLock",
decision' = decision,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
}
}
action SeedFaultyNilPrecommit(p: Proc_t): bool = {
val r = 0
all {
r.in(Rounds),
p.in(Faulty),
round' = round,
step' = step,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
BroadcastPrecommit(p, r, NilSnapshot),
firedAction' = "SeedFaultyNilPrecommit",
decision' = decision,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
}
}
action SeedOlderLockWithNilPrecommit(p: Proc_t): bool = {
val activeRound = 1
val lockRound = 0
val locked = Stream.get(lockRound)
val lockEvidence = {
src: p,
round: lockRound,
id: locked,
}
val nilEvidence = {
src: p,
round: activeRound,
id: NilSnapshot,
}
all {
activeRound.in(Rounds),
lockRound.in(Rounds),
p.in(Corr),
round' = round.set(p, activeRound),
step' = step.set(p, "precommit"),
lockedValue' = lockedValue.set(p, locked),
lockedRound' = lockedRound.set(p, lockRound),
validValue' = validValue.set(p, locked),
validRound' = validRound.set(p, lockRound),
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPrecommit' = msgsPrecommit
.setBy(lockRound, old => old.union(Set(lockEvidence)))
.setBy(activeRound, old => old.union(Set(nilEvidence))),
evidencePrecommit' = evidencePrecommit.union(Set(lockEvidence, nilEvidence)),
firedAction' = "SeedOlderLockWithNilPrecommit",
decision' = decision,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
}
}
action SeedCorrectPrecommitEvidence(p: Proc_t, r: Round_t, v: Snapshot_t): bool =
all {
p.in(Corr),
r.in(Rounds),
IsValidSnapshot(v),
BroadcastPrecommit(p, r, v),
firedAction' = "SeedCorrectPrecommitEvidence",
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
}
action SeedCorrectNilPrevoteState(p: Proc_t, r: Round_t): bool =
all {
p.in(Corr),
r.in(Rounds),
BroadcastPrevote(p, r, NilSnapshot),
firedAction' = "SeedCorrectNilPrevoteState",
round' = round,
step' = step.set(p, "prevote"),
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrecommit' = evidencePrecommit,
}
action SeedFaultyPrecommitEvidence(p: Proc_t, r: Round_t, v: Snapshot_t): bool =
all {
p.in(Faulty),
r.in(Rounds),
IsValidSnapshot(v),
BroadcastPrecommit(p, r, v),
firedAction' = "SeedFaultyPrecommitEvidence",
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
}
action SeedFaultyProposalEvidence(
p: Proc_t,
r: Round_t,
v: Snapshot_t,
vr: Round_t,
): bool =
all {
p.in(Faulty),
r.in(Rounds),
IsValidSnapshot(v),
vr == NilRound or vr.in(Rounds),
BroadcastProposal(p, r, v, vr),
firedAction' = "SeedFaultyProposalEvidence",
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
}
action SeedFaultyPrevoteEvidence(p: Proc_t, r: Round_t, v: Snapshot_t): bool =
all {
p.in(Faulty),
r.in(Rounds),
IsValidSnapshot(v),
BroadcastPrevote(p, r, v),
firedAction' = "SeedFaultyPrevoteEvidence",
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrecommit' = evidencePrecommit,
}
action SeedFaultyNilPrevoteEvidence(p: Proc_t, r: Round_t): bool =
all {
p.in(Faulty),
r.in(Rounds),
BroadcastPrevote(p, r, NilSnapshot),
firedAction' = "SeedFaultyNilPrevoteEvidence",
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrecommit' = evidencePrecommit,
}
action SeedFaultyNilPrecommitEvidence(p: Proc_t, r: Round_t): bool =
all {
p.in(Faulty),
r.in(Rounds),
BroadcastPrecommit(p, r, NilSnapshot),
firedAction' = "SeedFaultyNilPrecommitEvidence",
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
}
action Next =
nondet p = oneOf(Corr)
any {
InsertProposal(p),
nondet v = oneOf(Snapshots)
UponProposalInPropose(p, v),
nondet v = oneOf(Snapshots)
nondet vr = oneOf(Rounds)
UponProposalInProposeAndPrevote(p, v, vr),
TimeoutProposePrevoteNil(p),
nondet v = oneOf(Snapshots)
UponValuePrevoteQuorum(p, v),
TimeoutPrevotePrecommitNil(p),
UponNilPrevoteQuorum(p),
UponStreamChangePrecommitNil(p),
StartNextRoundAfterPrecommitQuorum(p),
TimeoutPrecommitStartNextRound(p),
nondet targetRound = oneOf(Rounds)
CatchUpToRound(p, targetRound),
nondet r = oneOf(Rounds)
nondet v = oneOf(Snapshots)
Decide(p, r, v),
}
val Agreement =
tuples(Corr, Corr).forall(((p, q)) =>
decision.get(p) == NilSnapshot or
decision.get(q) == NilSnapshot or
decision.get(p) == decision.get(q)
)
val Validity =
Corr.forall(p =>
decision.get(p) == NilSnapshot or IsValidSnapshot(decision.get(p))
)
val NoCorrectPrecommitEquivocation =
Corr.forall(p =>
Rounds.forall(r =>
msgsPrecommit.get(r).filter(m => m.src == p).map(m => m.id).size() <= 1
)
)
val NoCorrectPrevoteEquivocation =
Corr.forall(p =>
Rounds.forall(r =>
msgsPrevote.get(r).filter(m => m.src == p).map(m => m.id).size() <= 1
)
)
val CorrectValuePrevotesHaveJustifiedProposal =
Rounds.forall(r =>
msgsPrevote.get(r)
.filter(m => m.src.in(Corr) and m.id != NilSnapshot)
.forall(m => HasPrevoteJustifiedProposal(r, m.id))
)
val SameRoundNilAndValueCommitExcluded =
Rounds.forall(r =>
NilPrecommitCert(r) implies
Snapshots.forall(v => not(PrecommitQuorum(r, v)))
)
val LockedValueHasMatchingValidValue =
Corr.forall(p =>
lockedValue.get(p) == NilSnapshot or (
validValue.get(p) == lockedValue.get(p) and
validRound.get(p) >= lockedRound.get(p)
)
)
val CorrectValueLocksHavePrecommitEvidence =
Corr.forall(p =>
lockedValue.get(p) == NilSnapshot or (
lockedRound.get(p).in(Rounds) and
IsValidSnapshot(lockedValue.get(p)) and
HasValuePrecommitFrom(p, lockedRound.get(p), lockedValue.get(p))
)
)
val NilCertLeavesAtMostFCurrentRoundLocks =
Rounds.forall(r =>
NilPrecommitCert(r) implies
Snapshots.forall(v => size(CorrectValueLocks(r, v)) <= T)
)
val EvidenceCoversObservedMessages =
Rounds.forall(r =>
msgsPropose.get(r).subseteq(EvidencePropose) and
msgsPrevote.get(r).subseteq(EvidencePrevote) and
msgsPrecommit.get(r).subseteq(EvidencePrecommit)
)
def ConflictingValueCommits(
r1: Round_t,
v1: Snapshot_t,
r2: Round_t,
v2: Snapshot_t,
): bool =
IsValidSnapshot(v1) and
IsValidSnapshot(v2) and
v1 != v2 and
PrecommitQuorum(r1, v1) and
PrecommitQuorum(r2, v2)
def CorrectSameRoundEquivocationEvidence(
r: Round_t,
v1: Snapshot_t,
v2: Snapshot_t,
): bool =
v1 != v2 and
Corr.exists(p =>
HasValuePrecommitFrom(p, r, v1) and
HasValuePrecommitFrom(p, r, v2)
)
def CorrectNilValueEquivocationEvidence(r: Round_t, v: Snapshot_t): bool =
IsValidSnapshot(v) and
NilPrecommitCert(r) and
PrecommitQuorum(r, v) and
Corr.exists(p =>
HasNilPrecommitFrom(p, r) and HasValuePrecommitFrom(p, r, v)
)
def CorrectValueSwitchWithoutUnlock(
p: Proc_t,
fromRound: Round_t,
fromValue: Snapshot_t,
toRound: Round_t,
toValue: Snapshot_t,
): bool =
p.in(Corr) and
fromRound < toRound and
IsValidSnapshot(fromValue) and
IsValidSnapshot(toValue) and
fromValue != toValue and
HasValuePrecommitFrom(p, fromRound, fromValue) and
HasValuePrecommitFrom(p, toRound, toValue) and
not(NilPrecommitCert(fromRound))
def ProposalEquivocationIn(p: Proc_t, messages: Set[Propose_t]): bool =
tuples(messages, messages).exists(((m1, m2)) =>
m1 != m2 and
m1.src == p and
m2.src == p and
m1.round == m2.round
)
def VoteEquivocationIn(p: Proc_t, messages: Set[Vote_t]): bool =
tuples(messages, messages).exists(((m1, m2)) =>
m1 != m2 and
m1.src == p and
m2.src == p and
m1.round == m2.round
)
def EquivocationBy(p: Proc_t): bool =
ProposalEquivocationIn(p, EvidencePropose) or
VoteEquivocationIn(p, EvidencePrevote) or
VoteEquivocationIn(p, EvidencePrecommit)
def ValueVoteEvidenceFrom(p: Proc_t, r: Round_t, v: Snapshot_t): bool =
EvidencePrevote.contains({ src: p, round: r, id: v }) or
EvidencePrecommit.contains({ src: p, round: r, id: v })
def PriorValueLockEvidenceFrom(p: Proc_t, r: Round_t, v: Snapshot_t): bool =
EvidencePrecommit.contains({ src: p, round: r, id: v })
def EarlierRoundsHaveNoUnlockFor(r1: Round_t, r2: Round_t, v: Snapshot_t): bool =
Rounds
.filter(r => r1 <= r and r < r2)
.forall(r =>
size(Voters(EvidencePrevote.filter(m => m.round == r and m.id == v))) < THRESHOLD2
)
def AmnesiaBy(p: Proc_t): bool =
tuples(Rounds, Rounds).exists(((r1, r2)) =>
r1 < r2 and
Snapshots.exists(v1 =>
Snapshots.exists(v2 =>
v1 != v2 and
PriorValueLockEvidenceFrom(p, r1, v1) and
ValueVoteEvidenceFrom(p, r2, v2) and
EarlierRoundsHaveNoUnlockFor(r1, r2, v2) and
not(NilPrecommitCert(r1))
)
)
)
def DetectableFaultBy(p: Proc_t): bool =
EquivocationBy(p) or AmnesiaBy(p)
val DetectableFaults: Set[Proc_t] =
AllProcs.filter(p => DetectableFaultBy(p))
val Accountability =
Agreement or size(DetectableFaults) >= THRESHOLD1
def ConflictHasAccountabilityEvidence(
r1: Round_t,
v1: Snapshot_t,
r2: Round_t,
v2: Snapshot_t,
): bool =
ConflictingValueCommits(r1, v1, r2, v2) and
if (r1 == r2) {
CorrectSameRoundEquivocationEvidence(r1, v1, v2)
} else if (r1 < r2) {
CorrectNilValueEquivocationEvidence(r1, v1) or
Corr.exists(p => CorrectValueSwitchWithoutUnlock(p, r1, v1, r2, v2))
} else {
CorrectNilValueEquivocationEvidence(r2, v2) or
Corr.exists(p => CorrectValueSwitchWithoutUnlock(p, r2, v2, r1, v1))
}
val ConflictingCommitsAccountable =
Rounds.forall(r1 =>
Rounds.forall(r2 =>
Snapshots.forall(v1 =>
Snapshots.forall(v2 =>
ConflictingValueCommits(r1, v1, r2, v2) implies
ConflictHasAccountabilityEvidence(r1, v1, r2, v2)
)
)
)
)
val Safety =
Validity and
Agreement and
Accountability and
NoCorrectPrevoteEquivocation and
NoCorrectPrecommitEquivocation and
CorrectValuePrevotesHaveJustifiedProposal and
SameRoundNilAndValueCommitExcluded and
LockedValueHasMatchingValidValue and
CorrectValueLocksHavePrecommitEvidence and
NilCertLeavesAtMostFCurrentRoundLocks and
EvidenceCoversObservedMessages and
ConflictingCommitsAccountable
}
module CrosslinkResamplingTest {
import CrosslinkResampling.* from "./CrosslinkResampling"
export CrosslinkResampling.*
const ExpectedRound1Proposal: Snapshot_t
action unchangedAll = all {
round' = round,
step' = step,
decision' = decision,
lockedValue' = lockedValue,
lockedRound' = lockedRound,
validValue' = validValue,
validRound' = validRound,
cachedProposal' = cachedProposal,
cachedProposalRound' = cachedProposalRound,
msgsPropose' = msgsPropose,
msgsPrevote' = msgsPrevote,
msgsPrecommit' = msgsPrecommit,
evidencePropose' = evidencePropose,
evidencePrevote' = evidencePrevote,
evidencePrecommit' = evidencePrecommit,
firedAction' = firedAction,
}
run abandonedRoundProposalTest = {
Init
.then(SeedAbandonedRoundState("p1"))
.then(SeedAbandonedRoundState("p2"))
.then(SeedAbandonedRoundState("p3"))
.then(StartNextRoundAfterPrecommitQuorum("p2"))
.then(InsertProposal("p2"))
.then(all {
assert(msgsPropose.get(1).exists(m =>
m.src == "p2" and m.round == 1 and m.proposal == ExpectedRound1Proposal
)),
unchangedAll,
})
}
}
module CrosslinkStickyModel {
import CrosslinkResamplingTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 2,
Proposer = Map(0 -> "p1", 1 -> "p2", 2 -> "p3"),
Stream = Map(0 -> "s0", 1 -> "s1", 2 -> "s2"),
Snapshots = Set("s0", "s1", "s2"),
ResampleOnNilPrecommit = false,
ExpectedRound1Proposal = "s0",
).*
run currentProtocolCarriesStaleSampleTest = {
Init
.then(SeedAbandonedRoundState("p1"))
.then(SeedAbandonedRoundState("p2"))
.then(SeedAbandonedRoundState("p3"))
.then(StartNextRoundAfterPrecommitQuorum("p2"))
.then(InsertProposal("p2"))
.then(all {
assert(msgsPropose.get(1).exists(m =>
m.src == "p2" and m.round == 1 and m.proposal == "s0"
)),
unchangedAll,
})
}
run currentProtocolSameRoundLockBlocksFreshDecisionTest = {
Init
.then(SeedValidNilPrecommitState("p1"))
.then(SeedValidNilPrecommitState("p2"))
.then(SeedFaultyNilPrecommit("p4"))
.then(SeedSameRoundValueLock("p3"))
.then(StartNextRoundAfterPrecommitQuorum("p1"))
.then(StartNextRoundAfterPrecommitQuorum("p2"))
.then(StartNextRoundAfterPrecommitQuorum("p3"))
.then(InsertProposal("p2"))
.then(UponProposalPrevote("p1", "s1"))
.fail()
}
}
module CrosslinkNilResamplingModel {
import CrosslinkResamplingTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 2,
Proposer = Map(0 -> "p1", 1 -> "p2", 2 -> "p3"),
Stream = Map(0 -> "s0", 1 -> "s1", 2 -> "s2"),
Snapshots = Set("s0", "s1", "s2"),
ResampleOnNilPrecommit = true,
ExpectedRound1Proposal = "s1",
).*
run nilPrecommitResamplesFreshStreamTest = {
Init
.then(SeedAbandonedRoundState("p1"))
.then(SeedAbandonedRoundState("p2"))
.then(SeedAbandonedRoundState("p3"))
.then(StartNextRoundAfterPrecommitQuorum("p2"))
.then(InsertProposal("p2"))
.then(all {
assert(msgsPropose.get(1).exists(m =>
m.src == "p2" and m.round == 1 and m.proposal == "s1"
)),
unchangedAll,
})
}
run nilPrecommitPreservesOlderTendermintValueLockTest = {
Init
.then(SeedOlderLockWithNilPrecommit("p1"))
.then(SeedOlderLockWithNilPrecommit("p2"))
.then(SeedOlderLockWithNilPrecommit("p3"))
.then(StartNextRoundAfterPrecommitQuorum("p2"))
.then(all {
assert(lockedValue.get("p2") == "s0"),
assert(lockedRound.get("p2") == 0),
assert(validValue.get("p2") == "s0"),
assert(validRound.get("p2") == 0),
unchangedAll,
})
}
run nilPrecommitUnlocksSameRoundTendermintStateTest = {
Init
.then(SeedValidNilPrecommitState("p1"))
.then(SeedValidNilPrecommitState("p2"))
.then(SeedFaultyNilPrecommit("p4"))
.then(SeedSameRoundValueLock("p3"))
.then(StartNextRoundAfterPrecommitQuorum("p2"))
.then(StartNextRoundAfterPrecommitQuorum("p3"))
.then(all {
assert(validValue.get("p2") == "None"),
assert(validRound.get("p2") == -1),
assert(lockedValue.get("p2") == "None"),
assert(lockedRound.get("p2") == -1),
assert(lockedValue.get("p3") == "None"),
assert(lockedRound.get("p3") == -1),
assert(validValue.get("p3") == "None"),
assert(validRound.get("p3") == -1),
assert(NilPrecommitCert(0)),
assert(not(PrecommitQuorum(0, "s0"))),
unchangedAll,
})
}
run lateNilPrecommitCertificateUnlocksAbandonedRoundTest = {
Init
.then(SeedValidNilPrecommitState("p1"))
.then(SeedValidNilPrecommitState("p2"))
.then(SeedFaultyNilPrecommit("p4"))
.then(SeedTimedOutSameRoundValueLock("p3"))
.then(ApplyLateNilPrecommitCertificate("p3", 0))
.then(all {
assert(round.get("p3") == 1),
assert(validValue.get("p3") == "None"),
assert(validRound.get("p3") == -1),
assert(lockedValue.get("p3") == "None"),
assert(lockedRound.get("p3") == -1),
assert(NilPrecommitCert(0)),
assert(not(PrecommitQuorum(0, "s0"))),
unchangedAll,
})
}
run nilPrecommitUnlockResamplesAndDecidesFreshValueTest = {
Init
.then(SeedValidNilPrecommitState("p1"))
.then(SeedValidNilPrecommitState("p2"))
.then(SeedFaultyNilPrecommit("p4"))
.then(SeedSameRoundValueLock("p3"))
.then(StartNextRoundAfterPrecommitQuorum("p1"))
.then(StartNextRoundAfterPrecommitQuorum("p2"))
.then(StartNextRoundAfterPrecommitQuorum("p3"))
.then(InsertProposal("p2"))
.then(UponProposalPrevote("p1", "s1"))
.then(UponProposalPrevote("p2", "s1"))
.then(UponProposalPrevote("p3", "s1"))
.then(UponValuePrevoteQuorum("p1", "s1"))
.then(UponValuePrevoteQuorum("p2", "s1"))
.then(UponValuePrevoteQuorum("p3", "s1"))
.then(Decide("p1", 1, "s1"))
.then(all {
assert(decision.get("p1") == "s1"),
unchangedAll,
})
}
run conflictingCommitsExposeInvalidUnlockEvidenceTest = {
Init
.then(SeedCorrectPrecommitEvidence("p1", 0, "s0"))
.then(SeedCorrectPrecommitEvidence("p2", 0, "s0"))
.then(SeedFaultyPrecommitEvidence("p4", 0, "s0"))
.then(SeedCorrectPrecommitEvidence("p2", 1, "s1"))
.then(SeedCorrectPrecommitEvidence("p3", 1, "s1"))
.then(SeedFaultyPrecommitEvidence("p4", 1, "s1"))
.then(all {
assert(PrecommitQuorum(0, "s0")),
assert(PrecommitQuorum(1, "s1")),
assert(AmnesiaBy("p2")),
assert(AmnesiaBy("p4")),
assert(size(DetectableFaults) >= THRESHOLD1),
assert(Accountability),
assert(ConflictHasAccountabilityEvidence(0, "s0", 1, "s1")),
assert(ConflictingCommitsAccountable),
unchangedAll,
})
}
run conflictWithBogusNilUnlockExposesEquivocationEvidenceTest = {
Init
.then(SeedValidNilPrecommitState("p2"))
.then(SeedValidNilPrecommitState("p3"))
.then(SeedFaultyNilPrecommit("p4"))
.then(SeedCorrectPrecommitEvidence("p1", 0, "s0"))
.then(SeedCorrectPrecommitEvidence("p2", 0, "s0"))
.then(SeedFaultyPrecommitEvidence("p4", 0, "s0"))
.then(SeedCorrectPrecommitEvidence("p2", 1, "s1"))
.then(SeedCorrectPrecommitEvidence("p3", 1, "s1"))
.then(SeedFaultyPrecommitEvidence("p4", 1, "s1"))
.then(all {
assert(NilPrecommitCert(0)),
assert(PrecommitQuorum(0, "s0")),
assert(PrecommitQuorum(1, "s1")),
assert(EquivocationBy("p2")),
assert(EquivocationBy("p4")),
assert(size(DetectableFaults) >= THRESHOLD1),
assert(Accountability),
assert(CorrectNilValueEquivocationEvidence(0, "s0")),
assert(ConflictHasAccountabilityEvidence(0, "s0", 1, "s1")),
assert(ConflictingCommitsAccountable),
unchangedAll,
})
}
run nilPrecommitCertificateJustifiesSameRoundSwitchTest = {
Init
.then(SeedValidNilPrecommitState("p1"))
.then(SeedValidNilPrecommitState("p2"))
.then(SeedFaultyNilPrecommit("p4"))
.then(SeedSameRoundValueLock("p3"))
.then(StartNextRoundAfterPrecommitQuorum("p3"))
.then(SeedCorrectPrecommitEvidence("p1", 1, "s1"))
.then(SeedCorrectPrecommitEvidence("p2", 1, "s1"))
.then(SeedCorrectPrecommitEvidence("p3", 1, "s1"))
.then(all {
assert(NilPrecommitCert(0)),
assert(not(PrecommitQuorum(0, "s0"))),
assert(not(CorrectValueSwitchWithoutUnlock("p3", 0, "s0", 1, "s1"))),
assert(not(AmnesiaBy("p3"))),
assert(not(EquivocationBy("p3"))),
assert(Accountability),
assert(ConflictingCommitsAccountable),
unchangedAll,
})
}
run laterNilCertificateDoesNotUnlockOlderValueLockTest = {
Init
.then(SeedOlderLockWithNilPrecommit("p1"))
.then(SeedOlderLockWithNilPrecommit("p2"))
.then(SeedOlderLockWithNilPrecommit("p3"))
.then(SeedCorrectPrecommitEvidence("p2", 2, "s2"))
.then(SeedCorrectPrecommitEvidence("p3", 2, "s2"))
.then(SeedFaultyPrecommitEvidence("p4", 2, "s2"))
.then(all {
assert(PrecommitQuorum(0, "s0")),
assert(NilPrecommitCert(1)),
assert(PrecommitQuorum(2, "s2")),
assert(CorrectValueSwitchWithoutUnlock("p2", 0, "s0", 2, "s2")),
assert(AmnesiaBy("p2")),
assert(AmnesiaBy("p3")),
assert(size(DetectableFaults) >= THRESHOLD1),
assert(Accountability),
assert(ConflictHasAccountabilityEvidence(0, "s0", 2, "s2")),
assert(ConflictingCommitsAccountable),
unchangedAll,
})
}
}
module CrosslinkNilResamplingLivenessModel {
import CrosslinkResamplingTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 2,
Proposer = Map(0 -> "p1", 1 -> "p2", 2 -> "p3"),
Stream = Map(0 -> "s0", 1 -> "s1", 2 -> "s2"),
Snapshots = Set("s0", "s1", "s2"),
ResampleOnNilPrecommit = true,
ExpectedRound1Proposal = "s1",
).*
var phase: int
action LivenessInit = all {
Init,
phase' = 0,
}
action LivenessStep = any {
all { phase == 0, SeedValidNilPrecommitState("p1"), phase' = 1 },
all { phase == 1, SeedValidNilPrecommitState("p2"), phase' = 2 },
all { phase == 2, SeedFaultyNilPrecommit("p4"), phase' = 3 },
all { phase == 3, SeedSameRoundValueLock("p3"), phase' = 4 },
all { phase == 4, StartNextRoundAfterPrecommitQuorum("p1"), phase' = 5 },
all { phase == 5, StartNextRoundAfterPrecommitQuorum("p2"), phase' = 6 },
all { phase == 6, StartNextRoundAfterPrecommitQuorum("p3"), phase' = 7 },
all { phase == 7, InsertProposal("p2"), phase' = 8 },
all { phase == 8, UponProposalPrevote("p1", "s1"), phase' = 9 },
all { phase == 9, UponProposalPrevote("p2", "s1"), phase' = 10 },
all { phase == 10, UponProposalPrevote("p3", "s1"), phase' = 11 },
all { phase == 11, UponValuePrevoteQuorum("p1", "s1"), phase' = 12 },
all { phase == 12, UponValuePrevoteQuorum("p2", "s1"), phase' = 13 },
all { phase == 13, UponValuePrevoteQuorum("p3", "s1"), phase' = 14 },
all { phase == 14, Decide("p1", 1, "s1"), phase' = 15 },
all { phase == 15, unchangedAll, phase' = 15 },
}
val FreshDecisionByEnd =
phase < 15 or decision.get("p1") == "s1"
val LivenessSafety =
Safety and FreshDecisionByEnd
}
module CrosslinkResamplingModels {
import CrosslinkResamplingTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 2,
Proposer = Map(0 -> "p1", 1 -> "p2", 2 -> "p3"),
Stream = Map(0 -> "s0", 1 -> "s1", 2 -> "s2"),
Snapshots = Set("s0", "s1", "s2"),
ResampleOnNilPrecommit = false,
ExpectedRound1Proposal = "s0",
) as sticky
import CrosslinkResamplingTest(
Corr = Set("p1", "p2", "p3"),
Faulty = Set("p4"),
N = 4,
T = 1,
MaxRound = 2,
Proposer = Map(0 -> "p1", 1 -> "p2", 2 -> "p3"),
Stream = Map(0 -> "s0", 1 -> "s1", 2 -> "s2"),
Snapshots = Set("s0", "s1", "s2"),
ResampleOnNilPrecommit = true,
ExpectedRound1Proposal = "s1",
) as resampling
}

Dynamic Sigma Telemetry Integration

This note maps the dynamic-sigma Quint model inputs to production telemetry and calls out the pieces that are not present in the current prototype.

The core rule is that the controller should select a sigma at least as large as each independently required floor. One of those floors is the percentage of PoW hash power that is observably participating in Crosslink:

  • a hash-participation floor
  • a recent Tenderlink round-failure floor
  • a block-interval and rollback-depth risk floor
  • an economic rollback-risk and expected-loss floor

The Quint models intentionally keep these as bounded fixture inputs. A production controller needs a shared, conservative source for each input before it can change finality depth.

Current Prototype Boundary

By default, the live Rust proposal path still treats sigma as a fixed protocol parameter:

  • zebra-crosslink/src/chain.rs defines ZcashCrosslinkParameters::bc_confirmation_depth_sigma.
  • BftBlock::try_from(params, ...) checks that a fixed-sigma BFT block carries exactly that many PoW headers.
  • zebra-crosslink/src/lib.rs proposes and validates the current tip - bc_confirmation_depth_sigma candidate.
  • zebra-crosslink/src/viz.rs exposes chain, BFT, and finality state to the visualizer, but it is not a production telemetry source.

When dynamic_sigma_prototype is explicitly enabled, the proposer uses the prototype dynamic-sigma controller output and emits the tagged dynamic-sigma envelope with prototype evidence. This is only a live wire-path exercise. The evidence source is a fixed fixture, not production telemetry, and must be replaced before the dynamic variant can be enabled by default.

The branch now also includes zebra-crosslink/src/dynamic_sigma.rs, a pure Rust controller that derives conservative coverage and round-failure estimates from raw counters, validates a telemetry window, and selects the same sigma floor as the Quint telemetry fixture. It also has a proposal-carried evidence verifier that rejects a selected sigma outside the configured ladder or below the controller-required floor.

BftBlock::try_from_with_confirmation_depth is available as the selected-sigma block-construction hook, and BftBlock::try_from_with_dynamic_sigma_evidence now composes the evidence verifier with block construction. It validates proposal-carried evidence first, then checks the header count against the selected sigma. The existing BftBlock::try_from(params, ...) path still uses the fixed bc_confirmation_depth_sigma.

A production implementation must replace the fixed parameter at proposal and validation time with this consensus-safe controller output, serialize or commit the evidence in proposals, and populate the controller input from consensus-visible or proposal-verifiable telemetry.

Production Inputs

Quint input Production meaning Current source Missing production work
TotalHashWork Total PoW work observed in the calibration window. Block headers and chain work can be derived from validated PoW headers; the Rust telemetry assembly boundary now requires explicit total-work evidence before raw telemetry can be built. Define the exact window and whether competing side-branch work is included or only best-chain work.
CrosslinkParticipatingHashWork PoW work from blocks whose miners are participating in Crosslink. No complete production source yet; the Rust source contracts derive a work-weighted participating numerator from explicit observations or headers and reject missing participating-work evidence instead of assuming healthy participation. Add an objectively verifiable production participation marker or derive participation from valid Crosslink-finality content in blocks.
EstimatedCoverageRiskPct Conservative upper bound on the non-participating or unseen-work share. Can be computed from total and participating work once both are defined. Add safety margin for hidden work, delayed propagation, peer eclipse, and incomplete fork visibility.
TotalTenderlinkRounds Count of Tenderlink rounds in the measurement window. DynamicSigmaRoundEvent can accumulate started rounds into DynamicSigmaRoundCounters, but live Tenderlink event hooks are not wired yet. Wire durable round-start events from Tenderlink into the counter window.
FailedTenderlinkRounds Rounds that do not decide a value and require recovery. DynamicSigmaRoundEvent can accumulate nil-precommit, stale-proposal, timeout, invalid-proposal, and mixed-evidence failure labels, and validation rejects reason counters that outnumber failed rounds. Wire those labels to live Tenderlink recovery and timeout paths.
EstimatedRoundFailureRatePct Conservative upper bound on failed-round frequency. Derived from assembled round counters, with conservative margins applied by the raw telemetry conversion. Decide smoothing, hysteresis, and window size so transient jitter does not create unstable sigma changes.
MeasuredBlockIntervalVariancePct PoW timing instability over the same window. measured_block_interval_variance_pct_from_headers derives a conservative max adjacent-interval deviation from validated header times, and DynamicSigmaTimedHeaderObservationWindow composes that source with header-derived work participation. The policy-aware timed helper applies the same recent-window/minimum-work guardrail to the participation side, and target spacing can be derived from the active Zebra network upgrade. Decide whether timestamp-manipulation handling should become stricter or be replaced by a calibrated model.
MeasuredObservedReorgDepth Maximum rollback depth observed across best-tip changes in the window. DynamicSigmaBestTipTransition can derive rollback depth from old-tip, new-tip, and common-ancestor heights, but live state hooks are not wired yet. Add a metric that records replaced prefix depth for best-tip changes and side-branch releases.
RollbackRiskPpmAtSigma Modelled rollback probability for each candidate sigma. rollback_risk_curve_from_observed_rollback_depths can derive an empirical ppm exceedance curve from observed rollback-depth windows plus a conservative margin. Define the production window/history policy and decide whether the empirical estimator is sufficient or should be replaced by a calibrated offline model.
ValueAtRiskUnits Economic value exposed to rollback if a finalized point is wrong or delayed. The pure Rust controller now has an explicit DynamicSigmaEconomicExposurePolicy that distinguishes consensus-critical exposure from service-local exposure. Wire a production source if exposure is consensus-critical, or keep service-local exposure outside proposal validity.
MaxAcceptableExpectedLossUnits Governance or operator budget for expected loss. Consensus-critical policy carries this budget into proposal evidence; service-local policy maps to zero consensus exposure. Decide the governance/operator source for consensus-critical budgets, if any.

Hash-Participation Rule

Hash participation is not a Tenderlink voting threshold. It measures whether the PoW stream that finalizers are sampling is representative of the global best-chain race.

The production controller should compute participation as a work-weighted ratio:

participating_hash_work / total_hash_work

The numerator should only include objectively verifiable Crosslink-participating work. Self-reported pool share is not enough. A future implementation could use valid Crosslink finality-update content, a consensus-valid participation marker, or another marker that full nodes can verify from block data.

The denominator must be conservative. If a node cannot see all competing work, the estimator should bias toward lower participation, not higher participation. This is important because hidden or delayed PoW work is exactly the risk that requires a larger sigma.

The Rust prototype now has a pure source-side aggregation boundary for this metric. observed_hash_work_participation takes explicit PoW work observations tagged as either verified-participating or not verified-participating, sums all observed work into the denominator, and only sums verified-participating work into the numerator. Empty observation windows are rejected. This still does not define the production marker; it prevents the next source producer from treating unknown or unverified work as healthy participation. observed_hash_work_participation_with_window_policy adds the missing source window discipline: it selects the most recent bounded observation window, requires a minimum number of observations, and rejects windows whose total observed work is below a configured minimum before the participation percentage can influence sigma. The regression tests include a skewed window where two participating observations are outweighed by one larger non-participating work observation, forcing max sigma. That keeps the input tied to percentage of hash power, not number of observations, blocks, pools, or validators.

hash_work_observation_from_header is the first concrete source adapter for that boundary. It converts a validated PoW header's compact difficulty into work, classifies non-null Crosslink fat pointers as the current objective participation marker, and counts null-marker headers as observed but non-participating work. Invalid header difficulty fails closed instead of being counted as zero or healthy participation. The default adapter is intentionally a prototype marker bridge, but the _with_verifier variants let a production source require stricter fat-pointer validation before a non-null marker counts as verified participation. The verifier is where a deployment can require valid fat pointer contents, signatures, quorum, or a referenced BFT block before assigning verified-participating status.

The controller rule should match the Quint model shape:

  • if participation is at or above the target threshold, hash participation does not raise sigma by itself
  • if participation is below target but above critical, raise sigma to the degraded floor
  • if participation is below the critical threshold, force max sigma and expose a critical status

For diagnostics and proposal telemetry, the Rust controller exposes the observed participation share on DynamicSigmaDecision as a conservative lower-bound percentage. It is derived from the same exact work-threshold check used by the sigma floor, so fractional cases round down rather than implying a target threshold was met when the work comparison would reject it.

Economic Exposure Policy

The controller now makes the expected-loss boundary explicit: DynamicSigmaEconomicExposurePolicy::ConsensusCritical carries value-at-risk and loss-budget units into the same evidence that validators check, while DynamicSigmaEconomicExposurePolicy::ServiceLocal maps to zero consensus exposure. That means service-local risk can drive a local product policy, but it does not silently change the BFT validity rule or make honest validators disagree about a proposal.

If a deployment wants expected loss to affect consensus sigma, the value-at-risk and loss budget must be deterministic or proposal-verifiable. Proposal evidence validation rejects a selected sigma below the economic floor for consensus-critical exposure, while accepting base sigma when the exposure is explicitly service-local and all other floors are healthy.

Consensus-Safety Requirement

Dynamic sigma cannot be an unconstrained local heuristic if it changes what validators are willing to prevote or precommit.

Every validator that evaluates a proposal must be able to derive the same required sigma for the same BFT height and PoW view, or the required telemetry must be included in the BFT proposal and objectively validated. Otherwise two honest validators could disagree on whether the same head - sigma value is valid, creating a liveness failure that looks like a stream change.

This suggests two viable production shapes:

  • Deterministic controller: all inputs are derived from consensus-visible chain data and fixed parameters, so validators recompute the same sigma.
  • Proposal-carried controller evidence: the proposer includes measurement evidence and the BFT validity rules verify that the selected sigma is at least the required floor.

The Rust controller now prototypes the second shape for raw telemetry counters: the verifier reconstructs the conservative telemetry window, rejects selected sigma values below the required floor, and can be composed with BftBlock construction so the selected sigma controls header depth. The same module now exposes select_dynamic_sigma_proposal_evidence* helpers for raw telemetry and production-shaped telemetry components, with and without hysteresis, so proposal code uses the pure controller path instead of reimplementing evidence selection. A production deployment still needs precise validity rules for the source of each raw measurement and live source plumbing for carrying or committing the evidence. The proposal evidence structs now have deterministic Zcash serialization, and DynamicSigmaBftBlockPayload defines a tagged payload envelope containing the evidence followed by the BFT block. The envelope has a validation helper that replays evidence validation and rejects carried blocks that do not match the evidence-selected block.

The live Tenderlink callbacks now route proposal bytes through an explicit payload encoder/decoder. Legacy fixed-sigma BftBlock bytes are still emitted and accepted by the default prototype path. Tagged dynamic-sigma payloads are rejected by default, but a prototype-only config flag enables the proposer to emit the tagged envelope and enables validation callbacks to check the envelope against shared prototype dynamic-sigma parameters before accepting the carried BFT block. The proposer now derives selected_sigma by running the dynamic-sigma controller over prototype telemetry components instead of hard-coding the base sigma or bypassing assembly with raw counters. The decoded payload also carries its selected confirmation depth into the voting-time current-stream staleness check, so prototype dynamic proposals are compared against head - selected_sigma instead of the fixed-sigma sample. This preserves backward compatibility while preventing a dynamic-sigma payload from being silently treated as a fixed-sigma block, and it keeps the dynamic variant behind an explicit opt-in until production telemetry exists. If telemetry assembly or telemetry-to-evidence selection fails, the proposal path now fails closed instead of falling back to the base sigma.

In that prototype-gated path, hash participation already affects payload validity through the carried evidence: if the Crosslink-participating work share is below the configured target, the selected sigma must be at least the degraded floor; if it is below the critical threshold, the selected sigma must be the max floor.

The branch also now has a pure production-shaped telemetry assembly contract. DynamicSigmaTelemetryComponents::try_into_raw_telemetry requires explicit total hash work and explicit Crosslink-participating hash work, rejects inconsistent round counters, and only then builds DynamicSigmaRawTelemetry. This does not solve the source-of-truth problem by itself; it makes the next source-integration step fail closed instead of letting unknown participation or contradictory round metrics look like a healthy calibration window.

Hash-work participation has the same source-shaping pattern: DynamicSigmaHashWorkObservation records the observed work and whether that work has objective Crosslink participation evidence, while observed_hash_work_participation derives the component pair used by DynamicSigmaTelemetryComponents. The tests now cover healthy, degraded, and critical participation shares through this path, so lower verified participation raises or preserves the selected sigma floor instead of lowering it.

Header-derived observations now feed that same path: hash_work_observations_from_headers derives per-header work from difficulty_threshold.to_work(), uses the current non-null Crosslink fat pointer as the participation marker, and keeps all valid header work in the denominator. That makes the "percentage of hash power participating in Crosslink" input work-weighted rather than block-count-weighted.

telemetry_components_from_header_observation_window composes this with the rest of the source contract. A caller can supply PoW headers, Tenderlink round counters, best-tip transitions, variance telemetry, rollback-risk estimates, and economic exposure inputs in one window. The helper derives header work, derives the participation numerator and denominator, validates round counters, derives rollback depth, and returns the same DynamicSigmaTelemetryComponents used by proposal evidence selection. Its _with_verifier variant threads the same custom fat-pointer verifier through the whole header window, so rejected markers still contribute to total work but not to Crosslink-participating work. telemetry_components_from_header_observation_window_with_hash_work_policy then composes that header-derived numerator/denominator pair with DynamicSigmaHashWorkObservationWindowPolicy, allowing a production source to require enough recent headers and enough total observed work before the participation percentage can influence sigma. DynamicSigmaTimedHeaderObservationWindow removes one more manual source input: callers provide the expected target block spacing, and measured_block_interval_variance_pct_from_headers computes a conservative maximum adjacent-interval deviation from header timestamps, rounded up and capped at 100%. The estimator rejects windows with fewer than two headers, zero target spacing, or non-increasing adjacent timestamps rather than turning malformed timing evidence into a healthy variance reading. target_block_spacing_seconds_from_network_upgrade and target_block_spacing_seconds_for_height derive that expected spacing from Zebra's network-upgrade schedule, so production-shaped timed windows do not need to hard-code the 75-second post-Blossom target. telemetry_components_from_timed_header_observation_window_with_hash_work_policy applies the recent-window/minimum-work participation policy in that same timed header path, so deriving timestamp variance from headers does not require bypassing the hash-work participation guardrail.

The source contracts are now composed by telemetry_components_from_observation_window. It accepts hash-work observations, already accumulated Tenderlink round counters, best-tip transitions, block-variance telemetry, rollback-risk estimates, and economic exposure inputs. It derives the participation numerator/denominator, validates round-counter consistency, derives maximum observed rollback depth, and produces DynamicSigmaTelemetryComponents. This still leaves the live production marker, state source, and economic/risk estimators open, but it gives those producers a single pure assembly target.

Rollback-risk estimation now has a pure deterministic baseline: rollback_risk_curve_from_observed_rollback_depths takes a sequence of measurement-window rollback depths and computes, for each sigma in the ladder, the rounded-up parts-per-million frequency of windows whose rollback depth reached that sigma. A bounded margin is then added and capped at one million ppm. This produces a monotone RollbackRiskCurve that can feed the existing economic floor and expected-loss checks. DynamicSigmaRollbackRiskWindowPolicy now makes the history-window policy explicit: callers must provide at least a configured minimum number of rollback-depth windows, only the most recent bounded history is used, and invalid bounds or impossible ppm margins fail closed. DynamicSigmaBestTipTransitionRecorder defines the live recording contract: the first observed best tip seeds the recorder, subsequent tips must provide the common ancestor, and invalid transition evidence is rejected without advancing recorder state. rollback_depth_history_from_transition_windows then defines the history source shape: each measurement window of recorded best-tip transitions contributes one history sample equal to its maximum rollback depth, and invalid transition evidence fails before reaching sigma selection. It is intentionally empirical: a production deployment still has to wire live best-tip transition recording into this recorder and decide whether a calibrated offline model should override or augment this baseline.

CrosslinkDynamicSigmaTelemetry.qnt now mirrors that source boundary in the production-shaped telemetry harness: source hash-work samples derive the total-work denominator and participating numerator, source round counters are checked for consistency, source best-tip transition heights derive observed rollback depth, and adjacent header timestamps derive conservative block-interval variance before the controller checks the sigma floor.

Round telemetry now has a matching event contract. DynamicSigmaRoundEvent records started rounds, decisions, nil-precommit recovery, stale proposals, timeouts, invalid proposals, and mixed evidence into DynamicSigmaRoundCounters. Assembly still accepts direct counters for future deterministic or proposal-carried evidence, but the event API gives live Tenderlink hooks a single place to accumulate the durable window. Validation rejects impossible totals and failure-reason overcounts. The local Tenderlink fork now exposes lifecycle events for started rounds, decisions, nil-precommit recovery, stale proposals, and timeouts; the Zebra prototype records those events into a deduplicated in-process counter window when dynamic_sigma_prototype is enabled. Proposal evidence is still fixture-backed until the remaining PoW and round telemetry sources are promoted to consensus-safe or proposal-verifiable inputs.

The prototype proposal path now uses this same assembly boundary through dynamic_sigma_proposal_evidence_from_telemetry_components: telemetry components are assembled into raw telemetry, the controller selects the required sigma, and the proposal carries the selected value in its evidence. This keeps prototype fixtures aligned with the production-shaped contract while leaving the actual source producers as explicit remaining work.

The fixture itself now enters through the timed-header source shape: prototype PoW headers derive work-weighted participating hash power, the hash-work window policy enforces the recent/minimum-work guardrail, adjacent header timestamps derive block-interval variance, an empty best-tip transition window derives rollback depth 0, round events derive decided-round counters, and the resulting components feed proposal evidence selection. Live producers still need to replace those fixture headers.

Rollback-depth telemetry has the same shape. DynamicSigmaBestTipTransition represents a best-tip change by its previous tip height, new tip height, and common ancestor height. The helper derives rollback depth as the replaced previous-best-chain suffix and rejects impossible ancestor evidence. This gives the controller a precise production-facing input contract for observed reorg depth, but it still needs live state integration that can supply the actual common ancestor for best-tip transitions and side-branch releases.

Failure Modes

The production controller needs guardrails for adversarial telemetry:

  • Fake high participation is unsafe because it can keep sigma too low. This is why participation must be objectively derived from block data.
  • Fake low participation is a liveness attack because it can force max sigma. The protocol should still prefer safety, but operators need observability and hysteresis to distinguish degraded participation from measurement failure.
  • Short windows can oscillate sigma around the threshold. Use explicit windows, hysteresis, and bounded rate of sigma decrease.
  • Local-only value-at-risk estimates can make validators disagree. If expected loss affects consensus validity, the exposure model must be shared or proposal-carried.
  • Hidden hash power cannot be proven absent. The estimator should treat participation as an upper-confidence-bound problem and choose conservative sigma when coverage is uncertain.

The Rust controller now has a pure hysteresis helper for the short-window oscillation case. apply_dynamic_sigma_hysteresis raises immediately when a new window requires a larger sigma, but only lowers one ladder level after a configured number of stable lower-risk windows. The prototype evidence builder can now apply that helper from an explicit hysteresis state before carrying selected_sigma in proposal evidence. Proposal validation remains floor-based: validators reject selected sigma below the telemetry-required floor, while a hysteresis-selected sigma above that floor remains valid. The prototype service now stores in-process hysteresis state and advances it after a dynamic proposal payload is successfully encoded. DynamicSigmaHysteresisParameters and DynamicSigmaHysteresisState now also have deterministic Zcash serialization, so a production source has a stable encoding for persistence or proposal-carried state. The controller API now makes the state source explicit: DynamicSigmaHysteresisStateSource::Disabled selects the raw telemetry-required floor and returns no next state, while DurableLocal and ProposalCarried apply the supplied hysteresis state and return the next state to persist or carry. Production still needs to choose the durable, consensus-safe or proposal-verifiable state source before this becomes a deployed controller rule. The prototype proposal callback uses one proposal plan for both candidate-depth selection and payload encoding, which is the shape needed before that state is promoted beyond the prototype.

CrosslinkDynamicSigmaHysteresis.qnt mirrors that policy with bounded witnesses: participation-driven or reorg-driven sigma increases apply immediately, while recovery to a lower sigma requires stable lower-risk windows and descends one ladder step at a time.

Implementation Acceptance Criteria

A production implementation of the dynamic-sigma variant should provide:

  • a consensus-safe definition of Crosslink-participating PoW work
  • a deterministic or proposal-verifiable computation of total observed work; the pure hash-work source now supports minimum-history, maximum-recent-window, and minimum-total-work policy checks before deriving participation
  • round-start, round-failure, nil-precommit, stale-proposal, and decision counters
  • best-tip rollback-depth telemetry derived from actual fork transitions
  • an explicit rollback-risk estimator for each allowed sigma; the pure controller now includes an empirical observed-depth exceedance estimator and a bounded recent-history window policy fed by recorded best-tip transition windows, but production still needs live state hooks that call the recorder and may need a calibrated model
  • a block-interval variance source; the pure controller now derives a conservative max adjacent-interval deviation from header timestamps and offers a timed header observation-window adapter with the same hash-work policy guardrail, and the target-spacing helper derives the expected spacing from the active network upgrade, while production still needs any stricter calibrated timestamp policy
  • an economic exposure model or a clear decision that expected loss is service-local rather than consensus-critical; the pure controller now has an explicit policy split and tests for both paths, while production still needs a deterministic or proposal-verifiable source if consensus-critical exposure is enabled
  • a durable or proposal-carried hysteresis state source if the dynamic variant should smooth sigma decreases across windows rather than selecting the raw required floor each time; the hysteresis policy/state now have deterministic serialization, but production still needs to configure the typed source policy
  • tests showing that lower hash participation never lowers sigma; the pure Rust controller now covers the bounded Quint telemetry fixture and raw-counter estimate construction, and the prototype-gated Tenderlink payload decoder now rejects dynamic payload evidence whose Crosslink-participating hash-power share requires a higher sigma than the proposer selected. The hash-work observation tests now derive the participation numerator from explicit verified-participating observations and from PoW headers, then cover healthy, degraded, and critical shares through telemetry assembly. The observation-window tests now compose hash-work observations or headers, round counters, and best-tip transitions into telemetry components and reject invalid source counters, invalid header difficulty, or rollback evidence. The custom verifier tests show that stricter marker validation can reject a non-null fat pointer without removing the header from total work. The new pure telemetry assembly tests also reject missing participating-work evidence and inconsistent round counters, the event-counter tests reject failure-reason overcounts, and rollback-depth tests derive the observed reorg-depth input from explicit best-tip transition evidence, but live production source integration still needs tests
  • tests showing that dynamic sigma changes do not make honest validators reject each other's otherwise valid proposals; the pure Rust proposal-evidence verifier and BFT block-construction helper cover identical evidence determinism, evidence serialization, tagged payload encoding, payload/block mismatch rejection, fixed-vs-dynamic payload routing, below-floor rejection, selected-sigma header depth, prototype-gated proposal emission, and selected-sigma voting-time stale checks. The live proposer now runs the controller over a policy-guarded timed-header fixture before selecting sigma, but production telemetry-source tests are still needed
  • Quint coverage connecting the implemented telemetry rules back to CrosslinkDynamicSigmaTelemetry.qnt
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment