276 lines
11 KiB
OCaml
276 lines
11 KiB
OCaml
|
(*****************************************************************************)
|
||
|
(* *)
|
||
|
(* Open Source License *)
|
||
|
(* Copyright (c) 2018 Dynamic Ledger Solutions, Inc. <contact@tezos.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. *)
|
||
|
(* *)
|
||
|
(*****************************************************************************)
|
||
|
|
||
|
open Alpha_context
|
||
|
|
||
|
(** Returns the proposal submitted by the most delegates.
|
||
|
Returns None in case of a tie or if there are no proposals. *)
|
||
|
let select_winning_proposal proposals =
|
||
|
let merge proposal vote winners =
|
||
|
match winners with
|
||
|
| None -> Some ([proposal], vote)
|
||
|
| Some (winners, winners_vote) as previous ->
|
||
|
if Compare.Int32.(vote = winners_vote) then
|
||
|
Some (proposal :: winners, winners_vote)
|
||
|
else if Compare.Int32.(vote >= winners_vote) then
|
||
|
Some ([proposal], vote)
|
||
|
else
|
||
|
previous in
|
||
|
match Protocol_hash.Map.fold merge proposals None with
|
||
|
| None -> None
|
||
|
| Some ([proposal], _) -> Some proposal
|
||
|
| Some _ -> None (* in case of a tie, lets do nothing. *)
|
||
|
|
||
|
(** A proposal is approved if it has supermajority and the participation reaches
|
||
|
the current quorum.
|
||
|
Supermajority means the yays are more 8/10 of casted votes.
|
||
|
The participation is the ratio of all received votes, including passes, with
|
||
|
respect to the number of possible votes. The quorum starts at 80% and at
|
||
|
each vote is updated using the last expected quorum and the current
|
||
|
participation with the following weights:
|
||
|
newQ = oldQ * 8/10 + participation * 2/10 *)
|
||
|
let check_approval_and_update_quorum ctxt =
|
||
|
Vote.get_ballots ctxt >>=? fun ballots ->
|
||
|
Vote.listing_size ctxt >>=? fun maximum_vote ->
|
||
|
Vote.get_current_quorum ctxt >>=? fun expected_quorum ->
|
||
|
(* Note overflows: considering a maximum of 8e8 tokens, with roll size as
|
||
|
small as 1e3, there is a maximum of 8e5 rolls and thus votes.
|
||
|
In 'participation' an Int64 is used because in the worst case 'all_votes is
|
||
|
8e5 and after the multiplication is 8e9, making it potentially overflow a
|
||
|
signed Int32 which is 2e9. *)
|
||
|
let casted_votes = Int32.add ballots.yay ballots.nay in
|
||
|
let all_votes = Int32.add casted_votes ballots.pass in
|
||
|
let supermajority = Int32.div (Int32.mul 8l casted_votes) 10l in
|
||
|
let participation = (* in centile of percentage *)
|
||
|
Int64.to_int32
|
||
|
(Int64.div
|
||
|
(Int64.mul (Int64.of_int32 all_votes) 100_00L)
|
||
|
(Int64.of_int32 maximum_vote)) in
|
||
|
let outcome = Compare.Int32.(participation >= expected_quorum &&
|
||
|
ballots.yay >= supermajority) in
|
||
|
let updated_quorum =
|
||
|
Int32.div (Int32.add (Int32.mul 8l expected_quorum) (Int32.mul 2l participation)) 10l in
|
||
|
Vote.set_current_quorum ctxt updated_quorum >>=? fun ctxt ->
|
||
|
return (ctxt, outcome)
|
||
|
|
||
|
(** Implements the state machine of the amendment procedure.
|
||
|
Note that [freeze_listings], that computes the vote weight of each delegate,
|
||
|
is run at the beginning of each voting period.
|
||
|
*)
|
||
|
let start_new_voting_period ctxt =
|
||
|
Vote.get_current_period_kind ctxt >>=? function
|
||
|
| Proposal -> begin
|
||
|
Vote.get_proposals ctxt >>=? fun proposals ->
|
||
|
Vote.clear_proposals ctxt >>= fun ctxt ->
|
||
|
Vote.clear_listings ctxt >>=? fun ctxt ->
|
||
|
match select_winning_proposal proposals with
|
||
|
| None ->
|
||
|
Vote.freeze_listings ctxt >>=? fun ctxt ->
|
||
|
return ctxt
|
||
|
| Some proposal ->
|
||
|
Vote.init_current_proposal ctxt proposal >>=? fun ctxt ->
|
||
|
Vote.freeze_listings ctxt >>=? fun ctxt ->
|
||
|
Vote.set_current_period_kind ctxt Testing_vote >>=? fun ctxt ->
|
||
|
return ctxt
|
||
|
end
|
||
|
| Testing_vote ->
|
||
|
check_approval_and_update_quorum ctxt >>=? fun (ctxt, approved) ->
|
||
|
Vote.clear_ballots ctxt >>= fun ctxt ->
|
||
|
Vote.clear_listings ctxt >>=? fun ctxt ->
|
||
|
if approved then
|
||
|
let expiration = (* in two days maximum... *)
|
||
|
Time.add (Timestamp.current ctxt) (Constants.test_chain_duration ctxt) in
|
||
|
Vote.get_current_proposal ctxt >>=? fun proposal ->
|
||
|
fork_test_chain ctxt proposal expiration >>= fun ctxt ->
|
||
|
Vote.set_current_period_kind ctxt Testing >>=? fun ctxt ->
|
||
|
return ctxt
|
||
|
else
|
||
|
Vote.clear_current_proposal ctxt >>=? fun ctxt ->
|
||
|
Vote.freeze_listings ctxt >>=? fun ctxt ->
|
||
|
Vote.set_current_period_kind ctxt Proposal >>=? fun ctxt ->
|
||
|
return ctxt
|
||
|
| Testing ->
|
||
|
Vote.freeze_listings ctxt >>=? fun ctxt ->
|
||
|
Vote.set_current_period_kind ctxt Promotion_vote >>=? fun ctxt ->
|
||
|
return ctxt
|
||
|
| Promotion_vote ->
|
||
|
check_approval_and_update_quorum ctxt >>=? fun (ctxt, approved) ->
|
||
|
begin
|
||
|
if approved then
|
||
|
Vote.get_current_proposal ctxt >>=? fun proposal ->
|
||
|
activate ctxt proposal >>= fun ctxt ->
|
||
|
return ctxt
|
||
|
else
|
||
|
return ctxt
|
||
|
end >>=? fun ctxt ->
|
||
|
Vote.clear_ballots ctxt >>= fun ctxt ->
|
||
|
Vote.clear_listings ctxt >>=? fun ctxt ->
|
||
|
Vote.clear_current_proposal ctxt >>=? fun ctxt ->
|
||
|
Vote.freeze_listings ctxt >>=? fun ctxt ->
|
||
|
Vote.set_current_period_kind ctxt Proposal >>=? fun ctxt ->
|
||
|
return ctxt
|
||
|
|
||
|
type error += (* `Branch *)
|
||
|
| Invalid_proposal
|
||
|
| Unexpected_proposal
|
||
|
| Unauthorized_proposal
|
||
|
| Too_many_proposals
|
||
|
| Empty_proposal
|
||
|
| Unexpected_ballot
|
||
|
| Unauthorized_ballot
|
||
|
|
||
|
let () =
|
||
|
let open Data_encoding in
|
||
|
(* Invalid proposal *)
|
||
|
register_error_kind
|
||
|
`Branch
|
||
|
~id:"invalid_proposal"
|
||
|
~title:"Invalid proposal"
|
||
|
~description:"Ballot provided for a proposal that is not the current one."
|
||
|
~pp:(fun ppf () -> Format.fprintf ppf "Invalid proposal")
|
||
|
empty
|
||
|
(function Invalid_proposal -> Some () | _ -> None)
|
||
|
(fun () -> Invalid_proposal) ;
|
||
|
(* Unexpected proposal *)
|
||
|
register_error_kind
|
||
|
`Branch
|
||
|
~id:"unexpected_proposal"
|
||
|
~title:"Unexpected proposal"
|
||
|
~description:"Proposal recorded outside of a proposal period."
|
||
|
~pp:(fun ppf () -> Format.fprintf ppf "Unexpected proposal")
|
||
|
empty
|
||
|
(function Unexpected_proposal -> Some () | _ -> None)
|
||
|
(fun () -> Unexpected_proposal) ;
|
||
|
(* Unauthorized proposal *)
|
||
|
register_error_kind
|
||
|
`Branch
|
||
|
~id:"unauthorized_proposal"
|
||
|
~title:"Unauthorized proposal"
|
||
|
~description:"The delegate provided for the proposal is not in the voting listings."
|
||
|
~pp:(fun ppf () -> Format.fprintf ppf "Unauthorized proposal")
|
||
|
empty
|
||
|
(function Unauthorized_proposal -> Some () | _ -> None)
|
||
|
(fun () -> Unauthorized_proposal) ;
|
||
|
(* Unexpected ballot *)
|
||
|
register_error_kind
|
||
|
`Branch
|
||
|
~id:"unexpected_ballot"
|
||
|
~title:"Unexpected ballot"
|
||
|
~description:"Ballot recorded outside of a voting period."
|
||
|
~pp:(fun ppf () -> Format.fprintf ppf "Unexpected ballot")
|
||
|
empty
|
||
|
(function Unexpected_ballot -> Some () | _ -> None)
|
||
|
(fun () -> Unexpected_ballot) ;
|
||
|
(* Unauthorized ballot *)
|
||
|
register_error_kind
|
||
|
`Branch
|
||
|
~id:"unauthorized_ballot"
|
||
|
~title:"Unauthorized ballot"
|
||
|
~description:"The delegate provided for the ballot is not in the voting listings."
|
||
|
~pp:(fun ppf () -> Format.fprintf ppf "Unauthorized ballot")
|
||
|
empty
|
||
|
(function Unauthorized_ballot -> Some () | _ -> None)
|
||
|
(fun () -> Unauthorized_ballot) ;
|
||
|
(* Too many proposals *)
|
||
|
register_error_kind
|
||
|
`Branch
|
||
|
~id:"too_many_proposals"
|
||
|
~title:"Too many proposals"
|
||
|
~description:"The delegate reached the maximum number of allowed proposals."
|
||
|
~pp:(fun ppf () -> Format.fprintf ppf "Too many proposals")
|
||
|
empty
|
||
|
(function Too_many_proposals -> Some () | _ -> None)
|
||
|
(fun () -> Too_many_proposals) ;
|
||
|
(* Empty proposal *)
|
||
|
register_error_kind
|
||
|
`Branch
|
||
|
~id:"empty_proposal"
|
||
|
~title:"Empty proposal"
|
||
|
~description:"Proposal lists cannot be empty."
|
||
|
~pp:(fun ppf () -> Format.fprintf ppf "Empty proposal")
|
||
|
empty
|
||
|
(function Empty_proposal -> Some () | _ -> None)
|
||
|
(fun () -> Empty_proposal)
|
||
|
|
||
|
(* @return [true] if [List.length l] > [n] w/o computing length *)
|
||
|
let rec longer_than l n =
|
||
|
if Compare.Int.(n < 0) then assert false else
|
||
|
match l with
|
||
|
| [] -> false
|
||
|
| _ :: rest ->
|
||
|
if Compare.Int.(n = 0) then true
|
||
|
else (* n > 0 *)
|
||
|
longer_than rest (n-1)
|
||
|
|
||
|
let record_proposals ctxt delegate proposals =
|
||
|
begin match proposals with
|
||
|
| [] -> fail Empty_proposal
|
||
|
| _ :: _ -> return_unit
|
||
|
end >>=? fun () ->
|
||
|
Vote.get_current_period_kind ctxt >>=? function
|
||
|
| Proposal ->
|
||
|
Vote.in_listings ctxt delegate >>= fun in_listings ->
|
||
|
if in_listings then
|
||
|
Vote.recorded_proposal_count_for_delegate ctxt delegate >>=? fun count ->
|
||
|
fail_when
|
||
|
(longer_than proposals (Constants.max_proposals_per_delegate - count))
|
||
|
Too_many_proposals >>=? fun () ->
|
||
|
fold_left_s
|
||
|
(fun ctxt proposal ->
|
||
|
Vote.record_proposal ctxt proposal delegate)
|
||
|
ctxt proposals >>=? fun ctxt ->
|
||
|
return ctxt
|
||
|
else
|
||
|
fail Unauthorized_proposal
|
||
|
| Testing_vote | Testing | Promotion_vote ->
|
||
|
fail Unexpected_proposal
|
||
|
|
||
|
let record_ballot ctxt delegate proposal ballot =
|
||
|
Vote.get_current_period_kind ctxt >>=? function
|
||
|
| Testing_vote | Promotion_vote ->
|
||
|
Vote.get_current_proposal ctxt >>=? fun current_proposal ->
|
||
|
fail_unless (Protocol_hash.equal proposal current_proposal)
|
||
|
Invalid_proposal >>=? fun () ->
|
||
|
Vote.has_recorded_ballot ctxt delegate >>= fun has_ballot ->
|
||
|
fail_when has_ballot Unauthorized_ballot >>=? fun () ->
|
||
|
Vote.in_listings ctxt delegate >>= fun in_listings ->
|
||
|
if in_listings then
|
||
|
Vote.record_ballot ctxt delegate ballot
|
||
|
else
|
||
|
fail Unauthorized_ballot
|
||
|
| Testing | Proposal ->
|
||
|
fail Unexpected_ballot
|
||
|
|
||
|
let last_of_a_voting_period ctxt l =
|
||
|
Compare.Int32.(Int32.succ l.Level.voting_period_position =
|
||
|
Constants.blocks_per_voting_period ctxt )
|
||
|
|
||
|
let may_start_new_voting_period ctxt =
|
||
|
let level = Level.current ctxt in
|
||
|
if last_of_a_voting_period ctxt level then
|
||
|
start_new_voting_period ctxt
|
||
|
else
|
||
|
return ctxt
|