You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

474 lines
19 KiB

  1. -------------------- MODULE TendermintAcc_004_draft ---------------------------
  2. (*
  3. A TLA+ specification of a simplified Tendermint consensus, tuned for
  4. fork accountability. The simplifications are as follows:
  5. - the protocol runs for one height, that is, it is one-shot consensus
  6. - this specification focuses on safety, so timeouts are modelled
  7. with non-determinism
  8. - the proposer function is non-determinstic, no fairness is assumed
  9. - the messages by the faulty processes are injected right in the initial states
  10. - every process has the voting power of 1
  11. - hashes are modelled as identity
  12. Having the above assumptions in mind, the specification follows the pseudo-code
  13. of the Tendermint paper: https://arxiv.org/abs/1807.04938
  14. Byzantine processes can demonstrate arbitrary behavior, including
  15. no communication. We show that if agreement is violated, then the Byzantine
  16. processes demonstrate one of the two behaviours:
  17. - Equivocation: a Byzantine process may send two different values
  18. in the same round.
  19. - Amnesia: a Byzantine process may lock a value without unlocking
  20. the previous value that it has locked in the past.
  21. * Version 4. Remove defective processes, fix bugs, collect global evidence.
  22. * Version 3. Modular and parameterized definitions.
  23. * Version 2. Bugfixes in the spec and an inductive invariant.
  24. * Version 1. A preliminary specification.
  25. Zarko Milosevic, Igor Konnov, Informal Systems, 2019-2020.
  26. *)
  27. EXTENDS Integers, FiniteSets
  28. (********************* PROTOCOL PARAMETERS **********************************)
  29. CONSTANTS
  30. Corr, \* the set of correct processes
  31. Faulty, \* the set of Byzantine processes, may be empty
  32. N, \* the total number of processes: correct, defective, and Byzantine
  33. T, \* an upper bound on the number of Byzantine processes
  34. ValidValues, \* the set of valid values, proposed both by correct and faulty
  35. InvalidValues, \* the set of invalid values, never proposed by the correct ones
  36. MaxRound, \* the maximal round number
  37. Proposer \* the proposer function from 0..NRounds to 1..N
  38. ASSUME(N = Cardinality(Corr \union Faulty))
  39. (*************************** DEFINITIONS ************************************)
  40. AllProcs == Corr \union Faulty \* the set of all processes
  41. Rounds == 0..MaxRound \* the set of potential rounds
  42. NilRound == -1 \* a special value to denote a nil round, outside of Rounds
  43. RoundsOrNil == Rounds \union {NilRound}
  44. Values == ValidValues \union InvalidValues \* the set of all values
  45. NilValue == "None" \* a special value for a nil round, outside of Values
  46. ValuesOrNil == Values \union {NilValue}
  47. \* a value hash is modeled as identity
  48. Id(v) == v
  49. \* The validity predicate
  50. IsValid(v) == v \in ValidValues
  51. \* the two thresholds that are used in the algorithm
  52. THRESHOLD1 == T + 1 \* at least one process is not faulty
  53. THRESHOLD2 == 2 * T + 1 \* a quorum when having N > 3 * T
  54. (********************* TYPE ANNOTATIONS FOR APALACHE **************************)
  55. \* the operator for type annotations
  56. a <: b == a
  57. \* the type of message records
  58. MT == [type |-> STRING, src |-> STRING, round |-> Int,
  59. proposal |-> STRING, validRound |-> Int, id |-> STRING]
  60. \* a type annotation for a message
  61. AsMsg(m) == m <: MT
  62. \* a type annotation for a set of messages
  63. SetOfMsgs(S) == S <: {MT}
  64. \* a type annotation for an empty set of messages
  65. EmptyMsgSet == SetOfMsgs({})
  66. (********************* PROTOCOL STATE VARIABLES ******************************)
  67. VARIABLES
  68. round, \* a process round number: Corr -> Rounds
  69. step, \* a process step: Corr -> { "PROPOSE", "PREVOTE", "PRECOMMIT", "DECIDED" }
  70. decision, \* process decision: Corr -> ValuesOrNil
  71. lockedValue, \* a locked value: Corr -> ValuesOrNil
  72. lockedRound, \* a locked round: Corr -> RoundsOrNil
  73. validValue, \* a valid value: Corr -> ValuesOrNil
  74. validRound \* a valid round: Corr -> RoundsOrNil
  75. \* book-keeping variables
  76. VARIABLES
  77. msgsPropose, \* PROPOSE messages broadcast in the system, Rounds -> Messages
  78. msgsPrevote, \* PREVOTE messages broadcast in the system, Rounds -> Messages
  79. msgsPrecommit, \* PRECOMMIT messages broadcast in the system, Rounds -> Messages
  80. evidence, \* the messages that were used by the correct processes to make transitions
  81. action \* we use this variable to see which action was taken
  82. (* to see a type invariant, check TendermintAccInv3 *)
  83. \* a handy definition used in UNCHANGED
  84. vars == <<round, step, decision, lockedValue, lockedRound,
  85. validValue, validRound, evidence, msgsPropose, msgsPrevote, msgsPrecommit>>
  86. (********************* PROTOCOL INITIALIZATION ******************************)
  87. FaultyProposals(r) ==
  88. SetOfMsgs([type: {"PROPOSAL"}, src: Faulty,
  89. round: {r}, proposal: Values, validRound: RoundsOrNil])
  90. AllFaultyProposals ==
  91. SetOfMsgs([type: {"PROPOSAL"}, src: Faulty,
  92. round: Rounds, proposal: Values, validRound: RoundsOrNil])
  93. FaultyPrevotes(r) ==
  94. SetOfMsgs([type: {"PREVOTE"}, src: Faulty, round: {r}, id: Values])
  95. AllFaultyPrevotes ==
  96. SetOfMsgs([type: {"PREVOTE"}, src: Faulty, round: Rounds, id: Values])
  97. FaultyPrecommits(r) ==
  98. SetOfMsgs([type: {"PRECOMMIT"}, src: Faulty, round: {r}, id: Values])
  99. AllFaultyPrecommits ==
  100. SetOfMsgs([type: {"PRECOMMIT"}, src: Faulty, round: Rounds, id: Values])
  101. BenignRoundsInMessages(msgfun) ==
  102. \* the message function never contains a message for a wrong round
  103. \A r \in Rounds:
  104. \A m \in msgfun[r]:
  105. r = m.round
  106. \* The initial states of the protocol. Some faults can be in the system already.
  107. Init ==
  108. /\ round = [p \in Corr |-> 0]
  109. /\ step = [p \in Corr |-> "PROPOSE"]
  110. /\ decision = [p \in Corr |-> NilValue]
  111. /\ lockedValue = [p \in Corr |-> NilValue]
  112. /\ lockedRound = [p \in Corr |-> NilRound]
  113. /\ validValue = [p \in Corr |-> NilValue]
  114. /\ validRound = [p \in Corr |-> NilRound]
  115. /\ msgsPropose \in [Rounds -> SUBSET AllFaultyProposals]
  116. /\ msgsPrevote \in [Rounds -> SUBSET AllFaultyPrevotes]
  117. /\ msgsPrecommit \in [Rounds -> SUBSET AllFaultyPrecommits]
  118. /\ BenignRoundsInMessages(msgsPropose)
  119. /\ BenignRoundsInMessages(msgsPrevote)
  120. /\ BenignRoundsInMessages(msgsPrecommit)
  121. /\ evidence = EmptyMsgSet
  122. /\ action' = "Init"
  123. (************************ MESSAGE PASSING ********************************)
  124. BroadcastProposal(pSrc, pRound, pProposal, pValidRound) ==
  125. LET newMsg ==
  126. AsMsg([type |-> "PROPOSAL", src |-> pSrc, round |-> pRound,
  127. proposal |-> pProposal, validRound |-> pValidRound])
  128. IN
  129. msgsPropose' = [msgsPropose EXCEPT ![pRound] = msgsPropose[pRound] \union {newMsg}]
  130. BroadcastPrevote(pSrc, pRound, pId) ==
  131. LET newMsg == AsMsg([type |-> "PREVOTE",
  132. src |-> pSrc, round |-> pRound, id |-> pId])
  133. IN
  134. msgsPrevote' = [msgsPrevote EXCEPT ![pRound] = msgsPrevote[pRound] \union {newMsg}]
  135. BroadcastPrecommit(pSrc, pRound, pId) ==
  136. LET newMsg == AsMsg([type |-> "PRECOMMIT",
  137. src |-> pSrc, round |-> pRound, id |-> pId])
  138. IN
  139. msgsPrecommit' = [msgsPrecommit EXCEPT ![pRound] = msgsPrecommit[pRound] \union {newMsg}]
  140. (********************* PROTOCOL TRANSITIONS ******************************)
  141. \* lines 12-13
  142. StartRound(p, r) ==
  143. /\ step[p] /= "DECIDED" \* a decided process does not participate in consensus
  144. /\ round' = [round EXCEPT ![p] = r]
  145. /\ step' = [step EXCEPT ![p] = "PROPOSE"]
  146. \* lines 14-19, a proposal may be sent later
  147. InsertProposal(p) ==
  148. LET r == round[p] IN
  149. /\ p = Proposer[r]
  150. /\ step[p] = "PROPOSE"
  151. \* if the proposer is sending a proposal, then there are no other proposals
  152. \* by the correct processes for the same round
  153. /\ \A m \in msgsPropose[r]: m.src /= p
  154. /\ \E v \in ValidValues:
  155. LET proposal == IF validValue[p] /= NilValue THEN validValue[p] ELSE v IN
  156. BroadcastProposal(p, round[p], proposal, validRound[p])
  157. /\ UNCHANGED <<evidence, round, decision, lockedValue, lockedRound,
  158. validValue, step, validRound, msgsPrevote, msgsPrecommit>>
  159. /\ action' = "InsertProposal"
  160. \* lines 22-27
  161. UponProposalInPropose(p) ==
  162. \E v \in Values:
  163. /\ step[p] = "PROPOSE" (* line 22 *)
  164. /\ LET msg ==
  165. AsMsg([type |-> "PROPOSAL", src |-> Proposer[round[p]],
  166. round |-> round[p], proposal |-> v, validRound |-> NilRound]) IN
  167. /\ msg \in msgsPropose[round[p]] \* line 22
  168. /\ evidence' = {msg} \union evidence
  169. /\ LET mid == (* line 23 *)
  170. IF IsValid(v) /\ (lockedRound[p] = NilRound \/ lockedValue[p] = v)
  171. THEN Id(v)
  172. ELSE NilValue
  173. IN
  174. BroadcastPrevote(p, round[p], mid) \* lines 24-26
  175. /\ step' = [step EXCEPT ![p] = "PREVOTE"]
  176. /\ UNCHANGED <<round, decision, lockedValue, lockedRound,
  177. validValue, validRound, msgsPropose, msgsPrecommit>>
  178. /\ action' = "UponProposalInPropose"
  179. \* lines 28-33
  180. UponProposalInProposeAndPrevote(p) ==
  181. \E v \in Values, vr \in Rounds:
  182. /\ step[p] = "PROPOSE" /\ 0 <= vr /\ vr < round[p] \* line 28, the while part
  183. /\ LET msg ==
  184. AsMsg([type |-> "PROPOSAL", src |-> Proposer[round[p]],
  185. round |-> round[p], proposal |-> v, validRound |-> vr])
  186. IN
  187. /\ msg \in msgsPropose[round[p]] \* line 28
  188. /\ LET PV == { m \in msgsPrevote[vr]: m.id = Id(v) } IN
  189. /\ Cardinality(PV) >= THRESHOLD2 \* line 28
  190. /\ evidence' = PV \union {msg} \union evidence
  191. /\ LET mid == (* line 29 *)
  192. IF IsValid(v) /\ (lockedRound[p] <= vr \/ lockedValue[p] = v)
  193. THEN Id(v)
  194. ELSE NilValue
  195. IN
  196. BroadcastPrevote(p, round[p], mid) \* lines 24-26
  197. /\ step' = [step EXCEPT ![p] = "PREVOTE"]
  198. /\ UNCHANGED <<round, decision, lockedValue, lockedRound,
  199. validValue, validRound, msgsPropose, msgsPrecommit>>
  200. /\ action' = "UponProposalInProposeAndPrevote"
  201. \* lines 34-35 + lines 61-64 (onTimeoutPrevote)
  202. UponQuorumOfPrevotesAny(p) ==
  203. /\ step[p] = "PREVOTE" \* line 34 and 61
  204. /\ \E MyEvidence \in SUBSET msgsPrevote[round[p]]:
  205. \* find the unique voters in the evidence
  206. LET Voters == { m.src: m \in MyEvidence } IN
  207. \* compare the number of the unique voters against the threshold
  208. /\ Cardinality(Voters) >= THRESHOLD2 \* line 34
  209. /\ evidence' = MyEvidence \union evidence
  210. /\ BroadcastPrecommit(p, round[p], NilValue)
  211. /\ step' = [step EXCEPT ![p] = "PRECOMMIT"]
  212. /\ UNCHANGED <<round, decision, lockedValue, lockedRound,
  213. validValue, validRound, msgsPropose, msgsPrevote>>
  214. /\ action' = "UponQuorumOfPrevotesAny"
  215. \* lines 36-46
  216. UponProposalInPrevoteOrCommitAndPrevote(p) ==
  217. \E v \in ValidValues, vr \in RoundsOrNil:
  218. /\ step[p] \in {"PREVOTE", "PRECOMMIT"} \* line 36
  219. /\ LET msg ==
  220. AsMsg([type |-> "PROPOSAL", src |-> Proposer[round[p]],
  221. round |-> round[p], proposal |-> v, validRound |-> vr]) IN
  222. /\ msg \in msgsPropose[round[p]] \* line 36
  223. /\ LET PV == { m \in msgsPrevote[round[p]]: m.id = Id(v) } IN
  224. /\ Cardinality(PV) >= THRESHOLD2 \* line 36
  225. /\ evidence' = PV \union {msg} \union evidence
  226. /\ IF step[p] = "PREVOTE"
  227. THEN \* lines 38-41:
  228. /\ lockedValue' = [lockedValue EXCEPT ![p] = v]
  229. /\ lockedRound' = [lockedRound EXCEPT ![p] = round[p]]
  230. /\ BroadcastPrecommit(p, round[p], Id(v))
  231. /\ step' = [step EXCEPT ![p] = "PRECOMMIT"]
  232. ELSE
  233. UNCHANGED <<lockedValue, lockedRound, msgsPrecommit, step>>
  234. \* lines 42-43
  235. /\ validValue' = [validValue EXCEPT ![p] = v]
  236. /\ validRound' = [validRound EXCEPT ![p] = round[p]]
  237. /\ UNCHANGED <<round, decision, msgsPropose, msgsPrevote>>
  238. /\ action' = "UponProposalInPrevoteOrCommitAndPrevote"
  239. \* lines 47-48 + 65-67 (onTimeoutPrecommit)
  240. UponQuorumOfPrecommitsAny(p) ==
  241. /\ \E MyEvidence \in SUBSET msgsPrecommit[round[p]]:
  242. \* find the unique committers in the evidence
  243. LET Committers == { m.src: m \in MyEvidence } IN
  244. \* compare the number of the unique committers against the threshold
  245. /\ Cardinality(Committers) >= THRESHOLD2 \* line 47
  246. /\ evidence' = MyEvidence \union evidence
  247. /\ round[p] + 1 \in Rounds
  248. /\ StartRound(p, round[p] + 1)
  249. /\ UNCHANGED <<decision, lockedValue, lockedRound, validValue,
  250. validRound, msgsPropose, msgsPrevote, msgsPrecommit>>
  251. /\ action' = "UponQuorumOfPrecommitsAny"
  252. \* lines 49-54
  253. UponProposalInPrecommitNoDecision(p) ==
  254. /\ decision[p] = NilValue \* line 49
  255. /\ \E v \in ValidValues (* line 50*) , r \in Rounds, vr \in RoundsOrNil:
  256. /\ LET msg == AsMsg([type |-> "PROPOSAL", src |-> Proposer[r],
  257. round |-> r, proposal |-> v, validRound |-> vr]) IN
  258. /\ msg \in msgsPropose[r] \* line 49
  259. /\ LET PV == { m \in msgsPrecommit[r]: m.id = Id(v) } IN
  260. /\ Cardinality(PV) >= THRESHOLD2 \* line 49
  261. /\ evidence' = PV \union {msg} \union evidence
  262. /\ decision' = [decision EXCEPT ![p] = v] \* update the decision, line 51
  263. \* The original algorithm does not have 'DECIDED', but it increments the height.
  264. \* We introduced 'DECIDED' here to prevent the process from changing its decision.
  265. /\ step' = [step EXCEPT ![p] = "DECIDED"]
  266. /\ UNCHANGED <<round, lockedValue, lockedRound, validValue,
  267. validRound, msgsPropose, msgsPrevote, msgsPrecommit>>
  268. /\ action' = "UponProposalInPrecommitNoDecision"
  269. \* the actions below are not essential for safety, but added for completeness
  270. \* lines 20-21 + 57-60
  271. OnTimeoutPropose(p) ==
  272. /\ step[p] = "PROPOSE"
  273. /\ p /= Proposer[round[p]]
  274. /\ BroadcastPrevote(p, round[p], NilValue)
  275. /\ step' = [step EXCEPT ![p] = "PREVOTE"]
  276. /\ UNCHANGED <<round, lockedValue, lockedRound, validValue,
  277. validRound, decision, evidence, msgsPropose, msgsPrecommit>>
  278. /\ action' = "OnTimeoutPropose"
  279. \* lines 44-46
  280. OnQuorumOfNilPrevotes(p) ==
  281. /\ step[p] = "PREVOTE"
  282. /\ LET PV == { m \in msgsPrevote[round[p]]: m.id = Id(NilValue) } IN
  283. /\ Cardinality(PV) >= THRESHOLD2 \* line 36
  284. /\ evidence' = PV \union evidence
  285. /\ BroadcastPrecommit(p, round[p], Id(NilValue))
  286. /\ step' = [step EXCEPT ![p] = "PREVOTE"]
  287. /\ UNCHANGED <<round, lockedValue, lockedRound, validValue,
  288. validRound, decision, msgsPropose, msgsPrevote>>
  289. /\ action' = "OnQuorumOfNilPrevotes"
  290. \* lines 55-56
  291. OnRoundCatchup(p) ==
  292. \E r \in {rr \in Rounds: rr > round[p]}:
  293. LET RoundMsgs == msgsPropose[r] \union msgsPrevote[r] \union msgsPrecommit[r] IN
  294. \E MyEvidence \in SUBSET RoundMsgs:
  295. LET Faster == { m.src: m \in MyEvidence } IN
  296. /\ Cardinality(Faster) >= THRESHOLD1
  297. /\ evidence' = MyEvidence \union evidence
  298. /\ StartRound(p, r)
  299. /\ UNCHANGED <<decision, lockedValue, lockedRound, validValue,
  300. validRound, msgsPropose, msgsPrevote, msgsPrecommit>>
  301. /\ action' = "OnRoundCatchup"
  302. (*
  303. * A system transition. In this specificatiom, the system may eventually deadlock,
  304. * e.g., when all processes decide. This is expected behavior, as we focus on safety.
  305. *)
  306. Next ==
  307. \E p \in Corr:
  308. \/ InsertProposal(p)
  309. \/ UponProposalInPropose(p)
  310. \/ UponProposalInProposeAndPrevote(p)
  311. \/ UponQuorumOfPrevotesAny(p)
  312. \/ UponProposalInPrevoteOrCommitAndPrevote(p)
  313. \/ UponQuorumOfPrecommitsAny(p)
  314. \/ UponProposalInPrecommitNoDecision(p)
  315. \* the actions below are not essential for safety, but added for completeness
  316. \/ OnTimeoutPropose(p)
  317. \/ OnQuorumOfNilPrevotes(p)
  318. \/ OnRoundCatchup(p)
  319. (**************************** FORK SCENARIOS ***************************)
  320. \* equivocation by a process p
  321. EquivocationBy(p) ==
  322. \E m1, m2 \in evidence:
  323. /\ m1 /= m2
  324. /\ m1.src = p
  325. /\ m2.src = p
  326. /\ m1.round = m2.round
  327. /\ m1.type = m2.type
  328. \* amnesic behavior by a process p
  329. AmnesiaBy(p) ==
  330. \E r1, r2 \in Rounds:
  331. /\ r1 < r2
  332. /\ \E v1, v2 \in ValidValues:
  333. /\ v1 /= v2
  334. /\ AsMsg([type |-> "PRECOMMIT", src |-> p,
  335. round |-> r1, id |-> Id(v1)]) \in evidence
  336. /\ AsMsg([type |-> "PREVOTE", src |-> p,
  337. round |-> r2, id |-> Id(v2)]) \in evidence
  338. /\ \A r \in { rnd \in Rounds: r1 <= rnd /\ rnd < r2 }:
  339. LET prevotes ==
  340. { m \in evidence:
  341. m.type = "PREVOTE" /\ m.round = r /\ m.id = Id(v2) }
  342. IN
  343. Cardinality(prevotes) < THRESHOLD2
  344. (******************************** PROPERTIES ***************************************)
  345. \* the safety property -- agreement
  346. Agreement ==
  347. \A p, q \in Corr:
  348. \/ decision[p] = NilValue
  349. \/ decision[q] = NilValue
  350. \/ decision[p] = decision[q]
  351. \* the protocol validity
  352. Validity ==
  353. \A p \in Corr: decision[p] \in ValidValues \union {NilValue}
  354. (*
  355. The protocol safety. Two cases are possible:
  356. 1. There is no fork, that is, Agreement holds true.
  357. 2. A subset of faulty processes demonstrates equivocation or amnesia.
  358. *)
  359. Accountability ==
  360. \/ Agreement
  361. \/ \E Detectable \in SUBSET Faulty:
  362. /\ Cardinality(Detectable) >= THRESHOLD1
  363. /\ \A p \in Detectable:
  364. EquivocationBy(p) \/ AmnesiaBy(p)
  365. (****************** FALSE INVARIANTS TO PRODUCE EXAMPLES ***********************)
  366. \* This property is violated. You can check it to see how amnesic behavior
  367. \* appears in the evidence variable.
  368. NoAmnesia ==
  369. \A p \in Faulty: ~AmnesiaBy(p)
  370. \* This property is violated. You can check it to see an example of equivocation.
  371. NoEquivocation ==
  372. \A p \in Faulty: ~EquivocationBy(p)
  373. \* This property is violated. You can check it to see an example of agreement.
  374. \* It is not exactly ~Agreement, as we do not want to see the states where
  375. \* decision[p] = NilValue
  376. NoAgreement ==
  377. \A p, q \in Corr:
  378. (p /= q /\ decision[p] /= NilValue /\ decision[q] /= NilValue)
  379. => decision[p] /= decision[q]
  380. \* Either agreement holds, or the faulty processes indeed demonstrate amnesia.
  381. \* This property is violated. A counterexample should demonstrate equivocation.
  382. AgreementOrAmnesia ==
  383. Agreement \/ (\A p \in Faulty: AmnesiaBy(p))
  384. \* We expect this property to be violated. It shows us a protocol run,
  385. \* where one faulty process demonstrates amnesia without equivocation.
  386. \* However, the absence of amnesia
  387. \* is a tough constraint for Apalache. It has not reported a counterexample
  388. \* for n=4,f=2, length <= 5.
  389. ShowMeAmnesiaWithoutEquivocation ==
  390. (~Agreement /\ \E p \in Faulty: ~EquivocationBy(p))
  391. => \A p \in Faulty: ~AmnesiaBy(p)
  392. \* This property is violated on n=4,f=2, length=4 in less than 10 min.
  393. \* Two faulty processes may demonstrate amnesia without equivocation.
  394. AmnesiaImpliesEquivocation ==
  395. (\E p \in Faulty: AmnesiaBy(p)) => (\E q \in Faulty: EquivocationBy(q))
  396. (*
  397. This property is violated. You can check it to see that all correct processes
  398. may reach MaxRound without making a decision.
  399. *)
  400. NeverUndecidedInMaxRound ==
  401. LET AllInMax == \A p \in Corr: round[p] = MaxRound
  402. AllDecided == \A p \in Corr: decision[p] /= NilValue
  403. IN
  404. AllInMax => AllDecided
  405. =============================================================================