diff --git a/CHANGES.rst b/CHANGES.rst index 7792e0202fd4c88ebebb76eaf056a85b0c4083d3..37078cbc7e92921e4df519a86e06ebe837e0d572 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -107,6 +107,9 @@ Baker parent block. This is done to avoid having consensus operation branched on block that are not part of the canonical chain anymore.(MR :gl:`!13619`) +- Enforce some Tenderbake invariant's at consensus operations injection. (MR + :gl:`!`) + Accuser ------- diff --git a/src/proto_020_PsParisC/lib_delegate/baking_events.ml b/src/proto_020_PsParisC/lib_delegate/baking_events.ml index cc630dbc9769618316b04713701211c2be3bf063..a224fd1e6c8de480145d513fd5eeb827af5c7d2e 100644 --- a/src/proto_020_PsParisC/lib_delegate/baking_events.ml +++ b/src/proto_020_PsParisC/lib_delegate/baking_events.ml @@ -367,6 +367,21 @@ module State_transitions = struct ~pp2:Baking_state.pp_event ("event", Baking_state.event_encoding) + let discarding_preattestation = + declare_3 + ~section + ~name:"discarding_preattestation" + ~level:Info + ~msg: + "discarding outdated preattestation for {delegate} at level {level}, \ + round {round}" + ~pp1:Baking_state.pp_consensus_key_and_delegate + ("delegate", Baking_state.consensus_key_and_delegate_encoding) + ~pp2:pp_int32 + ("level", Data_encoding.int32) + ~pp3:Round.pp + ("round", Round.encoding) + let discarding_attestation = declare_3 ~section @@ -381,6 +396,87 @@ module State_transitions = struct ("level", Data_encoding.int32) ~pp3:Round.pp ("round", Round.encoding) + + let discarding_unexpected_preattestation_with_different_payload = + declare_5 + ~section + ~name:"discarding_unexpected_preattestation_with_different_payload" + ~level:Warning + ~msg: + "discarding preattestation for {delegate} with payload {payload} at \ + level {level}, round {round} where the prequorum was locked on a \ + different payload {state_payload}." + ~pp1:Baking_state.pp_consensus_key_and_delegate + ("delegate", Baking_state.consensus_key_and_delegate_encoding) + ~pp2:Block_payload_hash.pp + ("payload", Block_payload_hash.encoding) + ~pp3:pp_int32 + ("level", Data_encoding.int32) + ~pp4:Round.pp + ("round", Round.encoding) + ~pp5:Block_payload_hash.pp + ("state_payload", Block_payload_hash.encoding) + + let discarding_unexpected_attestation_without_prequorum_payload = + declare_3 + ~section + ~name:"discarding_unexpected_attestation_without_prequorum" + ~level:Warning + ~msg: + "discarding attestation for {delegate} at level {level}, round {round} \ + where no prequorum was reached." + ~pp1:Baking_state.pp_consensus_key_and_delegate + ("delegate", Baking_state.consensus_key_and_delegate_encoding) + ~pp2:pp_int32 + ("level", Data_encoding.int32) + ~pp3:Round.pp + ("round", Round.encoding) + + let discarding_unexpected_attestation_with_different_prequorum_payload = + declare_5 + ~section + ~name:"discarding_unexpected_attestation_with_different_prequorum" + ~level:Warning + ~msg: + "discarding attestation for {delegate} with payload {payload} at level \ + {level}, round {round} where the prequorum was on a different payload \ + {state_payload}." + ~pp1:Baking_state.pp_consensus_key_and_delegate + ("delegate", Baking_state.consensus_key_and_delegate_encoding) + ~pp2:Block_payload_hash.pp + ("payload", Block_payload_hash.encoding) + ~pp3:pp_int32 + ("level", Data_encoding.int32) + ~pp4:Round.pp + ("round", Round.encoding) + ~pp5:Block_payload_hash.pp + ("state_payload", Block_payload_hash.encoding) + + let discarding_unexpected_prequorum_reached = + declare_2 + ~section + ~name:"discarding_unexpected_prequorum_reached" + ~level:Info + ~msg: + "discarding unexpected prequorum reached for {candidate} while in \ + {phase} phase." + ~pp1:Block_hash.pp + ("candidate", Block_hash.encoding) + ~pp2:Baking_state.pp_phase + ("phase", Baking_state.phase_encoding) + + let discarding_unexpected_quorum_reached = + declare_2 + ~section + ~name:"discarding_unexpected_quorum_reached" + ~level:Info + ~msg: + "discarding unexpected quorum reached for {candidate} while in {phase} \ + phase." + ~pp1:Block_hash.pp + ("candidate", Block_hash.encoding) + ~pp2:Baking_state.pp_phase + ("phase", Baking_state.phase_encoding) end module Node_rpc = struct diff --git a/src/proto_020_PsParisC/lib_delegate/state_transitions.ml b/src/proto_020_PsParisC/lib_delegate/state_transitions.ml index 7610ea010eb75722f605e2ee2f4ff774deb70d72..9cc23c9604ded5cfcf15cc78a62cf2b4cd3f7a15 100644 --- a/src/proto_020_PsParisC/lib_delegate/state_transitions.ml +++ b/src/proto_020_PsParisC/lib_delegate/state_transitions.ml @@ -741,13 +741,71 @@ let prepare_attest_action state proposal = in Prepare_attestations {attestations} -let inject_early_arrived_attestations state = - let early_attestations = state.round_state.early_attestations in - match early_attestations with - | [] -> Lwt.return (state, Watch_quorum) - | first_signed_attestation :: _ as unbatched_signed_attestations -> ( - let new_round_state = {state.round_state with early_attestations = []} in - let new_state = {state with round_state = new_round_state} in +(* This function is called once a prequorum has been reached. *) +let may_inject_attestations state ~first_signed_attestation + ~other_signed_attestations = + let open Lwt_syntax in + let emit_discarding_unexpected_attestation_event + ?(payload : attestable_payload option) attestation = + let { + vote_consensus_content = {level; round = att_round; block_payload_hash; _}; + delegate; + _; + } = + attestation.unsigned_consensus_vote + in + let att_level = Raw_level.to_int32 level in + match payload with + | None -> + Events.( + emit + discarding_unexpected_attestation_without_prequorum_payload + (delegate, att_level, att_round)) + | Some payload -> + Events.( + emit + discarding_unexpected_attestation_with_different_prequorum_payload + ( delegate, + block_payload_hash, + att_level, + att_round, + payload.proposal.block.payload_hash )) + in + let check_payload state attestation do_action = + match state.level_state.attestable_payload with + | None -> + (* No attestable payload, either the prequorum has not been reached yet, + or an other issue occurred, we cannot inject the attestations. *) + let* () = emit_discarding_unexpected_attestation_event attestation in + do_nothing state + | Some payload -> + if + not + Block_payload_hash.( + payload.proposal.block.payload_hash + = attestation.unsigned_consensus_vote.vote_consensus_content + .block_payload_hash) + then + (* Attestable payload found in the state but it is different from the + one in the attestation operation, we cannot inject the + attestation. *) + let* () = + emit_discarding_unexpected_attestation_event ~payload attestation + in + do_nothing state + else do_action + in + match other_signed_attestations with + | [] -> + check_payload state first_signed_attestation + @@ + let signed_attestations = + make_singleton_consensus_vote_batch first_signed_attestation + in + Lwt.return (state, Inject_attestations {signed_attestations}) + | _ :: _ -> ( + check_payload state first_signed_attestation + @@ let batch_branch = get_branch_from_proposal state.level_state.latest_proposal in @@ -766,11 +824,25 @@ let inject_early_arrived_attestations state = Attestation batch_content ~batch_branch - unbatched_signed_attestations + (first_signed_attestation :: other_signed_attestations) |> function | Ok signed_attestations -> - Lwt.return (new_state, Inject_attestations {signed_attestations}) - | Error _err -> (* Unreachable *) do_nothing new_state) + Lwt.return (state, Inject_attestations {signed_attestations}) + | Error _err -> (* Unreachable *) do_nothing state) + +(* This function tries to inject attestations already prepared if the + prequorum is reached. *) +let may_inject_early_forged_attestations state = + let early_attestations = state.round_state.early_attestations in + match early_attestations with + | [] -> Lwt.return (state, Watch_quorum) + | first_signed_attestation :: other_signed_attestations -> + let new_round_state = {state.round_state with early_attestations = []} in + let new_state = {state with round_state = new_round_state} in + may_inject_attestations + new_state + ~first_signed_attestation + ~other_signed_attestations let prequorum_reached_when_awaiting_preattestations state candidate preattestations = @@ -828,7 +900,7 @@ let prequorum_reached_when_awaiting_preattestations state candidate else (* We already triggered preemptive attestation forging, we either have those already or we are waiting for them. *) - inject_early_arrived_attestations new_state + may_inject_early_forged_attestations new_state let quorum_reached_when_waiting_attestations state candidate attestation_qc = let open Lwt_syntax in @@ -916,7 +988,55 @@ let handle_expected_applied_proposal (state : Baking_state.t) = let new_state = update_current_phase new_state Idle in do_nothing new_state -let handle_arriving_attestation state signed_attestation = +let handle_forged_preattestation state signed_preattestation = + let open Lwt_syntax in + let { + vote_consensus_content = + { + level; + round = att_round; + block_payload_hash = att_payload_hash; + slot = _; + }; + delegate; + _; + } = + signed_preattestation.unsigned_consensus_vote + in + let att_level = Raw_level.to_int32 level in + let check_payload state preattestation do_action = + match state.level_state.attestable_payload with + | None -> + (* No attestable payload set, we are free to inject the + preattestations *) + do_action + | Some payload -> + if + not + Block_payload_hash.( + payload.proposal.block.payload_hash + = preattestation.unsigned_consensus_vote.vote_consensus_content + .block_payload_hash) + then + (* The preattestation payload does not match the one set in the + state, we cannot inject the preattestation. *) + let* () = + Events.( + emit + discarding_unexpected_preattestation_with_different_payload + ( delegate, + att_payload_hash, + att_level, + att_round, + payload.proposal.block.payload_hash )) + in + do_nothing state + else do_action + in + check_payload state signed_preattestation + @@ Lwt.return (state, Inject_preattestation {signed_preattestation}) + +let handle_forged_attestation state signed_attestation = let open Lwt_syntax in let { vote_consensus_content = @@ -960,12 +1080,12 @@ let handle_arriving_attestation state signed_attestation = let new_state = {state with round_state = new_round_state} in do_nothing new_state | Idle | Awaiting_attestations | Awaiting_application -> - (* For these three phases, we have necessarily already reached the - prequorum: we are safe to inject. *) - let signed_attestations = - make_singleton_consensus_vote_batch signed_attestation - in - Lwt.return (state, Inject_attestations {signed_attestations}) + (* For these three phases, we should have already reached the prequorum. + If this is not the case, the attestations will not be injected. *) + may_inject_attestations + state + ~first_signed_attestation:signed_attestation + ~other_signed_attestations:[] let handle_forge_event state forge_event = match forge_event with @@ -975,9 +1095,9 @@ let handle_forge_event state forge_event = Inject_block {prepared_block; force_injection = false; asynchronous = true} ) | Preattestation_ready signed_preattestation -> - Lwt.return (state, Inject_preattestation {signed_preattestation}) + handle_forged_preattestation state signed_preattestation | Attestation_ready signed_attestation -> - handle_arriving_attestation state signed_attestation + handle_forged_attestation state signed_attestation (* Hypothesis: - The state is not to be modified outside this module @@ -1089,10 +1209,20 @@ let step (state : Baking_state.t) (event : Baking_state.event) : preattestation_qc | Awaiting_attestations, Quorum_reached (candidate, attestation_qc) -> quorum_reached_when_waiting_attestations state candidate attestation_qc - (* Unreachable cases *) - | Idle, (Prequorum_reached _ | Quorum_reached _) - | Awaiting_preattestations, Quorum_reached _ - | (Awaiting_application | Awaiting_attestations), Prequorum_reached _ - | Awaiting_application, Quorum_reached _ -> - (* This cannot/should not happen *) + (* Unreachable cases modulo concurrency. *) + | ( (Idle | Awaiting_application | Awaiting_attestations), + Prequorum_reached (candidate, _operations_pqc) ) -> + (* Unexpected prequorum reached, we do not lock on it and discard it. *) + let* () = + Events.( + emit discarding_unexpected_prequorum_reached (candidate.hash, phase)) + in + do_nothing state + | ( (Idle | Awaiting_preattestations | Awaiting_application), + Quorum_reached (candidate, _operations_qc) ) -> + (* Unexpected quorum reached, we discard it. *) + let* () = + Events.( + emit discarding_unexpected_quorum_reached (candidate.hash, phase)) + in do_nothing state diff --git a/src/proto_alpha/lib_delegate/baking_events.ml b/src/proto_alpha/lib_delegate/baking_events.ml index cc630dbc9769618316b04713701211c2be3bf063..a224fd1e6c8de480145d513fd5eeb827af5c7d2e 100644 --- a/src/proto_alpha/lib_delegate/baking_events.ml +++ b/src/proto_alpha/lib_delegate/baking_events.ml @@ -367,6 +367,21 @@ module State_transitions = struct ~pp2:Baking_state.pp_event ("event", Baking_state.event_encoding) + let discarding_preattestation = + declare_3 + ~section + ~name:"discarding_preattestation" + ~level:Info + ~msg: + "discarding outdated preattestation for {delegate} at level {level}, \ + round {round}" + ~pp1:Baking_state.pp_consensus_key_and_delegate + ("delegate", Baking_state.consensus_key_and_delegate_encoding) + ~pp2:pp_int32 + ("level", Data_encoding.int32) + ~pp3:Round.pp + ("round", Round.encoding) + let discarding_attestation = declare_3 ~section @@ -381,6 +396,87 @@ module State_transitions = struct ("level", Data_encoding.int32) ~pp3:Round.pp ("round", Round.encoding) + + let discarding_unexpected_preattestation_with_different_payload = + declare_5 + ~section + ~name:"discarding_unexpected_preattestation_with_different_payload" + ~level:Warning + ~msg: + "discarding preattestation for {delegate} with payload {payload} at \ + level {level}, round {round} where the prequorum was locked on a \ + different payload {state_payload}." + ~pp1:Baking_state.pp_consensus_key_and_delegate + ("delegate", Baking_state.consensus_key_and_delegate_encoding) + ~pp2:Block_payload_hash.pp + ("payload", Block_payload_hash.encoding) + ~pp3:pp_int32 + ("level", Data_encoding.int32) + ~pp4:Round.pp + ("round", Round.encoding) + ~pp5:Block_payload_hash.pp + ("state_payload", Block_payload_hash.encoding) + + let discarding_unexpected_attestation_without_prequorum_payload = + declare_3 + ~section + ~name:"discarding_unexpected_attestation_without_prequorum" + ~level:Warning + ~msg: + "discarding attestation for {delegate} at level {level}, round {round} \ + where no prequorum was reached." + ~pp1:Baking_state.pp_consensus_key_and_delegate + ("delegate", Baking_state.consensus_key_and_delegate_encoding) + ~pp2:pp_int32 + ("level", Data_encoding.int32) + ~pp3:Round.pp + ("round", Round.encoding) + + let discarding_unexpected_attestation_with_different_prequorum_payload = + declare_5 + ~section + ~name:"discarding_unexpected_attestation_with_different_prequorum" + ~level:Warning + ~msg: + "discarding attestation for {delegate} with payload {payload} at level \ + {level}, round {round} where the prequorum was on a different payload \ + {state_payload}." + ~pp1:Baking_state.pp_consensus_key_and_delegate + ("delegate", Baking_state.consensus_key_and_delegate_encoding) + ~pp2:Block_payload_hash.pp + ("payload", Block_payload_hash.encoding) + ~pp3:pp_int32 + ("level", Data_encoding.int32) + ~pp4:Round.pp + ("round", Round.encoding) + ~pp5:Block_payload_hash.pp + ("state_payload", Block_payload_hash.encoding) + + let discarding_unexpected_prequorum_reached = + declare_2 + ~section + ~name:"discarding_unexpected_prequorum_reached" + ~level:Info + ~msg: + "discarding unexpected prequorum reached for {candidate} while in \ + {phase} phase." + ~pp1:Block_hash.pp + ("candidate", Block_hash.encoding) + ~pp2:Baking_state.pp_phase + ("phase", Baking_state.phase_encoding) + + let discarding_unexpected_quorum_reached = + declare_2 + ~section + ~name:"discarding_unexpected_quorum_reached" + ~level:Info + ~msg: + "discarding unexpected quorum reached for {candidate} while in {phase} \ + phase." + ~pp1:Block_hash.pp + ("candidate", Block_hash.encoding) + ~pp2:Baking_state.pp_phase + ("phase", Baking_state.phase_encoding) end module Node_rpc = struct diff --git a/src/proto_alpha/lib_delegate/state_transitions.ml b/src/proto_alpha/lib_delegate/state_transitions.ml index 7610ea010eb75722f605e2ee2f4ff774deb70d72..9cc23c9604ded5cfcf15cc78a62cf2b4cd3f7a15 100644 --- a/src/proto_alpha/lib_delegate/state_transitions.ml +++ b/src/proto_alpha/lib_delegate/state_transitions.ml @@ -741,13 +741,71 @@ let prepare_attest_action state proposal = in Prepare_attestations {attestations} -let inject_early_arrived_attestations state = - let early_attestations = state.round_state.early_attestations in - match early_attestations with - | [] -> Lwt.return (state, Watch_quorum) - | first_signed_attestation :: _ as unbatched_signed_attestations -> ( - let new_round_state = {state.round_state with early_attestations = []} in - let new_state = {state with round_state = new_round_state} in +(* This function is called once a prequorum has been reached. *) +let may_inject_attestations state ~first_signed_attestation + ~other_signed_attestations = + let open Lwt_syntax in + let emit_discarding_unexpected_attestation_event + ?(payload : attestable_payload option) attestation = + let { + vote_consensus_content = {level; round = att_round; block_payload_hash; _}; + delegate; + _; + } = + attestation.unsigned_consensus_vote + in + let att_level = Raw_level.to_int32 level in + match payload with + | None -> + Events.( + emit + discarding_unexpected_attestation_without_prequorum_payload + (delegate, att_level, att_round)) + | Some payload -> + Events.( + emit + discarding_unexpected_attestation_with_different_prequorum_payload + ( delegate, + block_payload_hash, + att_level, + att_round, + payload.proposal.block.payload_hash )) + in + let check_payload state attestation do_action = + match state.level_state.attestable_payload with + | None -> + (* No attestable payload, either the prequorum has not been reached yet, + or an other issue occurred, we cannot inject the attestations. *) + let* () = emit_discarding_unexpected_attestation_event attestation in + do_nothing state + | Some payload -> + if + not + Block_payload_hash.( + payload.proposal.block.payload_hash + = attestation.unsigned_consensus_vote.vote_consensus_content + .block_payload_hash) + then + (* Attestable payload found in the state but it is different from the + one in the attestation operation, we cannot inject the + attestation. *) + let* () = + emit_discarding_unexpected_attestation_event ~payload attestation + in + do_nothing state + else do_action + in + match other_signed_attestations with + | [] -> + check_payload state first_signed_attestation + @@ + let signed_attestations = + make_singleton_consensus_vote_batch first_signed_attestation + in + Lwt.return (state, Inject_attestations {signed_attestations}) + | _ :: _ -> ( + check_payload state first_signed_attestation + @@ let batch_branch = get_branch_from_proposal state.level_state.latest_proposal in @@ -766,11 +824,25 @@ let inject_early_arrived_attestations state = Attestation batch_content ~batch_branch - unbatched_signed_attestations + (first_signed_attestation :: other_signed_attestations) |> function | Ok signed_attestations -> - Lwt.return (new_state, Inject_attestations {signed_attestations}) - | Error _err -> (* Unreachable *) do_nothing new_state) + Lwt.return (state, Inject_attestations {signed_attestations}) + | Error _err -> (* Unreachable *) do_nothing state) + +(* This function tries to inject attestations already prepared if the + prequorum is reached. *) +let may_inject_early_forged_attestations state = + let early_attestations = state.round_state.early_attestations in + match early_attestations with + | [] -> Lwt.return (state, Watch_quorum) + | first_signed_attestation :: other_signed_attestations -> + let new_round_state = {state.round_state with early_attestations = []} in + let new_state = {state with round_state = new_round_state} in + may_inject_attestations + new_state + ~first_signed_attestation + ~other_signed_attestations let prequorum_reached_when_awaiting_preattestations state candidate preattestations = @@ -828,7 +900,7 @@ let prequorum_reached_when_awaiting_preattestations state candidate else (* We already triggered preemptive attestation forging, we either have those already or we are waiting for them. *) - inject_early_arrived_attestations new_state + may_inject_early_forged_attestations new_state let quorum_reached_when_waiting_attestations state candidate attestation_qc = let open Lwt_syntax in @@ -916,7 +988,55 @@ let handle_expected_applied_proposal (state : Baking_state.t) = let new_state = update_current_phase new_state Idle in do_nothing new_state -let handle_arriving_attestation state signed_attestation = +let handle_forged_preattestation state signed_preattestation = + let open Lwt_syntax in + let { + vote_consensus_content = + { + level; + round = att_round; + block_payload_hash = att_payload_hash; + slot = _; + }; + delegate; + _; + } = + signed_preattestation.unsigned_consensus_vote + in + let att_level = Raw_level.to_int32 level in + let check_payload state preattestation do_action = + match state.level_state.attestable_payload with + | None -> + (* No attestable payload set, we are free to inject the + preattestations *) + do_action + | Some payload -> + if + not + Block_payload_hash.( + payload.proposal.block.payload_hash + = preattestation.unsigned_consensus_vote.vote_consensus_content + .block_payload_hash) + then + (* The preattestation payload does not match the one set in the + state, we cannot inject the preattestation. *) + let* () = + Events.( + emit + discarding_unexpected_preattestation_with_different_payload + ( delegate, + att_payload_hash, + att_level, + att_round, + payload.proposal.block.payload_hash )) + in + do_nothing state + else do_action + in + check_payload state signed_preattestation + @@ Lwt.return (state, Inject_preattestation {signed_preattestation}) + +let handle_forged_attestation state signed_attestation = let open Lwt_syntax in let { vote_consensus_content = @@ -960,12 +1080,12 @@ let handle_arriving_attestation state signed_attestation = let new_state = {state with round_state = new_round_state} in do_nothing new_state | Idle | Awaiting_attestations | Awaiting_application -> - (* For these three phases, we have necessarily already reached the - prequorum: we are safe to inject. *) - let signed_attestations = - make_singleton_consensus_vote_batch signed_attestation - in - Lwt.return (state, Inject_attestations {signed_attestations}) + (* For these three phases, we should have already reached the prequorum. + If this is not the case, the attestations will not be injected. *) + may_inject_attestations + state + ~first_signed_attestation:signed_attestation + ~other_signed_attestations:[] let handle_forge_event state forge_event = match forge_event with @@ -975,9 +1095,9 @@ let handle_forge_event state forge_event = Inject_block {prepared_block; force_injection = false; asynchronous = true} ) | Preattestation_ready signed_preattestation -> - Lwt.return (state, Inject_preattestation {signed_preattestation}) + handle_forged_preattestation state signed_preattestation | Attestation_ready signed_attestation -> - handle_arriving_attestation state signed_attestation + handle_forged_attestation state signed_attestation (* Hypothesis: - The state is not to be modified outside this module @@ -1089,10 +1209,20 @@ let step (state : Baking_state.t) (event : Baking_state.event) : preattestation_qc | Awaiting_attestations, Quorum_reached (candidate, attestation_qc) -> quorum_reached_when_waiting_attestations state candidate attestation_qc - (* Unreachable cases *) - | Idle, (Prequorum_reached _ | Quorum_reached _) - | Awaiting_preattestations, Quorum_reached _ - | (Awaiting_application | Awaiting_attestations), Prequorum_reached _ - | Awaiting_application, Quorum_reached _ -> - (* This cannot/should not happen *) + (* Unreachable cases modulo concurrency. *) + | ( (Idle | Awaiting_application | Awaiting_attestations), + Prequorum_reached (candidate, _operations_pqc) ) -> + (* Unexpected prequorum reached, we do not lock on it and discard it. *) + let* () = + Events.( + emit discarding_unexpected_prequorum_reached (candidate.hash, phase)) + in + do_nothing state + | ( (Idle | Awaiting_preattestations | Awaiting_application), + Quorum_reached (candidate, _operations_qc) ) -> + (* Unexpected quorum reached, we discard it. *) + let* () = + Events.( + emit discarding_unexpected_quorum_reached (candidate.hash, phase)) + in do_nothing state