Files
tendermint/spec/abci++/v4.md
T

6.0 KiB
Raw Blame History

Tendermint v4 Markdown pseudocode

This is a multi-threaded implementation of ABCI++, where ProcessProposal starts when the proposal is received, but ends before precommitting.

Initialization

h_p ← 0
round_p ← 0
step_p is one of {propose, prevote, precommit}
decision_p ← Vector()
lockedValue_p ← nil
validValue_p ← nil
validRound_p ← -1

StartRound(round)

function startRound(round) {
    round_p ← round
    step_p ← propose
    if proposer(h_p, round_p) = p {
        if validValue_p != nil {
            proposal ← validValue_p
        } else {
            txdata ← mempool.GetBlock()
            // getUnpreparedBlockProposal fills in header
            unpreparedProposal ← getUnpreparedBlockProposal(txdata)
            proposal ← ABCI.PrepareProposal(unpreparedProposal)
        }
        broadcast ⟨PROPOSAL, h_p, round_p, proposal, validRound_p⟩
    } else {
        schedule OnTimeoutPropose(h_p,round_p) to be executed after timeoutPropose(round_p)
    }
}

ReceiveProposal

In the case where the local node is not locked on any round, the following is ran:

upon ⟨PROPOSAL, h_p, round_p, v, −1) from proposer(h_p, round_p) while step_p = propose do {
    if valid(v) ∧ ABCI.VerifyHeader(h_p, v.header) ∧ (lockedRound_p = −1 ∨ lockedValue_p = v) {
        // We fork process proposal into a parallel process
        Fork ABCI.ProcessProposal(h_p, v)
        broadcast ⟨PREVOTE, h_p, round_p, id(v)⟩ 
    } else {
        broadcast ⟨PREVOTE, h_p, round_p, nil⟩ 
    }
    step_p ← prevote
}

In the case where the node is locked on a round, the following is ran:

upon ⟨PROPOSAL, h_p, round_p, v, vr⟩
  from proposer(h_p, round_p)
  AND 2f + 1 ⟨PREVOTE, h_p, vr, id(v)⟩ 
  while step_p = propose ∧ (vr ≥ 0 ∧ vr < round_p) do {
    if valid(v) ∧ ABCI.VerifyHeader(h_p, v.header) ∧ (lockedRound_p ≤ vr ∨ lockedValue_p = v) {
        // We fork process proposal into a parallel process
        Fork ABCI.ProcessProposal(h_p, v)
        broadcast ⟨PREVOTE, h_p, round_p, id(v)⟩
    } else {
        broadcast ⟨PREVOTE, h_p, round_p, nil⟩
    }
    step_p ← prevote
}

Prevote timeout

Upon receiving 2f + 1 prevotes, setup a timeout.

upon 2f + 1 ⟨PREVOTE, h_p, vr, -1⟩ 
  with step_p = prevote for the first time, do {
    schedule OnTimeoutPrevote(h_p, round_p) to be executed after timeoutPrevote(round_p)
}

with OnTimeoutPrevote defined as:

def OnTimeoutPrevote(height, round) {
    if (height = h_p && round = round_p && step_p = prevote) {
        // Join the ProcessProposal, and output any evidence in case it has some.
        processProposalOutput ← Join ABCI.ProcessProposal(h_p, v)
        for evidence in processProposalOutput.evidence_list {
            broadcast ⟨EVIDENCE, evidence⟩ 
        }

        precommit_extension ← ABCI.ExtendVote(h_p, round_p, nil)
        broadcast ⟨PRECOMMIT, h_p, round_p, nil, precommit_extension⟩
        step_p ← precommit
    }
}

Receiving enough prevotes to precommit

The following code is ran upon receiving 2f + 1 prevotes for the same block

upon ⟨PROPOSAL, h_p, round_p, v, *⟩
  from proposer(h_p, round_p)
  AND 2f + 1 ⟨PREVOTE, h_p, vr, id(v)⟩ 
while valid(v) ∧ step_p >= prevote for the first time do {
    if (step_p = prevote) {
        lockedValue_p ← v
        lockedRound_p ← round_p
        processProposalOutput ← Join ABCI.ProcessProposal(h_p, v)
        // If the proposal is valid precommit as before.
        // If it was invalid, precommit nil.
        // Note that ABCI.ProcessProposal(h_p, v).accept is deterministic for all honest nodes.
        precommit_value ← nil
        if processProposalOutput.accept {
            precommit_value ← id(v)
        }
        precommit_extension ← ABCI.ExtendVote(h_p, round_p, precommit_value)
        broadcast ⟨PRECOMMIT, h_p, round_p, precommit_value, precommit_extension⟩
        for evidence in processProposalOutput.evidence_list {
            broadcast ⟨EVIDENCE, evidence⟩ 
        }

        step_p ← precommit
    }
    validValue_p ← v
    validRound_p ← round_p
}

And upon receiving 2f + 1 prevotes for nil:

upon 2f + 1 ⟨PREVOTE, h_p, round_p, nil⟩ 
  while step_p = prevote do {
    // Join ABCI.ProcessProposal, and broadcast any evidence if it exists.
    processProposalOutput ← Join ABCI.ProcessProposal(h_p, v)
    for evidence in processProposalOutput.evidence_list {
        broadcast ⟨EVIDENCE, evidence⟩ 
    }

    precommit_extension ← ABCI.ExtendVote(h_p, round_p, nil)
    broadcast ⟨PRECOMMIT, h_p, round_p, nil, precommit_extension⟩
    step_p ← precommit
}

Upon receiving a precommit

Upon receiving a precommit precommit, we ensure that ABCI.VerifyVoteExtension(precommit.precommit_extension) = true before accepting the precommit. This is akin to how we check the signature on precommits normally, hence its not wrapped in the syntax of methods from the paper.

Precommit timeout

Upon receiving 2f + 1 precommits, setup a timeout.

upon 2f + 1 ⟨PRECOMMIT, h_p, vr, *⟩ for the first time, do {
    schedule OnTimeoutPrecommit(h_p, round_p) to be executed after timeoutPrecommit(round_p)
}

with OnTimeoutPrecommit defined as:

def OnTimeoutPrecommit(height, round) {
    if (height = h_p && round = round_p) {
        StartRound(round_p + 1)
    }
}

Upon Receiving 2f + 1 precommits

The following code is ran upon receiving 2f + 1 precommits for the same block

upon ⟨PROPOSAL, h_p, r, v, *⟩
  from proposer(h_p, r)
  AND 2f + 1 ⟨ PRECOMMIT, h_p, r, id(v)⟩ 
  while decision_p[h_p] = nil do {
    if (valid(v)) {
        decision_p[h_p] ← v
        h_p ← h_p + 1
        reset lockedRound_p, lockedValue_p,validRound_p and validValue_p to initial values
        ABCI.FinalizeBlock(id(v))
        StartRound(0)
    }
}

If we don't see 2f + 1 precommits for the same block, we wait until we get 2f + 1 precommits, and the timeout occurs.