123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485(*****************************************************************************)(* *)(* 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. *)(* *)(*****************************************************************************)typeround=int32typet=roundmoduleMap=Map.Make(Int32)include(Compare.Int32:Compare.Swithtypet:=t)letzero=0lletsuccn=ifCompare.Int32.equalnInt32.max_inttheninvalid_arg"round_repr.succ: cannot apply succ to maximum round value"elseInt32.succnletppfmti=Format.fprintffmt"%ld"itypeerror+=Negative_roundofinttypeerror+=Round_overflowofintlet()=letopenData_encodinginregister_error_kind`Permanent~id:"negative_round"~title:"Negative round"~description:"Round cannot be built out of negative integers."~pp:(funppfi->Format.fprintfppf"Negative round cannot be built out of negative integers (%Ld)"i)(obj1(req"Negative_round"int64))(functionNegative_roundi->Some(Int64.of_inti)|_->None)(funi->Negative_round(Int64.to_inti));register_error_kind`Permanent~id:"round_overflow"~title:"Round overflow"~description:"Round cannot be built out of integer greater than maximum int32 value."~pp:(funppfi->Format.fprintfppf"Round cannot be built out of integer greater than maximum int32 value \
(%Ld)"i)(obj1(req"Round_overflow"int64))(functionRound_overflowi->Some(Int64.of_inti)|_->None)(funi->Round_overflow(Int64.to_inti))letof_int32i=ifi>=0lthenOkielseerror(Negative_round(Int32.to_inti))[@@inline]letpredr=letp=Int32.predrinof_int32pletof_inti=ifCompare.Int.(i<0)thenerror(Negative_roundi)else(* i is positive *)leti32=Int32.of_intiinifCompare.Int.(Int32.to_inti32=i)thenOki32elseerror(Round_overflowi)letto_inti32=leti=Int32.to_inti32inifInt32.(equal(of_inti)i32)thenokielseerror(Round_overflowi)letto_int32t=t[@@inline]letto_slotround~committee_size=to_intround>>?funr->letslot=rmodcommittee_sizeinSlot_repr.of_intslotletencoding=Data_encoding.conv_with_guard(funi->i)(funi->matchof_int32iwith|Ok_asres->res|Error_->Error"Round_repr.encoding: negative round")Data_encoding.int32moduleDurations=structtypet={first_round_duration:Period_repr.t;delay_increment_per_round:Period_repr.t;}typeerror+=|Non_increasing_roundsof{increment:Period_repr.t}|Round_durations_must_be_at_least_one_secondof{round:Period_repr.t}let()=register_error_kind`Permanent~id:"durations.non_increasing_rounds"~title:"Non increasing round"~description:"The provided rounds are not increasing."~pp:(funppfincrement->Format.fprintfppf"The provided rounds are not increasing (increment: %a)"Period_repr.ppincrement)Data_encoding.(obj1(req"increment"Period_repr.encoding))(function|Non_increasing_rounds{increment}->Someincrement|_->None)(funincrement->Non_increasing_rounds{increment})letppfmtt=Format.fprintffmt"%a,@ +%a"Period_repr.ppt.first_round_durationPeriod_repr.ppt.delay_increment_per_roundletcreate~first_round_duration~delay_increment_per_round=error_whenCompare.Int64.(Period_repr.to_secondsfirst_round_duration<1L)(Round_durations_must_be_at_least_one_second{round=first_round_duration})>>?fun()->error_whenCompare.Int64.(Period_repr.to_secondsdelay_increment_per_round<1L)(Non_increasing_rounds{increment=delay_increment_per_round})>>?fun()->ok{first_round_duration;delay_increment_per_round}letcreate_opt~first_round_duration~delay_increment_per_round=matchcreate~first_round_duration~delay_increment_per_roundwith|Okv->Somev|Error_->Noneletencoding=letopenData_encodinginconv_with_guard(fun{first_round_duration;delay_increment_per_round}->(first_round_duration,delay_increment_per_round))(fun(first_round_duration,delay_increment_per_round)->matchcreate_opt~first_round_duration~delay_increment_per_roundwith|None->Error"Either round durations are non-increasing or minimal block \
delay < 1"|Somerounds->Okrounds)(obj2(req"first_round_duration"Period_repr.encoding)(req"delay_increment_per_round"Period_repr.encoding))letround_duration{first_round_duration;delay_increment_per_round}round=ifCompare.Int32.(round<0l)theninvalid_arg"round must be a non-negative integer"elseletfirst_round_duration_s=Period_repr.to_secondsfirst_round_durationanddelay_increment_per_round_s=Period_repr.to_secondsdelay_increment_per_roundinPeriod_repr.of_seconds_exnInt64.(addfirst_round_duration_s(mul(of_int32round)delay_increment_per_round_s))endtypeerror+=Round_too_highofint32let()=letopenData_encodinginregister_error_kind`Permanent~id:"round_too_high"~title:"round too high"~description:"block round too high."~pp:(funppfround->Format.fprintfppf"Block round is too high: %ld"round)(obj1(req"level_offset_too_high"int32))(functionRound_too_highround->Someround|_->None)(funround->Round_too_highround)(* The duration of round n follows the arithmetic sequence:
round_duration(0) = first_round_duration
round_duration(r+1) = round_duration(r) + delay_increment_per_round
Hence, this sequence can be explicited into:
round_duration(r) = first_round_duration + r * delay_increment_per_round
The level offset of round r is the sum of the durations of the rounds up
until round r - 1. In other words, when r > 0
raw_level_offset_of_round(0) = 0
raw_level_offset_of_round(r+1) =
raw_level_offset_of_round(r) + round_duration(r)
Hence
raw_level_offset_of_round(r) = Σ_{k=0}^{r-1} (round_duration(k))
After unfolding the series, the same function can be finally explicited into
raw_level_offset_of_round(0) = 0
raw_level_offset_of_round(r) = r * first_round_duration
+ 1/2 * r * (r - 1) * delay_increment_per_round
*)letraw_level_offset_of_roundround_durations~round=ifCompare.Int32.(round=zero)thenokInt64.zeroelseletsum_durations=letDurations.{first_round_duration;delay_increment_per_round}=round_durationsinletroundz=Int64.of_int32roundinletm=Z.of_int64Int64.(div(mulroundz(predroundz))(of_int2))inZ.(add(mulm(Z.of_int64@@Period_repr.to_secondsdelay_increment_per_round))(mul(Z.of_int32round)(Z.of_int64@@Period_repr.to_secondsfirst_round_duration)))inifCompare.Z.(sum_durations>Z.of_int64Int64.max_int)thenerror(Round_too_highround)elseok(Z.to_int64sum_durations)typeerror+=Level_offset_too_highofPeriod_repr.tlet()=letopenData_encodinginregister_error_kind`Permanent~id:"level_offset_too_high"~title:"level offset too high"~description:"The block's level offset is too high."~pp:(funppfoffset->Format.fprintfppf"The block's level offset is too high: %a"Period_repr.ppoffset)(obj1(req"level_offset_too_high"Period_repr.encoding))(functionLevel_offset_too_highoffset->Someoffset|_->None)(funoffset->Level_offset_too_highoffset)typeround_and_offset={round:int32;offset:Period_repr.t}(** Complexity: O(log level_offset). *)letround_and_offsetround_durations~level_offset=letlevel_offset_in_seconds=Period_repr.to_secondslevel_offsetin(* We set the bound as 2^53 to prevent overflows when computing the
variable [discr] for reasonable values of [first_round_duration] and
[delay_increment_per_round]. This bound is derived by a rough approximation
from the inequation [discr] < Int64.max_int. *)letoverflow_bound=Int64.shift_rightInt64.max_int10inifCompare.Int64.(overflow_bound<level_offset_in_seconds)thenerror(Level_offset_too_highlevel_offset)elseletDurations.{first_round_duration;delay_increment_per_round}=round_durationsinletfirst_round_duration=Period_repr.to_secondsfirst_round_durationinletdelay_increment_per_round=Period_repr.to_secondsdelay_increment_per_roundin(* If [level_offset] is lower than the first round duration, then
the solution straightforward. *)ifCompare.Int64.(level_offset_in_seconds<first_round_duration)thenok{round=0l;offset=level_offset}elseletround=ifCompare.Int64.(delay_increment_per_round=Int64.zero)then(* Case when delay_increment_per_round is zero and a simple
linear solution exists. *)Int64.divlevel_offset_in_secondsfirst_round_durationelse(* Case when the increment is non-negative and we look for the
quadratic solution. *)letpow_2n=Int64.mulnninletdoublen=Int64.shift_leftn1inlettimes_8n=Int64.shift_leftn3inlethalfn=Int64.shift_rightn1in(* The integer square root is implemented using the Newton-Raphson
method. For any integer N, the convergence within the
neighborhood of √N is ensured within log2 (N) steps. *)letsqrt(n:int64)=letx0=ref(halfn)inifCompare.Int64.(!x0>1L)then(letx1=ref(half(Int64.add!x0(Int64.divn!x0)))inwhileCompare.Int64.(!x1<!x0)dox0:=!x1;x1:=half(Int64.add!x0(Int64.divn!x0))done;!x0)elsenin(* The idea is to solve the following equation in [round] and
use its integer value:
Σ_{k=0}^{round-1} round_duration(k) = level_offset
After unfolding the sum and expanding terms, we obtain a
quadratic equation:
delay_increment_per_round × round²
+ (2 first_round_duration - delay_increment_per_round) × round
- 2 level_offset
= 0
From there, we compute the discriminant and the solution of
the equation.
Refer to https://gitlab.com/tezos/tezos/-/merge_requests/4009
for more explanations.
*)letdiscr=Int64.add(pow_2(Int64.sub(doublefirst_round_duration)delay_increment_per_round))(times_8(Int64.muldelay_increment_per_roundlevel_offset_in_seconds))inInt64.div(Int64.add(Int64.subdelay_increment_per_round(doublefirst_round_duration))(sqrtdiscr))(doubledelay_increment_per_round)inraw_level_offset_of_roundround_durations~round:(Int64.to_int32round)>>?funcurrent_level_offset->ok{round=Int64.to_int32round;offset=Period_repr.of_seconds_exn(Int64.sub(Period_repr.to_secondslevel_offset)current_level_offset);}(** Complexity: O(|round_durations|). *)lettimestamp_of_roundround_durations~predecessor_timestamp~predecessor_round~round=letpred_round_duration=Durations.round_durationround_durationspredecessor_roundin(* First, the function computes when the current level l is supposed
to start. This is given by adding to the timestamp of the round
of predecessor level l-1 [predecessor_timestamp], the duration of
its last round [predecessor_round]. *)Time_repr.(predecessor_timestamp+?pred_round_duration)>>?funstart_of_current_level->(* Finally, we sum the durations of the rounds at the current level l until
reaching current [round]. *)raw_level_offset_of_roundround_durations~round>>?funlevel_offset->letlevel_offset=Period_repr.of_seconds_exnlevel_offsetinTime_repr.(start_of_current_level+?level_offset)(** Unlike [timestamp_of_round], this function gets the starting time
of a given round, given the timestamp and the round of a proposal
at the same level.
We compute the starting time of [considered_round] from a given
[round_durations] description, some [current_round], and its
starting time [current_timestamp].
Complexity: O(|round_durations|). *)lettimestamp_of_another_round_same_levelround_durations~current_timestamp~current_round~considered_round=raw_level_offset_of_roundround_durations~round:considered_round>>?funtarget_offset->raw_level_offset_of_roundround_durations~round:current_round>>?funcurrent_offset->ok@@Time_repr.of_secondsInt64.(add(sub(Time_repr.to_secondscurrent_timestamp)current_offset)target_offset)typeerror+=|Round_of_past_timestampof{provided_timestamp:Time.t;predecessor_timestamp:Time.t;predecessor_round:t;}let()=letopenData_encodinginregister_error_kind`Permanent~id:"round_of_past_timestamp"~title:"Round_of_timestamp for past timestamp"~description:"Provided timestamp is before the expected level start."~pp:(funppf(provided_ts,predecessor_ts,round)->Format.fprintfppf"Provided timestamp (%a) is before the expected level start (computed \
based on predecessor_ts %a at round %a)."Time.pp_humprovided_tsTime.pp_humpredecessor_tsppround)(obj3(req"provided_timestamp"Time.encoding)(req"predecessor_timestamp"Time.encoding)(req"predecessor_round"encoding))(function|Round_of_past_timestamp{provided_timestamp;predecessor_timestamp;predecessor_round}->Some(provided_timestamp,predecessor_timestamp,predecessor_round)|_->None)(fun(provided_timestamp,predecessor_timestamp,predecessor_round)->Round_of_past_timestamp{provided_timestamp;predecessor_timestamp;predecessor_round})letround_of_timestampround_durations~predecessor_timestamp~predecessor_round~timestamp=letround_duration=Durations.round_durationround_durationspredecessor_roundinTime_repr.(predecessor_timestamp+?round_duration)>>?funstart_of_current_level->Period_repr.of_seconds(Time_repr.difftimestampstart_of_current_level)|>Error_monad.record_trace(Round_of_past_timestamp{predecessor_timestamp;provided_timestamp=timestamp;predecessor_round;})>>?fundiff->round_and_offsetround_durations~level_offset:diff>>?funround_and_offset->okround_and_offset.roundletlevel_offset_of_roundround_durations~round=raw_level_offset_of_roundround_durations~round>>?funoffset->ok(Period_repr.of_seconds_exnoffset)moduleInternals_for_test=structtyperound_and_offset_raw={round:round;offset:Period_repr.t}letround_and_offsetround_durations~level_offset=round_and_offsetround_durations~level_offset>|?funv->{round=v.round;offset=v.offset}end