(*****************************************************************************) (* *) (* Open Source License *) (* Copyright (c) 2018 Dynamic Ledger Solutions, Inc. *) (* *) (* 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. *) (* *) (*****************************************************************************) (** Endorsing a block adds an extra layer of confidence to the Tezos's PoS algorithm. The block endorsing operation must be included in the following block. Each endorser possess a number of slots corresponding to their priority. After [preserved_cycles], a reward is given to the endorser. This reward depends on the priority of the block that contains the endorsements. *) open Protocol open Alpha_context open Test_utils open Test_tez (****************************************************************) (* Utility functions *) (****************************************************************) let get_expected_reward ctxt ~priority ~baker ~endorsing_power = begin if baker then Context.get_baking_reward ctxt ~priority ~endorsing_power else return (Test_tez.Tez.of_int 0) end >>=? fun baking_reward -> Context.get_endorsing_reward ctxt ~priority ~endorsing_power >>=? fun endorsing_reward -> Test_tez.Tez.(endorsing_reward +? baking_reward) >>?= fun reward -> return reward let get_expected_deposit ctxt ~baker ~endorsing_power = Context.get_constants ctxt >>=? fun Constants. { parametric = { endorsement_security_deposit ; block_security_deposit ; _ } ; _ } -> let open Environment in let open Tez in let baking_deposit = if baker then block_security_deposit else of_int 0 in endorsement_security_deposit *? (Int64.of_int endorsing_power) >>?= fun endorsement_deposit -> endorsement_deposit +? baking_deposit >>?= fun deposit -> return deposit (* [baker] is true if the [pkh] has also baked the current block, in which case correspoding deposit and reward should be ajusted *) let assert_endorser_balance_consistency ~loc ?(priority=0) ?(baker=false) ~endorsing_power ctxt pkh initial_balance = let contract = Contract.implicit_contract pkh in get_expected_reward ctxt ~priority ~baker ~endorsing_power >>=? fun reward -> get_expected_deposit ctxt ~baker ~endorsing_power >>=? fun deposit -> Assert.balance_was_debited ~loc ctxt contract initial_balance deposit >>=? fun () -> Context.Contract.balance ~kind:Rewards ctxt contract >>=? fun reward_balance -> Assert.equal_tez ~loc reward_balance reward >>=? fun () -> Context.Contract.balance ~kind:Deposit ctxt contract >>=? fun deposit_balance -> Assert.equal_tez ~loc deposit_balance deposit let delegates_with_slots endorsers = List.map (fun (endorser: Delegate_services.Endorsing_rights.t) -> endorser.delegate) endorsers let endorsing_power endorsers = List.fold_left (fun sum (endorser: Delegate_services.Endorsing_rights.t) -> sum + List.length endorser.slots) 0 endorsers (****************************************************************) (* Tests *) (****************************************************************) (** Apply a single endorsement from the slot 0 endorser *) let simple_endorsement () = Context.init 5 >>=? fun (b, _) -> Context.get_endorser (B b) >>=? fun (delegate, slots) -> Op.endorsement ~delegate (B b) () >>=? fun op -> Context.Contract.balance (B b) (Contract.implicit_contract delegate) >>=? fun initial_balance -> let policy = Block.Excluding [ delegate ] in Block.get_next_baker ~policy b >>=? fun (_, priority, _) -> Block.bake ~policy ~operations:[Operation.pack op] b >>=? fun b2 -> assert_endorser_balance_consistency ~loc:__LOC__ (B b2) ~priority ~endorsing_power:(List.length slots) delegate initial_balance (** Apply a maximum number of endorsements. An endorser can be selected twice. *) let max_endorsement () = let endorsers_per_block = 16 in Context.init ~endorsers_per_block 32 >>=? fun (b, _) -> Context.get_endorsers (B b) >>=? fun endorsers -> Assert.equal_int ~loc:__LOC__ (List.length (List.concat (List.map (fun { Alpha_services.Delegate.Endorsing_rights.slots ; _ } -> slots) endorsers))) endorsers_per_block >>=? fun () -> fold_left_s (fun (delegates, ops, balances) (endorser : Alpha_services.Delegate.Endorsing_rights.t) -> let delegate = endorser.delegate in Context.Contract.balance (B b) (Contract.implicit_contract delegate) >>=? fun balance -> Op.endorsement ~delegate (B b) () >>=? fun op -> return (delegate :: delegates, Operation.pack op :: ops, (List.length endorser.slots, balance) :: balances) ) ([], [], []) endorsers >>=? fun (delegates, ops, previous_balances) -> Block.bake ~policy:(Excluding delegates) ~operations:(List.rev ops) b >>=? fun b -> (* One account can endorse more than one time per level, we must check that the bonds are summed up *) iter_s (fun (endorser_account, (endorsing_power, previous_balance)) -> assert_endorser_balance_consistency ~loc:__LOC__ (B b) ~endorsing_power endorser_account previous_balance ) (List.combine delegates previous_balances) (** Check every that endorsers' balances are consistent with different priorities *) let consistent_priorities () = let priorities = 0 -- 64 in Context.init 64 >>=? fun (b, _) -> fold_left_s (fun (b, used_pkhes) priority -> (* Choose an endorser that has not baked nor endorsed before *) Context.get_endorsers (B b) >>=? fun endorsers -> let endorser = List.find_opt (fun (e: Delegate_services.Endorsing_rights.t) -> not (Signature.Public_key_hash.Set.mem e.delegate used_pkhes) ) endorsers in match endorser with | None -> return (b, used_pkhes) (* not enough fresh endorsers; we "stop" *) | Some endorser -> Context.Contract.balance (B b) (Contract.implicit_contract endorser.delegate) >>=? fun balance -> Op.endorsement ~delegate:endorser.delegate (B b) () >>=? fun operation -> let operation = Operation.pack operation in Block.get_next_baker ~policy:(By_priority priority) b >>=? fun (baker, _, _) -> let used_pkhes = Signature.Public_key_hash.Set.add baker used_pkhes in let used_pkhes = Signature.Public_key_hash.Set.add endorser.delegate used_pkhes in (* Bake with a specific priority *) Block.bake ~policy:(By_priority priority) ~operation b >>=? fun b -> let is_baker = Signature.Public_key_hash.(baker = endorser.delegate) in assert_endorser_balance_consistency ~loc:__LOC__ ~priority ~baker:is_baker (B b) ~endorsing_power:(List.length endorser.slots) endorser.delegate balance >>=? fun () -> return (b, used_pkhes) ) (b, Signature.Public_key_hash.Set.empty) priorities >>=? fun _b -> return_unit (** Check that after [preserved_cycles] cycles the endorser gets his reward *) let reward_retrieval () = Context.init 5 >>=? fun (b, _) -> Context.get_constants (B b) >>=? fun Constants. { parametric = { preserved_cycles ; _ } ; _ } -> Context.get_endorser (B b) >>=? fun (endorser, slots) -> Context.Contract.balance (B b) (Contract.implicit_contract endorser) >>=? fun balance -> Op.endorsement ~delegate:endorser (B b) () >>=? fun operation -> let operation = Operation.pack operation in let policy = Block.Excluding [ endorser ] in Block.get_next_baker ~policy b >>=? fun (_, priority, _) -> Block.bake ~policy ~operation b >>=? fun b -> (* Bake (preserved_cycles + 1) cycles *) fold_left_s (fun b _ -> Block.bake_until_cycle_end ~policy:(Excluding [ endorser ]) b ) b (0 -- preserved_cycles) >>=? fun b -> get_expected_reward (B b) ~priority ~baker:false ~endorsing_power:(List.length slots) >>=? fun reward -> Assert.balance_was_credited ~loc:__LOC__ (B b) (Contract.implicit_contract endorser) balance reward (** Check that after [preserved_cycles] cycles endorsers get their reward. Two endorsers are used and they endorse in different cycles. *) let reward_retrieval_two_endorsers () = Context.init 5 >>=? fun (b, _) -> Context.get_constants (B b) >>=? fun Constants. { parametric = { preserved_cycles ; endorsement_reward ; endorsement_security_deposit ; _ } ; _ } -> Context.get_endorsers (B b) >>=? fun endorsers -> let endorser1 = List.hd endorsers in let endorser2 = List.hd (List.tl endorsers) in Context.Contract.balance (B b) (Contract.implicit_contract endorser1.delegate) >>=? fun balance1 -> Context.Contract.balance (B b) (Contract.implicit_contract endorser2.delegate) >>=? fun balance2 -> Lwt.return Tez.(endorsement_security_deposit *? Int64.of_int (List.length endorser1.slots)) >>=? fun security_deposit1 -> (* endorser1 endorses the genesis block in cycle 0 *) Op.endorsement ~delegate:endorser1.delegate (B b) () >>=? fun operation1 -> let policy = Block.Excluding [ endorser1.delegate ; endorser2.delegate ] in Block.get_next_baker ~policy b >>=? fun (_, priority, _) -> Tez.(endorsement_reward /? Int64.(succ (of_int priority))) >>?= fun reward_per_slot -> Lwt.return Tez.(reward_per_slot *? Int64.of_int (List.length endorser1.slots)) >>=? fun reward1 -> (* bake next block, include endorsement of endorser1 *) Block.bake ~policy ~operation:(Operation.pack operation1) b >>=? fun b -> Assert.balance_was_debited ~loc:__LOC__ (B b) (Contract.implicit_contract endorser1.delegate) balance1 security_deposit1 >>=? fun () -> Assert.balance_is ~loc:__LOC__ (B b) (Contract.implicit_contract endorser2.delegate) balance2 >>=? fun () -> (* complete cycle 0 *) Block.bake_until_cycle_end ~policy b >>=? fun b -> Assert.balance_was_debited ~loc:__LOC__ (B b) (Contract.implicit_contract endorser1.delegate) balance1 security_deposit1 >>=? fun () -> Assert.balance_is ~loc:__LOC__ (B b) (Contract.implicit_contract endorser2.delegate) balance2 >>=? fun () -> (* get the slots of endorser2 for the current block *) Context.get_endorsers (B b) >>=? fun endorsers -> let same_endorser2 endorser = Signature.Public_key_hash.(endorser.Delegate_services.Endorsing_rights.delegate = endorser2.delegate) in let endorser2 = List.find same_endorser2 endorsers in (* No exception raised: in sandboxed mode endorsers do not change between blocks *) Lwt.return Tez.(endorsement_security_deposit *? Int64.of_int (List.length endorser2.slots)) >>=? fun security_deposit2 -> (* endorser2 endorses the last block in cycle 0 *) Op.endorsement ~delegate:endorser2.delegate (B b) () >>=? fun operation2 -> (* bake first block in cycle 1, include endorsement of endorser2 *) Block.bake ~policy ~operation:(Operation.pack operation2) b >>=? fun b -> let priority = b.header.protocol_data.contents.priority in Tez.(endorsement_reward /? Int64.(succ (of_int priority))) >>?= fun reward_per_slot -> Lwt.return Tez.(reward_per_slot *? Int64.of_int (List.length endorser2.slots)) >>=? fun reward2 -> Assert.balance_was_debited ~loc:__LOC__ (B b) (Contract.implicit_contract endorser1.delegate) balance1 security_deposit1 >>=? fun () -> Assert.balance_was_debited ~loc:__LOC__ (B b) (Contract.implicit_contract endorser2.delegate) balance2 security_deposit2 >>=? fun () -> (* bake [preserved_cycles] cycles *) fold_left_s (fun b _ -> Assert.balance_was_debited ~loc:__LOC__ (B b) (Contract.implicit_contract endorser1.delegate) balance1 security_deposit1 >>=? fun () -> Assert.balance_was_debited ~loc:__LOC__ (B b) (Contract.implicit_contract endorser2.delegate) balance2 security_deposit2 >>=? fun () -> Block.bake_until_cycle_end ~policy b ) b (1 -- preserved_cycles) >>=? fun b -> Assert.balance_was_credited ~loc:__LOC__ (B b) (Contract.implicit_contract endorser1.delegate) balance1 reward1 >>=? fun () -> Assert.balance_was_debited ~loc:__LOC__ (B b) (Contract.implicit_contract endorser2.delegate) balance2 security_deposit2 >>=? fun () -> (* bake cycle [preserved_cycle + 1] *) Block.bake_until_cycle_end ~policy b >>=? fun b -> Assert.balance_was_credited ~loc:__LOC__ (B b) (Contract.implicit_contract endorser1.delegate) balance1 reward1 >>=? fun () -> Assert.balance_was_credited ~loc:__LOC__ (B b) (Contract.implicit_contract endorser2.delegate) balance2 reward2 (****************************************************************) (* The following test scenarios are supposed to raise errors. *) (****************************************************************) (** Wrong endorsement predecessor : apply an endorsement with an incorrect block predecessor *) let wrong_endorsement_predecessor () = Context.init 5 >>=? fun (b, _) -> Context.get_endorser (B b) >>=? fun (genesis_endorser, _slots) -> Block.bake b >>=? fun b' -> Op.endorsement ~delegate:genesis_endorser ~signing_context:(B b) (B b') () >>=? fun operation -> let operation = Operation.pack operation in Block.bake ~operation b' >>= fun res -> Assert.proto_error ~loc:__LOC__ res begin function | Apply.Wrong_endorsement_predecessor _ -> true | _ -> false end (** Invalid_endorsement_level : apply an endorsement with an incorrect level (i.e. the predecessor level) *) let invalid_endorsement_level () = Context.init 5 >>=? fun (b, _) -> Context.get_level (B b) >>=? fun genesis_level -> Block.bake b >>=? fun b -> Op.endorsement ~level:genesis_level (B b) () >>=? fun operation -> let operation = Operation.pack operation in Block.bake ~operation b >>= fun res -> Assert.proto_error ~loc:__LOC__ res begin function | Apply.Invalid_endorsement_level -> true | _ -> false end (** Duplicate endorsement : apply an endorsement that has already been done *) let duplicate_endorsement () = Context.init 5 >>=? fun (b, _) -> Incremental.begin_construction b >>=? fun inc -> Op.endorsement (B b) () >>=? fun operation -> let operation = Operation.pack operation in Incremental.add_operation inc operation >>=? fun inc -> Op.endorsement (B b) () >>=? fun operation -> let operation = Operation.pack operation in Incremental.add_operation inc operation >>= fun res -> Assert.proto_error ~loc:__LOC__ res begin function | Apply.Duplicate_endorsement _ -> true | _ -> false end (** Apply a single endorsement from the slot 0 endorser *) let not_enough_for_deposit () = Context.init 5 ~endorsers_per_block:1 >>=? fun (b_init, contracts) -> Error_monad.map_s (fun c -> Context.Contract.manager (B b_init) c >>=? fun m -> return (m, c)) contracts >>=? fun managers -> Block.bake b_init >>=? fun b -> (* retrieve the level 2's endorser *) Context.get_endorser (B b) >>=? fun (endorser, _slots) -> let _, contract_other_than_endorser = List.find (fun (c, _) -> not (Signature.Public_key_hash.equal c.Account.pkh endorser)) managers in let _, contract_of_endorser = List.find (fun (c, _) -> (Signature.Public_key_hash.equal c.Account.pkh endorser)) managers in Context.Contract.balance (B b) (Contract.implicit_contract endorser) >>=? fun initial_balance -> (* Empty the future endorser account *) Op.transaction (B b_init) contract_of_endorser contract_other_than_endorser initial_balance >>=? fun op_trans -> Block.bake ~operation:op_trans b_init >>=? fun b -> (* Endorse with a zero balance *) Op.endorsement ~delegate:endorser (B b) () >>=? fun op_endo -> Block.bake ~policy:(Excluding [endorser]) ~operation:(Operation.pack op_endo) b >>= fun res -> Assert.proto_error ~loc:__LOC__ res begin function | Delegate_storage.Balance_too_low_for_deposit _ -> true | _ -> false end (* check that a block with not enough endorsement cannot be baked *) let endorsement_threshold () = let initial_endorsers = 28 in let num_accounts = 100 in Context.init ~initial_endorsers num_accounts >>=? fun (b, _) -> Context.get_endorsers (B b) >>=? fun endorsers -> let num_endorsers = List.length endorsers in (* we try to bake with more and more endorsers, but at each iteration with a timestamp smaller than required *) iter_s (fun i -> (* the priority is chosen rather arbitrarily *) let priority = num_endorsers - i in let crt_endorsers = List.take_n i endorsers in let endorsing_power = endorsing_power crt_endorsers in let delegates = delegates_with_slots crt_endorsers in map_s (fun x -> Op.endorsement ~delegate:x (B b) ()) delegates >>=? fun ops -> Context.get_minimal_valid_time (B b) ~priority ~endorsing_power >>=? fun timestamp -> (* decrease the timestamp by one second *) let seconds = Int64.(sub (of_string (Timestamp.to_seconds_string timestamp)) 1L) in match Timestamp.of_seconds (Int64.to_string seconds) with | None -> failwith "timestamp to/from string manipulation failed" | Some timestamp -> Block.bake ~timestamp ~policy:(By_priority priority) ~operations:(List.map Operation.pack ops) b >>= fun b2 -> Assert.proto_error ~loc:__LOC__ b2 begin function | Baking.Timestamp_too_early _ | Apply.Not_enough_endorsements_for_priority _ -> true | _ -> false end) (0 -- (num_endorsers-1)) >>=? fun () -> (* we bake with all endorsers endorsing, at the right time *) let priority = 0 in let endorsing_power = endorsing_power endorsers in let delegates = delegates_with_slots endorsers in map_s (fun delegate -> Op.endorsement ~delegate (B b) ()) delegates >>=? fun ops -> Context.get_minimal_valid_time (B b) ~priority ~endorsing_power >>=? fun timestamp -> Block.bake ~policy:(By_priority priority) ~timestamp ~operations:(List.map Operation.pack ops) b >>= fun _ -> return_unit let test_fitness_gap () = let num_accounts = 5 in Context.init num_accounts >>=? fun (b, _) -> begin match Fitness_repr.to_int64 b.header.shell.fitness with | Ok fitness -> return (Int64.to_int fitness) | Error _ -> assert false end >>=? fun fitness -> Context.get_endorser (B b) >>=? fun (delegate, _slots) -> Op.endorsement ~delegate (B b) () >>=? fun op -> (* bake at priority 0 succeed thanks to enough endorsements *) Block.bake ~policy:(By_priority 0) ~operations:[Operation.pack op] b >>=? fun b -> begin match Fitness_repr.to_int64 b.header.shell.fitness with | Ok new_fitness -> return ((Int64.to_int new_fitness) - fitness) | Error _ -> assert false end >>=? fun res -> (* in Emmy+, the fitness increases by 1, so the difference between the fitness at level 1 and at level 0 is 1, independently if the number fo endorements (here 1) *) Assert.equal_int ~loc:__LOC__ res 1 >>=? fun () -> return_unit let tests = [ Test.tztest "Simple endorsement" `Quick simple_endorsement ; Test.tztest "Maximum endorsement" `Quick max_endorsement ; Test.tztest "Consistent priorities" `Quick consistent_priorities ; Test.tztest "Reward retrieval" `Quick reward_retrieval ; Test.tztest "Reward retrieval two endorsers" `Quick reward_retrieval_two_endorsers ; Test.tztest "Endorsement threshold" `Quick endorsement_threshold ; Test.tztest "Fitness gap" `Quick test_fitness_gap ; (* Fail scenarios *) Test.tztest "Wrong endorsement predecessor" `Quick wrong_endorsement_predecessor ; Test.tztest "Invalid endorsement level" `Quick invalid_endorsement_level ; Test.tztest "Duplicate endorsement" `Quick duplicate_endorsement ; Test.tztest "Not enough for deposit" `Quick not_enough_for_deposit ; ]