123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895(*****************************************************************************)(* *)(* Open Source License *)(* Copyright (c) 2021 Nomadic Labs <contact@nomadic-labs.com> *)(* *)(* Permission is hereby granted, free of charge, to any person obtaining a *)(* copy of this software and associated documentation files (the "Software"),*)(* to deal in the Software without restriction, including without limitation *)(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *)(* and/or sell copies of the Software, and to permit persons to whom the *)(* Software is furnished to do so, subject to the following conditions: *)(* *)(* The above copyright notice and this permission notice shall be included *)(* in all copies or substantial portions of the Software. *)(* *)(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*)(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *)(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *)(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*)(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *)(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *)(* DEALINGS IN THE SOFTWARE. *)(* *)(*****************************************************************************)openProtocolopenAlpha_contextopenBaking_stateopenBaking_actionsmoduleEvents=Baking_events.State_transitionsletdo_nothingstate=Lwt.return(state,Do_nothing)typeproposal_acceptance=Invalid|Outdated_proposal|Valid_proposalletis_acceptable_proposal_for_current_levelstate(proposal:Baking_state.proposal)=letcurrent_round=state.round_state.current_roundinifRound.(current_round<proposal.block.round)thenEvents.(emitunexpected_proposal_round(current_round,proposal.block.round))>>=fun()->Lwt.returnInvalidelseifRound.(current_round>proposal.block.round)thenLwt.returnOutdated_proposalelse(* current_round = proposal.round *)letprevious_proposal=state.level_state.latest_proposalinifRound.(proposal.block.round=previous_proposal.block.round)&&Block_hash.(proposal.block.hash<>previous_proposal.block.hash)&&Block_hash.(proposal.predecessor.hash=previous_proposal.predecessor.hash)then(* An existing proposal was found at the same round: the
proposal is bad and should be punished by the accuser *)Events.(emitproposal_for_round_already_seen(proposal.block.hash,current_round,previous_proposal.block.hash))>>=fun()->Lwt.returnInvalidelse(* current_round = proposal.block.round ∧
proposal.block.round <> previous_proposal.block.round
=>
proposal.block.round > previous_proposal.block.round
The proposal has the expected round and the previous proposal
is a predecessor therefore the proposal is valid *)Lwt.returnValid_proposalletmake_consensus_liststateproposal=(* TODO efficiently iterate on the slot map instead of removing
duplicate endorsements *)letlevel=Raw_level.of_int32state.level_state.current_level|>function|Okl->l|_->assertfalseinletround=proposal.block.roundinletblock_payload_hash=proposal.block.payload_hashinSlotMap.fold(fun_slot(consensus_key_and_delegate,slots)acc->(consensus_key_and_delegate,{slot=slots.first_slot;level;round;block_payload_hash})::acc)state.level_state.delegate_slots.own_delegate_slots[]|>List.sort_uniqcompare(* If we do not have any slots, we won't inject any operation but we
will still participate to determine an elected block *)letmake_preendorse_actionstateproposal=letpreendorsements:(consensus_key_and_delegate*consensus_content)list=make_consensus_liststateproposalinInject_preendorsements{preendorsements}letupdate_proposal~is_proposal_appliedstateproposal=Events.(emitupdating_latest_proposalproposal.block.hash)>>=fun()->letprev_proposal=state.level_state.latest_proposalinletis_latest_proposal_applied=(* mark as applied if it is indeed applied or if this specific proposal was
already marked as applied *)is_proposal_applied||prev_proposal.block.hash=proposal.block.hash&&state.level_state.is_latest_proposal_appliedinletnew_level_state={state.level_statewithis_latest_proposal_applied;latest_proposal=proposal;}inLwt.return{statewithlevel_state=new_level_state}letmay_update_proposal~is_proposal_appliedstate(proposal:proposal)=assert(Compare.Int32.(state.level_state.latest_proposal.block.shell.level=proposal.block.shell.level));ifRound.(state.level_state.latest_proposal.block.round<proposal.block.round)thenupdate_proposal~is_proposal_appliedstateproposalelseLwt.returnstateletpreendorsestateproposal=ifBaking_state.is_first_block_in_protocolproposalthen(* We do not preendorse the first transition block *)letnew_state=update_current_phasestateIdleinLwt.return(new_state,Do_nothing)elseEvents.(emitattempting_preendorse_proposalproposal.block.hash)>>=fun()->letnew_state=(* We await for the block to be applied before updating its
locked values. *)ifstate.level_state.is_latest_proposal_appliedthenupdate_current_phasestateAwaiting_preendorsementselseupdate_current_phasestateAwaiting_applicationinLwt.return(new_state,make_preendorse_actionstateproposal)letextract_pqcstate(new_proposal:proposal)=matchnew_proposal.block.prequorumwith|None->None|Somepqc->letadd_voting_poweracc(op:Kind.preendorsementOperation.t)=letopenProtocol.Alpha_context.Operationinlet{shell=_;protocol_data={contents=Single(Preendorsement{slot;_});_};_;}=opinmatchSlotMap.findslotstate.level_state.delegate_slots.all_delegate_slotswith|None->(* cannot happen if the map is correctly populated *)acc|Some{endorsing_power;_}->acc+endorsing_powerinletvoting_power=List.fold_leftadd_voting_power0pqc.preendorsementsinletconsensus_threshold=state.global_state.constants.parametric.consensus_thresholdinifCompare.Int.(voting_power>=consensus_threshold)thenSome(pqc.preendorsements,pqc.round)elseNoneletmay_update_endorsable_payload_with_internal_pqcstate(new_proposal:proposal)=match(new_proposal.block.prequorum,state.level_state.endorsable_payload)with|None,_->(* The proposal does not contain a PQC: no need to update *)state|Some{round=new_round;_},Some{prequorum={round=old_round;_};_}whenRound.(new_round<old_round)->(* The proposal pqc is outdated, do not update *)state|Somebetter_prequorum,_->assert(Block_payload_hash.(better_prequorum.block_payload_hash=new_proposal.block.payload_hash));assert(Compare.Int32.(better_prequorum.level=new_proposal.block.shell.level));letnew_endorsable_payload=Some{proposal=new_proposal;prequorum=better_prequorum}inletnew_level_state={state.level_statewithendorsable_payload=new_endorsable_payload}in{statewithlevel_state=new_level_state}letmay_update_is_latest_proposal_applied~is_proposal_appliedstatenew_proposal=letcurrent_proposal=state.level_state.latest_proposalinifis_proposal_applied&&Block_hash.(current_proposal.block.hash=new_proposal.block.hash)thenletnew_level_state={state.level_statewithis_latest_proposal_applied=true}inletnew_state={statewithlevel_state=new_level_state}innew_stateelsestatelethas_already_been_handledstatenew_proposal=letcurrent_proposal=state.level_state.latest_proposalinBlock_hash.(current_proposal.block.hash=new_proposal.block.hash)&&state.level_state.is_latest_proposal_appliedletrechandle_proposal~is_proposal_appliedstate(new_proposal:proposal)=letcurrent_level=state.level_state.current_levelinletnew_proposal_level=new_proposal.block.shell.levelinletcurrent_proposal=state.level_state.latest_proposalinletstate=may_update_is_latest_proposal_applied~is_proposal_appliedstatenew_proposalinifCompare.Int32.(current_level>new_proposal_level)then(* The baker is ahead, a reorg may have happened. Do nothing:
wait for the node to send us the branch's head. This new head
should have a fitness that is greater than our current
proposal and thus, its level should be at least the same as
our current proposal's level. *)Events.(emitbaker_is_ahead_of_node(current_level,new_proposal_level))>>=fun()->do_nothingstateelseifCompare.Int32.(current_level=new_proposal_level)thenif(* The received head is a new proposal for the current level:
let's check if it's a valid one for us. *)Block_hash.(current_proposal.predecessor.hash<>new_proposal.predecessor.hash)thenEvents.(emitnew_proposal_is_on_another_branch(current_proposal.predecessor.hash,new_proposal.predecessor.hash))>>=fun()->may_switch_branch~is_proposal_appliedstatenew_proposalelseis_acceptable_proposal_for_current_levelstatenew_proposal>>=function|Invalid->(* The proposal is invalid: we ignore it *)Events.(emitskipping_invalid_proposal())>>=fun()->do_nothingstate|Outdated_proposal->(* Check whether we need to update our endorsable payload *)letstate=may_update_endorsable_payload_with_internal_pqcstatenew_proposalin(* The proposal is outdated: we update to be able to extract
its included endorsements but we do not endorse it *)Events.(emitoutdated_proposalnew_proposal.block.hash)>>=fun()->may_update_proposal~is_proposal_appliedstatenew_proposal>>=funstate->do_nothingstate|Valid_proposal->((* Valid_proposal => proposal.round = current_round *)(* Check whether we need to update our endorsable payload *)letnew_state=may_update_endorsable_payload_with_internal_pqcstatenew_proposalinmay_update_proposal~is_proposal_appliednew_statenew_proposal>>=funnew_state->(* The proposal is valid but maybe we already locked on a payload *)matchnew_state.level_state.locked_roundwith|Somelocked_round->(ifBlock_payload_hash.(locked_round.payload_hash=new_proposal.block.payload_hash)then(* when the new head has the same payload as our
[locked_round], we accept it and preendorse *)preendorsenew_statenew_proposalelse(* The payload is different *)matchnew_proposal.block.prequorumwith|Some{round;_}whenRound.(locked_round.round<round)->(* This PQC is above our locked_round, we can preendorse it *)preendorsenew_statenew_proposal|_->(* We shouldn't preendorse this proposal, but we
should at least watch (pre)quorums events on it
but only when it is applied otherwise we await
for the proposal to be applied. *)ifis_proposal_appliedthenletnew_state=update_current_phasenew_stateAwaiting_preendorsementsinLwt.return(new_state,Watch_proposal)elsedo_nothingnew_state)|None->(* Otherwise, we did not lock on any payload, thus we can
preendorse it *)preendorsenew_statenew_proposal)else(* Last case: new_proposal_level > current_level *)(* Possible scenarios:
- we received a block for a next level
- we received our own block
This is where we update our [level_state] (and our [round_state]) *)Events.(emitnew_head_with_increasing_level())>>=fun()->letnew_level=new_proposal.block.shell.levelinletcompute_new_state~current_round~delegate_slots~next_level_delegate_slots=letround_state={current_round;current_phase=Idle;delayed_prequorum=None}inletlevel_state={current_level=new_level;latest_proposal=new_proposal;is_latest_proposal_applied=is_proposal_applied;(* Unlock values *)locked_round=None;endorsable_payload=None;elected_block=None;delegate_slots;next_level_delegate_slots;next_level_proposed_round=None;}in(* recursive call with the up-to-date state to handle the new
level proposals *)handle_proposal~is_proposal_applied{statewithlevel_state;round_state}new_proposalinletaction=Update_to_level{new_level_proposal=new_proposal;compute_new_state}inLwt.return(state,action)andmay_switch_branch~is_proposal_appliedstatenew_proposal=letswitch_branchstate=Events.(emitswitching_branch())>>=fun()->(* If we are on a different branch, we also need to update our
[round_state] accordingly.
The recursive call to [handle_proposal] cannot end up
with an invalid proposal as it's on a different branch, thus
there is no need to backtrack to the former state as the new
proposal must end up being the new [latest_proposal]. That's
why we update it here. *)letround_update={Baking_actions.new_round_proposal=new_proposal;handle_proposal=(funstate->handle_proposal~is_proposal_appliedstatenew_proposal);}inupdate_proposal~is_proposal_appliedstatenew_proposal>>=funnew_state->(* TODO if the branch proposal is outdated, we should
trigger an [End_of_round] to participate *)Lwt.return(new_state,Synchronize_roundround_update)inletcurrent_endorsable_payload=state.level_state.endorsable_payloadinmatch(current_endorsable_payload,new_proposal.block.prequorum)with|None,Some_|None,None->Events.(emitbranch_proposal_has_better_fitness())>>=fun()->(* The new branch contains a PQC (and we do not) or a better
fitness, we switch. *)switch_branchstate|Some_,None->(* We have a better PQC, we don't switch as we are able to
propose a better chain if we stay on our current one. *)Events.(emitbranch_proposal_has_no_prequorum())>>=fun()->do_nothingstate|Some{prequorum=current_pqc;_},Somenew_pqc->ifRound.(current_pqc.round>new_pqc.round)thenEvents.(emitbranch_proposal_has_lower_prequorum())>>=fun()->(* The other's branch PQC is lower than ours, do not
switch *)do_nothingstateelseifRound.(current_pqc.round<new_pqc.round)thenEvents.(emitbranch_proposal_has_better_prequorum())>>=fun()->(* Their PQC is better than ours: we switch *)switch_branchstateelse(* current_pqc.round = new_pqc *)(* There is a PQC on two branches with the same round and
the same level but not the same predecessor : it's
impossible unless if there was some double-baking. This
shouldn't happen but do nothing anyway. *)Events.(emitbranch_proposal_has_same_prequorum())>>=fun()->do_nothingstateletmay_register_early_prequorumstate((candidate,_)asreceived_prequorum)=ifBlock_hash.(candidate.Operation_worker.hash<>state.level_state.latest_proposal.block.hash)thenEvents.(emitunexpected_pqc_while_waiting_for_application(candidate.hash,state.level_state.latest_proposal.block.hash))>>=fun()->do_nothingstateelseEvents.(emitpqc_while_waiting_for_applicationcandidate.hash)>>=fun()->letnew_round_state={state.round_statewithdelayed_prequorum=Somereceived_prequorum}inletnew_state={statewithround_state=new_round_state}indo_nothingnew_state(** In the association map [delegate_slots], the function returns an
optional pair ([delegate], [endorsing_slot]) if for the current
[round], the validator [delegate] has a endorsing slot. *)letround_proposerstatedelegate_slotsround=(* TODO: make sure that for each slots all rounds in the map are filled *)(* !FIXME! Endorsers and proposer are differents sets *)(* !FIXME! the slotmap may be inconsistent & may sure to document
the invariants *)letround_mod=Int32.to_int(Round.to_int32round)modstate.global_state.constants.parametric.consensus_committee_sizeinSlotMap.findstate.level_state.delegate_slots.all_slots_by_round.(round_mod)delegate_slots(** Inject a fresh block proposal containing the current operations of the
mempool in [state] and the additional [endorsements] and [dal_attestations]
for [delegate] at round [round]. *)letpropose_fresh_block_action~endorsements~dal_attestations?last_proposal~(predecessor:block_info)statedelegateround=(* TODO check if there is a trace where we could not have updated the level *)(* The block to bake embeds the operations gathered by the
worker. However, consensus operations that are not relevant for
this block are filtered out. In the case of proposing a new fresh
block, the block is supposed to carry only endorsements for the
previous level. *)letoperation_pool=(* 1. Fetch operations from the mempool. *)letcurrent_mempool=letpool=Operation_worker.get_current_operationsstate.global_state.operation_workerin(* Considered the operations in the previous proposal as well *)matchlast_proposalwith|Someproposal->let{Operation_pool.votes_payload;anonymous_payload;managers_payload;}=proposal.payloadinList.fold_leftOperation_pool.add_operationspool[votes_payload;anonymous_payload;managers_payload]|None->poolin(* 2. Filter and only retain relevant endorsements. *)letrelevant_consensus_operations=letendorsement_filter={Operation_pool.level=predecessor.shell.level;round=predecessor.round;payload_hash=predecessor.payload_hash;}inOperation_pool.filter_with_relevant_consensus_ops~endorsement_filter~preendorsement_filter:Nonecurrent_mempool.consensusinletfiltered_mempool={current_mempoolwithconsensus=relevant_consensus_operations}in(* 3. Add the additional given [endorsements] and [dal_attestations].
N.b. this is a set: there won't be duplicates *)letpool=Operation_pool.add_operationsfiltered_mempool(List.mapOperation.packendorsements)inOperation_pool.add_operationspool(List.mapOperation.packdal_attestations)inletkind=Freshoperation_poolinEvents.(emitproposing_fresh_block(delegate,round))>>=fun()->letforce_apply=state.global_state.config.force_apply||Round.(round<>zero)(* This is used as a safety net by applying blocks on round > 0, in case
validation-only did not produce a correct round-0 block. *)inletblock_to_bake={predecessor;round;delegate;kind;force_apply}inletupdated_state=update_current_phasestateIdleinLwt.return@@Inject_block{block_to_bake;updated_state}letpropose_block_actionstatedelegateround(proposal:proposal)=(* Possible cases:
1. There was a proposal but the PQC was not reached.
2. There was a proposal and the PQC was reached. We repropose the
[endorsable_payload] if it exists, not the [locked_round] as it
may be older. *)matchstate.level_state.endorsable_payloadwith|None->Events.(emitno_endorsable_payload_fresh_block())>>=fun()->(* For case 1, we may re-inject with the same payload or a fresh
one. We make the choice of baking a fresh one: the previous
proposal may have been rejected because the block may have been
valid but may be considered "bad" (censored operations, empty
block, etc.) by the other validators. *)(* Invariant: there is no locked round if there is no endorsable
payload *)assert(state.level_state.locked_round=None);letendorsements_in_last_proposal=proposal.block.quorumin(* Also insert the DAL attestations from the proposal, because the mempool
may not contain them anymore *)(* TODO: https://gitlab.com/tezos/tezos/-/issues/4671
The block may therefore contain multiple attestations for the same delegate. *)letdal_attestations_in_last_proposal=proposal.block.dal_attestationsinpropose_fresh_block_action~endorsements:endorsements_in_last_proposal~dal_attestations:dal_attestations_in_last_proposalstate~last_proposal:proposal.block~predecessor:proposal.predecessordelegateround|Some{proposal;prequorum}->Events.(emitrepropose_blockproposal.block.payload_hash)>>=fun()->(* For case 2, we re-inject the same block as [endorsable_round]
but we may add some left-overs endorsements. Therefore, the
operations we need to include are:
- the proposal's included endorsements
- the potential missing new endorsements for the
previous block
- the PQC of the endorsable payload *)letconsensus_operations=(* Fetch preendorsements and endorsements from the mempool
(that could be missing from the proposal), filter, then add
consensus operations of the proposal itself, and convert
into [packed_operation trace]. *)letmempool_consensus_operations=(Operation_worker.get_current_operationsstate.global_state.operation_worker).consensusinletall_consensus_operations=(* Add the proposal and pqc consensus operations to the
mempool *)List.fold_left(funsetop->Operation_pool.Operation_set.addopset)mempool_consensus_operations(List.mapOperation.packproposal.block.quorum@List.mapOperation.packprequorum.preendorsements)inletendorsement_filter={Operation_pool.level=proposal.predecessor.shell.level;round=proposal.predecessor.round;payload_hash=proposal.predecessor.payload_hash;}inletpreendorsement_filter=Some{Operation_pool.level=prequorum.level;round=prequorum.round;payload_hash=prequorum.block_payload_hash;}inOperation_pool.(filter_with_relevant_consensus_ops~endorsement_filter~preendorsement_filterall_consensus_operations|>Operation_set.elements)inletpayload_hash=proposal.block.payload_hashinletpayload_round=proposal.block.payload_roundinletpayload=proposal.block.payloadinletkind=Reproposal{consensus_operations;payload_hash;payload_round;payload}inletforce_apply=true(* This is used as a safety net by applying blocks on round > 0, in case
validation-only did not produce a correct round-0 block. *)inletblock_to_bake={predecessor=proposal.predecessor;round;delegate;kind;force_apply}inletupdated_state=update_current_phasestateIdleinLwt.return@@Inject_block{block_to_bake;updated_state}letend_of_roundstatecurrent_round=letnew_round=Round.succcurrent_roundinletnew_round_state={state.round_statewithcurrent_round=new_round}inletnew_state={statewithround_state=new_round_state}in(* we need to check if we need to bake for this round or not *)matchround_proposernew_statenew_state.level_state.delegate_slots.own_delegate_slotsnew_state.round_state.current_roundwith|None->Events.(emitno_proposal_slot(current_round,state.level_state.current_level,new_round))>>=fun()->(* We don't have any delegate that may propose a new block for
this round -- We will wait for preendorsements when the next
level block arrive. Meanwhile, we are idle *)letnew_state=update_current_phasenew_stateIdleindo_nothingnew_state|Some(delegate,_)->letlatest_proposal=state.level_state.latest_proposalinifBaking_state.is_first_block_in_protocollatest_proposalthen(* Do not inject a block for the previous protocol! (Let the
baker of the previous protocol do it.) *)do_nothingnew_stateelseEvents.(emitproposal_slot(current_round,state.level_state.current_level,new_round,delegate))>>=fun()->(* We have a delegate, we need to determine what to inject *)propose_block_actionnew_statedelegatenew_roundstate.level_state.latest_proposal>>=funaction->Lwt.return(new_state,action)lettime_to_bake_at_next_levelstateat_round=(* It is now time to update the state level *)(* We need to keep track for which block we have 2f+1 *endorsements*, that is,
which will become the new predecessor_block *)(* Invariant: endorsable_round >= round(elected block) >= locked_round *)letround_proposer_opt=round_proposerstatestate.level_state.next_level_delegate_slots.own_delegate_slotsat_roundinmatch(state.level_state.elected_block,round_proposer_opt)with|None,_|_,None->(* Unreachable: the [Time_to_bake_next_level] event can only be
triggered when we have a slot and an elected block *)assertfalse|Someelected_block,Some(delegate,_)->letendorsements=elected_block.endorsement_qcinletdal_attestations=(* Unlike endorsements, we don't watch and store DAL attestations for
each proposal, we'll retrieve them from the mempool *)[]inletnew_level_state={state.level_statewithnext_level_proposed_round=Someat_round}inletnew_state={statewithlevel_state=new_level_state}inpropose_fresh_block_action~endorsements~dal_attestations~predecessor:elected_block.proposal.blocknew_statedelegateat_round>>=funaction->Lwt.return(new_state,action)letupdate_locked_roundstateroundpayload_hash=letlocked_round=Some{payload_hash;round}inletnew_level_state={state.level_statewithlocked_round}in{statewithlevel_state=new_level_state}letmake_endorse_actionstateproposal=letendorsements:(consensus_key_and_delegate*consensus_content)list=make_consensus_liststateproposalinInject_endorsements{endorsements}letprequorum_reached_when_awaiting_preendorsementsstatecandidatepreendorsements=letlatest_proposal=state.level_state.latest_proposalinifBlock_hash.(candidate.Operation_worker.hash<>latest_proposal.block.hash)thenEvents.(emitunexpected_prequorum_received(candidate.hash,latest_proposal.block.hash))>>=fun()->do_nothingstateelseifnotstate.level_state.is_latest_proposal_appliedthenEvents.(emithandling_prequorum_on_non_applied_proposal())>>=fun()->do_nothingstateelseletprequorum={level=latest_proposal.block.shell.level;round=latest_proposal.block.round;block_payload_hash=latest_proposal.block.payload_hash;preendorsements(* preendorsements may be nil when [consensus_threshold] is 0 *);}inletnew_endorsable_payload={proposal=latest_proposal;prequorum}inletnew_level_state=letlevel_state_with_new_payload={state.level_statewithendorsable_payload=Somenew_endorsable_payload;}inmatchstate.level_state.endorsable_payloadwith|None->level_state_with_new_payload|Someendorsable_payload->ifRound.(endorsable_payload.prequorum.round<new_endorsable_payload.prequorum.round)thenlevel_state_with_new_payloadelsestate.level_stateinletnew_state={statewithlevel_state=new_level_state}inletnew_state=update_locked_roundnew_statelatest_proposal.block.roundlatest_proposal.block.payload_hashinletnew_state=update_current_phasenew_stateAwaiting_endorsementsinLwt.return(new_state,make_endorse_actionnew_statelatest_proposal)letquorum_reached_when_waiting_endorsementsstatecandidateendorsement_qc=letlatest_proposal=state.level_state.latest_proposalinifBlock_hash.(candidate.Operation_worker.hash<>latest_proposal.block.hash)thenEvents.(emitunexpected_quorum_received(candidate.hash,latest_proposal.block.hash))>>=fun()->do_nothingstateelseletnew_level_state=matchstate.level_state.elected_blockwith|None->letelected_block=Some{proposal=latest_proposal;endorsement_qc}in{state.level_statewithelected_block}|Some_->(* If we already have an elected block, do not update it: the
earliest, the better. *)state.level_stateinletnew_round_state={state.round_statewithcurrent_phase=Idle}inletnew_state={statewithround_state=new_round_state;level_state=new_level_state}indo_nothingnew_statelethandle_expected_applied_proposal(state:Baking_state.t)=letnew_level_state={state.level_statewithis_latest_proposal_applied=true}inletnew_state={statewithlevel_state=new_level_state}inmatchnew_state.round_state.delayed_prequorumwith|None->(* The application arrived before the prequorum: just wait for the prequorum. *)letnew_state=update_current_phasenew_stateAwaiting_preendorsementsindo_nothingnew_state|Some(candidate,preendorsement_qc)->(* The application arrived after the prequorum: handle the
prequorum received earlier.
Start by resetting the delayed_prequorum *)letnew_round_state={new_state.round_statewithdelayed_prequorum=None}inletnew_state={statewithlevel_state=new_level_state;round_state=new_round_state;}inprequorum_reached_when_awaiting_preendorsementsnew_statecandidatepreendorsement_qc(* Hypothesis:
- The state is not to be modified outside this module
(NB: there are exceptions in Baking_actions: the corner cases
[update_to_level] and [synchronize_round] and
the hack used by [inject_block])
- new_proposal's received blocks are expected to belong to our current
round
- [Prequorum_reached] can only be received when we've seen a new head
- [Quorum_reached] can only be received when we've seen a
[Prequorum_reached] *)letstep(state:Baking_state.t)(event:Baking_state.event):(Baking_state.t*Baking_actions.t)Lwt.t=letphase=state.round_state.current_phaseinEvents.(emitstep_current_phase(phase,event))>>=fun()->match(phase,event)with(* Handle timeouts *)|_,Timeout(End_of_round{ending_round})->(* If the round is ending, stop everything currently going on and
increment the round. *)end_of_roundstateending_round|_,Timeout(Time_to_bake_next_level{at_round})->(* If it is time to bake the next level, stop everything currently
going on and propose the next level block *)time_to_bake_at_next_levelstateat_round|Idle,New_head_proposalproposal->Events.(emitnew_head(proposal.block.hash,proposal.block.shell.level,proposal.block.round))>>=fun()->handle_proposal~is_proposal_applied:truestateproposal|Awaiting_application,New_head_proposalproposal->ifBlock_hash.(state.level_state.latest_proposal.block.hash<>proposal.block.hash)thenEvents.(emitnew_head(proposal.block.hash,proposal.block.shell.level,proposal.block.round))>>=fun()->Events.(emitunexpected_new_head_while_waiting_for_application())>>=fun()->handle_proposal~is_proposal_applied:truestateproposalelseEvents.(emitapplied_expected_proposal_receivedproposal.block.hash)>>=fun()->handle_expected_applied_proposalstate|Awaiting_endorsements,New_head_proposalproposal|Awaiting_preendorsements,New_head_proposalproposal->Events.(emitnew_head(proposal.block.hash,proposal.block.shell.level,proposal.block.round))>>=fun()->Events.(emitnew_head_while_waiting_for_qc())>>=fun()->handle_proposal~is_proposal_applied:truestateproposal|Idle,New_valid_proposalproposal->Events.(emitnew_valid_proposal(proposal.block.hash,proposal.block.shell.level,proposal.block.round))>>=fun()->handle_proposal~is_proposal_applied:falsestateproposal|Awaiting_application,New_valid_proposalproposal|Awaiting_endorsements,New_valid_proposalproposal|Awaiting_preendorsements,New_valid_proposalproposal->Events.(emitnew_valid_proposal(proposal.block.hash,proposal.block.shell.level,proposal.block.round))>>=fun()->ifhas_already_been_handledstateproposalthenEvents.(emitvalid_proposal_received_after_application())>>=fun()->do_nothingstateelseEvents.(emitnew_valid_proposal_while_waiting_for_qc())>>=fun()->handle_proposal~is_proposal_applied:falsestateproposal|Awaiting_application,Prequorum_reached(candidate,preendorsement_qc)->may_register_early_prequorumstate(candidate,preendorsement_qc)|Awaiting_preendorsements,Prequorum_reached(candidate,preendorsement_qc)->prequorum_reached_when_awaiting_preendorsementsstatecandidatepreendorsement_qc|Awaiting_endorsements,Quorum_reached(candidate,endorsement_qc)->quorum_reached_when_waiting_endorsementsstatecandidateendorsement_qc(* Unreachable cases *)|Idle,(Prequorum_reached_|Quorum_reached_)|Awaiting_preendorsements,Quorum_reached_|Awaiting_endorsements,Prequorum_reached_|Awaiting_application,Quorum_reached_->(* This cannot/should not happen *)do_nothingstate