Along with a helper that checks which messages have already been sent, this is the prepare phase. The precondition is that we haven’t already sent any prepare messages for this timestamp. In practice, this means that each timestamp is unique and attempted only once. This is a more restrictive condition than is strictly needed — it isn’t even possible to implement the algorithm the way it is shown as we would need to see messages that other proposers sent. That’s okay! The implementation doesn’t have to exactly match the specification. It is usually desirable to make the specification as simple as possible. Lamport uses the same trick in his Paxos specification. Choosing the level of abstraction is a hard part of writing specifications. This specification also says nothing about durability or recovering from crashes. Creating such a specification is possible but would be more complicated.

The prepare phase here is identical to Paxos, except that we also keep a record in the history to be able to check later that our register is linearizable.

Here again when processing a prepare message, the Gryadka algorithm is the same as a plain-old Paxos write-once register. The precondition is that the acceptor’s current prepared timestamp must be lower than the received timestamp. Remember that if any precondition is false, none of these actions are taken. One other thing to note is that a message is sent back to the proposer, but the received message that initiated this action isn’t consumed. That simulates network duplication — we rely on the precondition for correctness.

After receiving a quorum of “promise” messages that satisfy the old value of our operation, the proposer sends out the “accept” messages. This is the difference between Gryadka and a write-once Paxos register. The value to be accepted is the new value of the CAS operation, while in Paxos we would send the PromisedValue as the value to be accepted (or if that was some “null” value, we would accept any value we desire). This is a tiny change, but it completely changes the algorithm. In my opinion, this is why distributed systems are hard to reason about — the effects of one small change have a massive effect on the overall system.

One thing to note here is that PromisedValue is always a value because we initialized the system with an initial value at all nodes. Incorporating a “null” value would also be possible but would complicate the specification here.

Processing the accept message at a given acceptor is straightforward, the timestamp of the message must match the timestamp the acceptor is prepared for. If it does match, the acceptor’s state is updated and an “accepted” message is sent back to the proposer.

After receiving a quorum of “accepted” messages, the proposer can respond back to the client that the value has been changed. This finalizes the operation, so the successful response is recorded in the history. That’s it! The core of the algorithm is only a few dozen lines of math and much easier to digest than the python code in the original post.

One thing you might notice is that this specification doesn’t include any sort of negative acknowledgement. Any operation that never completes successfully — either because of message delays, preconditions not being met, or any other reason — is simply never responded to. Again, that’s a deliberate decision to simplify the specification. You probably wouldn’t implement a system this way in the real world. We could extend the specification to simulate NACKs, but it doesn’t change the essence of the algorithm.