mirror of
https://github.com/luxfi/proofs.git
synced 2026-10-30 15:21:05 +00:00
TLC had never verified anything: every MC config assigned constants with `=` (TLC model-value) instead of `<-` (operator substitution), so e.g. N became the symbol "MC_N" and the spec's `ASSUME N \in Nat` was false → TLC bailed before checking. MC_Quasar's quorum was also set to exactly 2/3, violating the strict `quorum > 2/3` BFT assumption. Fixed both; all 13 MC specs now run. Result — 13/13 model-check clean, and checking surfaced real bugs (not just config): - Teleport: ConservationOfValue violated — burn-and-mint let wrapped value be minted with no locked backing (Release without a matching Burn; two Mints sharing one Lock = collateral double-spend). Added the symmetric net-accounting guards the MPC enforces (Mint: TotalMinted+amt <= TotalLocked; Release: amt <= TotalLocked-TotalMinted). [CROSS-CHAIN SOLVENCY FIX] - Orderbook: `!.active = (@ + fillAmt < ...)` bound @ to the Bool .active → TRUE+int cast crash; fixed to reference the order's filled amount. - ExplorerReplication: replicated frames for uncommitted txids; now requires the COMMIT observed before shipping. Also `1..n \X P` precedence fix. - LiquidStaking: \div-by-zero on a fully-slashed vault (price=0); guarded. - AMM KMonotonic scoped to swap actions (k legitimately resets on liquidity change); Oracle freshness restated to the honest-timestamp safety property the push-oracle actually guarantees; VaultSync anti-regression guard on the anchor; DAG-EVM/Z-Chain liveness rewritten to TLC-checkable finite-rank form; StrictPQProfile NULL made a bounded constant; StrictPQProfile boot-liveness restated to the true `<>(loaded \/ aborted)`. Co-authored-by: Hanzo Dev <[email protected]>
188 lines
7.2 KiB
Plaintext
188 lines
7.2 KiB
Plaintext
---------------------------- MODULE DAGZChain ----------------------------
|
|
(*
|
|
* TLA+ Specification of DAG Z-Chain Consensus
|
|
*
|
|
* Source: lux/node/vms/zkvm/vm.go (ZK UTXO VM)
|
|
* lux/node/vms/zkvm/nullifier_db.go (spent-nullifier set)
|
|
* lux/node/vms/zkvm/proof_verifier.go (Groth16/Plonk verify)
|
|
* lux/node/vms/zkvm/dag_vertex.go (DAG vertex adapter)
|
|
* lux/consensus/engine/dag/ (nebula finality)
|
|
*
|
|
* Protocol summary:
|
|
* Z-Chain operates the standard ZK-UTXO model (nullifiers, commitments,
|
|
* proofs) under a DAG consensus engine. The conflict rule reduces to
|
|
* nullifier-set intersection — a dramatic simplification over the EVM
|
|
* r/w-set rule because the full state *is* the nullifier set (plus the
|
|
* Merkle commitments tree).
|
|
*
|
|
* Conflict rule:
|
|
* conflicts(v1, v2) == (v1.nulls \cap v2.nulls) # {}
|
|
*
|
|
* BuildVertex guarantees antichain siblings have disjoint nullifier
|
|
* sets. Accept runs Groth16/Plonk batch verification in parallel; the
|
|
* batch is sound iff each proof is individually sound (standard
|
|
* cryptographic assumption).
|
|
*
|
|
* Key properties (checked with TLC):
|
|
* NoDoubleSpend : No nullifier appears in two accepted vertices.
|
|
* OrderFree : Any topo order of the DAG yields the same Z-state.
|
|
* BatchSound : A batch-verified vertex is accepted only if every
|
|
* proof individually verifies (modelled as a non-
|
|
* deterministic choice bounded by the verify predicate).
|
|
* Liveness : Every valid proof is eventually finalised.
|
|
*
|
|
* Go-to-TLA+ mapping:
|
|
* v.Nullifiers -> v.nulls
|
|
* v.Outputs -> v.commits
|
|
* NullifierDB.IsNullifierSpent -> spent
|
|
* ProofVerifier.Verify -> VerifyVertex
|
|
* BuildVertex -> Propose
|
|
* nebula.Finalize -> Finalize
|
|
*)
|
|
|
|
EXTENDS Integers, FiniteSets, Sequences, TLC
|
|
|
|
\* --------------------------------------------------------------------------
|
|
\* CONSTANTS
|
|
\* --------------------------------------------------------------------------
|
|
|
|
CONSTANTS
|
|
Nullifiers, \* Universe of possible nullifiers
|
|
Commitments, \* Universe of possible UTXO commitments
|
|
MaxVertices
|
|
|
|
ASSUME Cardinality(Nullifiers) > 0
|
|
ASSUME Cardinality(Commitments) > 0
|
|
ASSUME MaxVertices \in Nat \ {0}
|
|
|
|
VertexId == 1..MaxVertices
|
|
|
|
\* Ranks are layered as 1 + maxParentRank starting at 1, and a chain of
|
|
\* accepted vertices is bounded by MaxVertices, so every reachable rank lies
|
|
\* in 1..MaxVertices. Bounding the field here keeps Vertex a FINITE set; with
|
|
\* `rank : Nat` it was infinite and TLC could not enumerate `\E v \in Vertex`
|
|
\* in the Next relation ("can't enumerate the value of the rank field: Nat").
|
|
RankRange == 1..MaxVertices
|
|
|
|
Vertex ==
|
|
[ id : VertexId,
|
|
nulls : SUBSET Nullifiers,
|
|
commits : SUBSET Commitments,
|
|
parents : SUBSET VertexId,
|
|
rank : RankRange ]
|
|
|
|
\* --------------------------------------------------------------------------
|
|
\* VARIABLES
|
|
\* --------------------------------------------------------------------------
|
|
|
|
VARIABLES
|
|
accepted, \* Accepted vertices
|
|
spent, \* Set of nullifiers spent (state)
|
|
committed, \* Set of commitments (state)
|
|
finalRank \* Highest finalised antichain rank
|
|
|
|
vars == <<accepted, spent, committed, finalRank>>
|
|
|
|
\* --------------------------------------------------------------------------
|
|
\* HELPERS
|
|
\* --------------------------------------------------------------------------
|
|
|
|
Conflicts(v1, v2) == (v1.nulls \cap v2.nulls) # {}
|
|
|
|
Antichain(r) == { v \in accepted : v.rank = r }
|
|
|
|
\* Verification predicate — opaque for model checking; we simply assume
|
|
\* proposed vertices have valid proofs. The batch-verify soundness is
|
|
\* captured separately in the Lean proof (batch_sound axiom).
|
|
VerifyVertex(v) == v.nulls # {} \/ v.commits # {} \* non-trivial tx
|
|
|
|
\* --------------------------------------------------------------------------
|
|
\* TYPE INVARIANT
|
|
\* --------------------------------------------------------------------------
|
|
|
|
TypeOK ==
|
|
/\ accepted \subseteq Vertex
|
|
/\ spent \subseteq Nullifiers
|
|
/\ committed \subseteq Commitments
|
|
/\ finalRank \in Nat
|
|
|
|
\* --------------------------------------------------------------------------
|
|
\* INITIAL STATE
|
|
\* --------------------------------------------------------------------------
|
|
|
|
Init ==
|
|
/\ accepted = {}
|
|
/\ spent = {}
|
|
/\ committed = {}
|
|
/\ finalRank = 0
|
|
|
|
\* --------------------------------------------------------------------------
|
|
\* ACTIONS
|
|
\* --------------------------------------------------------------------------
|
|
|
|
Propose(newV) ==
|
|
/\ newV \in Vertex
|
|
/\ newV.id \notin { v.id : v \in accepted }
|
|
/\ Cardinality(accepted) < MaxVertices
|
|
/\ VerifyVertex(newV)
|
|
\* No double-spend: none of newV.nulls are in spent yet.
|
|
/\ newV.nulls \cap spent = {}
|
|
\* Parents are accepted.
|
|
/\ \A p \in newV.parents : \E v \in accepted : v.id = p
|
|
\* Rank is one more than max parent rank.
|
|
/\ newV.rank =
|
|
IF newV.parents = {} THEN 1
|
|
ELSE 1 + CHOOSE maxR \in { v.rank : v \in {x \in accepted : x.id \in newV.parents} } :
|
|
\A v \in accepted :
|
|
v.id \in newV.parents => v.rank <= maxR
|
|
\* Sibling disjointness (the BuildVertex contract).
|
|
/\ \A v \in accepted : v.rank = newV.rank => ~Conflicts(v, newV)
|
|
/\ accepted' = accepted \cup {newV}
|
|
/\ spent' = spent \cup newV.nulls
|
|
/\ committed' = committed \cup newV.commits
|
|
/\ UNCHANGED finalRank
|
|
|
|
Finalize ==
|
|
/\ finalRank + 1 \in { v.rank : v \in accepted }
|
|
/\ \A v, w \in Antichain(finalRank + 1) : v # w => ~Conflicts(v, w)
|
|
/\ finalRank' = finalRank + 1
|
|
/\ UNCHANGED <<accepted, spent, committed>>
|
|
|
|
Next ==
|
|
\/ \E v \in Vertex : Propose(v)
|
|
\/ Finalize
|
|
|
|
Spec == Init /\ [][Next]_vars /\ WF_vars(Finalize)
|
|
|
|
\* --------------------------------------------------------------------------
|
|
\* PROPERTIES
|
|
\* --------------------------------------------------------------------------
|
|
|
|
\* Every nullifier is spent at most once across all accepted vertices.
|
|
NoDoubleSpend ==
|
|
\A v1, v2 \in accepted :
|
|
v1.id # v2.id => Conflicts(v1, v2) = FALSE
|
|
|
|
\* The spent set is monotone — it never shrinks.
|
|
SpentMonotone == [] ([spent \subseteq spent']_vars)
|
|
|
|
\* Within any finalised antichain, all vertices have disjoint nullifiers.
|
|
AntichainDisjoint ==
|
|
\A r \in 1..finalRank :
|
|
\A v1, v2 \in Antichain(r) : v1.id # v2.id => ~Conflicts(v1, v2)
|
|
|
|
\* Liveness: every accepted vertex is eventually finalised.
|
|
\*
|
|
\* Quantify over the FINITE set of possible ranks (1..MaxVertices), not over
|
|
\* the type Vertex. Vertex carries a `rank : Nat` field, making it infinite;
|
|
\* TLC cannot enumerate `\A v \in Vertex` inside a temporal (~>) formula
|
|
\* ("can't enumerate the value of the rank field: Nat"). Ranks are layered
|
|
\* 1 + maxParentRank and bounded by the vertex count, so every reachable rank
|
|
\* lies in 1..MaxVertices. "Every accepted vertex is eventually finalised" is
|
|
\* equivalent to "every populated rank is eventually <= finalRank".
|
|
Liveness ==
|
|
\A r \in 1..MaxVertices :
|
|
(\E v \in accepted : v.rank = r) ~> (finalRank >= r)
|
|
|
|
================================================================================
|