From c2cf5133009563bef084cf1da19b8ef1652e7a4c Mon Sep 17 00:00:00 2001 From: Mathias Bourgoin Date: Thu, 21 Dec 2023 17:04:52 +0100 Subject: [PATCH 1/3] Tezt/Tezos: Add missing RPC/client commands for Adaptive Issuance --- tezt/lib_tezos/RPC.ml | 85 +++++++++++++++++++++++++++++++++++++++ tezt/lib_tezos/RPC.mli | 50 ++++++++++++++++++++++- tezt/lib_tezos/client.ml | 47 +++++++++++++++++----- tezt/lib_tezos/client.mli | 38 ++++++++++++++--- 4 files changed, 204 insertions(+), 16 deletions(-) diff --git a/tezt/lib_tezos/RPC.ml b/tezt/lib_tezos/RPC.ml index b08cb92975b5..b979be969dbd 100644 --- a/tezt/lib_tezos/RPC.ml +++ b/tezt/lib_tezos/RPC.ml @@ -351,6 +351,11 @@ let get_chain_block_metadata ?(chain = "main") ?(block = "head") ?version () = balance_updates; } +let get_chain_block_metadata_raw ?(chain = "main") ?(block = "head") ?version () + = + let query_string = Query_arg.opt "version" Fun.id version in + make ~query_string GET ["chains"; chain; "blocks"; block; "metadata"] Fun.id + let get_chain_block_protocols ?(chain = "main") ?(block = "head") () = make GET ["chains"; chain; "blocks"; block; "protocols"] Fun.id @@ -729,6 +734,70 @@ let get_chain_block_context_contract_staking_numerator ?(chain = "main") ] JSON.as_int +let get_chain_block_context_contract_staked_balance ?(chain = "main") + ?(block = "head") contract = + make + GET + [ + "chains"; + chain; + "blocks"; + block; + "context"; + "contracts"; + contract; + "staked_balance"; + ] + JSON.as_int + +let get_chain_block_context_contract_unstake_requests ?(chain = "main") + ?(block = "head") contract = + make + GET + [ + "chains"; + chain; + "blocks"; + block; + "context"; + "contracts"; + contract; + "unstake_requests"; + ] + Fun.id + +let get_chain_block_context_contract_unstaked_finalizable_balance + ?(chain = "main") ?(block = "head") contract = + make + GET + [ + "chains"; + chain; + "blocks"; + block; + "context"; + "contracts"; + contract; + "unstaked_finalizable_balance"; + ] + JSON.as_int + +let get_chain_block_context_contract_unstaked_frozen_balance ?(chain = "main") + ?(block = "head") contract = + make + GET + [ + "chains"; + chain; + "blocks"; + block; + "context"; + "contracts"; + contract; + "unstaked_frozen_balance"; + ] + JSON.as_int + let get_chain_block_helper_baking_rights ?(chain = "main") ?(block = "head") ?delegate ?level () = let query_string = @@ -1149,6 +1218,22 @@ let get_chain_block_context_delegate ?(chain = "main") ?(block = "head") pkh = ["chains"; chain; "blocks"; block; "context"; "delegates"; pkh] Fun.id +let get_chain_block_context_delegate_active_staking_parameters ?(chain = "main") + ?(block = "head") pkh = + make + GET + [ + "chains"; + chain; + "blocks"; + block; + "context"; + "delegates"; + pkh; + "active_staking_parameters"; + ] + Fun.id + let get_chain_block_context_delegate_current_frozen_deposits ?(chain = "main") ?(block = "head") pkh = make diff --git a/tezt/lib_tezos/RPC.mli b/tezt/lib_tezos/RPC.mli index 2d3604bff9c4..bc4a268c34b5 100644 --- a/tezt/lib_tezos/RPC.mli +++ b/tezt/lib_tezos/RPC.mli @@ -319,6 +319,13 @@ type block_metadata = { val get_chain_block_metadata : ?chain:string -> ?block:string -> ?version:string -> unit -> block_metadata t +(** RPC: [GET /chains//blocks//metadata] + + [chain] defaults to ["main"]. + [block] defaults to ["head"]. *) +val get_chain_block_metadata_raw : + ?chain:string -> ?block:string -> ?version:string -> unit -> JSON.t t + (** RPC: [GET /chains//blocks//protocols] [chain] defaults to ["main"]. @@ -707,6 +714,38 @@ val get_chain_block_context_contract_storage_paid_space : val get_chain_block_context_contract_staking_numerator : ?chain:string -> ?block:string -> string -> int t +(** RPC: [GET /chains//blocks//context/contracts//staked_balance] + + [chain] defaults to ["main"]. + [block] defaults to ["head"]. +*) +val get_chain_block_context_contract_staked_balance : + ?chain:string -> ?block:string -> string -> int t + +(** RPC: [GET /chains//blocks//context/contracts//unstake_requests] + + [chain] defaults to ["main"]. + [block] defaults to ["head"]. +*) +val get_chain_block_context_contract_unstake_requests : + ?chain:string -> ?block:string -> string -> JSON.t t + +(** RPC: [GET /chains//blocks//context/contracts//unstaked_finalizable_balance] + + [chain] defaults to ["main"]. + [block] defaults to ["head"]. +*) +val get_chain_block_context_contract_unstaked_finalizable_balance : + ?chain:string -> ?block:string -> string -> int t + +(** RPC: [GET /chains//blocks//context/contracts//unstaked_frozen_balance] + + [chain] defaults to ["main"]. + [block] defaults to ["head"]. +*) +val get_chain_block_context_contract_unstaked_frozen_balance : + ?chain:string -> ?block:string -> string -> int t + (** RPC: [GET /chains//blocks//helpers/baking_rights] [chain] defaults to ["main"]. @@ -1006,6 +1045,13 @@ val get_chain_block_context_delegates : val get_chain_block_context_delegate : ?chain:string -> ?block:string -> string -> JSON.t t +(** RPC: [GET /chains//blocks//context/delegates//active_staking_parameters] + + [chain] defaults to ["main"]. + [block] defaults to ["head"]. *) +val get_chain_block_context_delegate_active_staking_parameters : + ?chain:string -> ?block:string -> string -> JSON.t t + (** RPC: [GET /chains//blocks//context/delegates//current_frozen_deposits] [chain] defaults to ["main"]. @@ -1030,7 +1076,8 @@ val get_chain_block_context_delegate_frozen_balance : (** RPC: [GET /chains//blocks//context/delegates//frozen_balance_by_cycle] [chain] defaults to ["main"]. - [block] defaults to ["head"]. *) + [block] defaults to ["head"]. +*) val get_chain_block_context_delegate_frozen_balance_by_cycle : ?chain:string -> ?block:string -> string -> JSON.t t @@ -1182,7 +1229,6 @@ val get_chain_block_context_adaptive_issuance_launch_cycle : ?chain:string -> ?block:string -> unit -> JSON.t t (** RPC: [GET /chains//blocks//context/issuance/expected_issuance] - [chain] defaults to ["main"]. [block] defaults to ["head"]. *) val get_chain_block_context_issuance_expected_issuance : diff --git a/tezt/lib_tezos/client.ml b/tezt/lib_tezos/client.ml index 1cdee37014c6..dab686125f74 100644 --- a/tezt/lib_tezos/client.ml +++ b/tezt/lib_tezos/client.ml @@ -1370,12 +1370,16 @@ let spawn_submit_ballot ?(key = Constant.bootstrap1.alias) ?(wait = "none") let submit_ballot ?key ?wait ~proto_hash vote client = spawn_submit_ballot ?key ?wait ~proto_hash vote client |> Process.check -let set_deposits_limit ?hooks ?endpoint ?(wait = "none") ~src ~limit client = +let spawn_set_deposits_limit ?hooks ?endpoint ?(wait = "none") ~src ~limit + client = spawn_command ?hooks ?endpoint client (["--wait"; wait] @ ["set"; "deposits"; "limit"; "for"; src; "to"; limit]) + +let set_deposits_limit ?hooks ?endpoint ?wait ~src ~limit client = + spawn_set_deposits_limit ?hooks ?endpoint ?wait ~src ~limit client |> Process.check_and_read_stdout let unset_deposits_limit ?hooks ?endpoint ?(wait = "none") ~src client = @@ -4240,29 +4244,54 @@ let as_foreign_endpoint = function port = Proxy_server.rpc_port ps; } -let spawn_stake ?(wait = "none") amount dlgt client = +let get_receipt_for ~operation ?(check_previous = 10) client = spawn_command client - @@ ["--wait"; wait; "stake"; Tez.to_string amount; "for"; dlgt] + @@ [ + "get"; + "receipt"; + "for"; + operation; + "--check-previous"; + string_of_int check_previous; + ] + |> Process.check_and_read_stdout -let stake ?wait amount dlgt client = - spawn_stake ?wait amount dlgt client |> Process.check +let spawn_stake ?(wait = "none") amount ~staker client = + spawn_command client + @@ ["--wait"; wait; "stake"; Tez.to_string amount; "for"; staker] -let spawn_set_delegate_parameters dlgt limit edge client = +let stake ?wait amount ~staker client = + spawn_stake ?wait amount ~staker client |> Process.check + +let spawn_unstake ?(wait = "none") amount ~staker client = + spawn_command client + @@ ["--wait"; wait; "unstake"; Tez.to_string amount; "for"; staker] + +let unstake ?wait amount ~staker client = + spawn_unstake ?wait amount ~staker client |> Process.check + +let spawn_finalize_unstake ?(wait = "none") ~staker client = + spawn_command client @@ ["--wait"; wait; "finalize"; "unstake"; "for"; staker] + +let finalize_unstake ?wait ~staker client = + spawn_finalize_unstake ?wait ~staker client |> Process.check + +let spawn_set_delegate_parameters ~delegate ~limit ~edge client = spawn_command client @@ [ "set"; "delegate"; "parameters"; "for"; - dlgt; + delegate; "--limit-of-staking-over-baking"; limit; "--edge-of-baking-over-staking"; edge; ] -let set_delegate_parameters dlgt limit edge client = - spawn_set_delegate_parameters dlgt limit edge client |> Process.check +let set_delegate_parameters ~delegate ~limit ~edge client = + spawn_set_delegate_parameters ~delegate ~limit ~edge client |> Process.check module RPC = struct let call_raw ?log_command ?log_status_on_exit ?log_output ?better_errors diff --git a/tezt/lib_tezos/client.mli b/tezt/lib_tezos/client.mli index a6d7e2577844..0ab587023cb5 100644 --- a/tezt/lib_tezos/client.mli +++ b/tezt/lib_tezos/client.mli @@ -1069,6 +1069,16 @@ val set_deposits_limit : t -> string Lwt.t +(* Same as [set_deposits_limit], but do not wait for the process to exit. *) +val spawn_set_deposits_limit : + ?hooks:Process.hooks -> + ?endpoint:endpoint -> + ?wait:string -> + src:string -> + limit:string -> + t -> + Process.t + (** Run [octez-client unset deposits limit for ]. *) val unset_deposits_limit : ?hooks:Process.hooks -> @@ -3049,17 +3059,35 @@ val publish_dal_commitment : endpoint. *) val as_foreign_endpoint : endpoint -> Endpoint.t +(** Run [octez-client get receipt for --check-previous ]. *) +val get_receipt_for : + operation:string -> ?check_previous:int -> t -> string Lwt.t + (** Run [octez-client stake for ]. *) -val stake : ?wait:string -> Tez.t -> string -> t -> unit Lwt.t +val stake : ?wait:string -> Tez.t -> staker:string -> t -> unit Lwt.t (** Same as [stake], but do not wait for the process to exit. *) -val spawn_stake : ?wait:string -> Tez.t -> string -> t -> Process.t +val spawn_stake : ?wait:string -> Tez.t -> staker:string -> t -> Process.t + +(** Run [octez-client unstake for ]. *) +val unstake : ?wait:string -> Tez.t -> staker:string -> t -> unit Lwt.t + +(** Same as [unstake], but do not wait for the process to exit. *) +val spawn_unstake : ?wait:string -> Tez.t -> staker:string -> t -> Process.t + +(** Run [octez-client finalize_unstake for ]. *) +val finalize_unstake : ?wait:string -> staker:string -> t -> unit Lwt.t + +(** Same as [finalize_unstake], but do not wait for the process to exit. *) +val spawn_finalize_unstake : ?wait:string -> staker:string -> t -> Process.t -(** Run [octez-client set delegate parameters for --limit-of-staking-over-baking --edge-of-baking-over-staking ]. *) -val set_delegate_parameters : string -> string -> string -> t -> unit Lwt.t +(** Run [octez-client set delegate parameters for --limit-of-staking-over-baking --edge-of-baking-over-staking ]. *) +val set_delegate_parameters : + delegate:string -> limit:string -> edge:string -> t -> unit Lwt.t (** Same as [set_delegate_parameters], but do not wait for the process to exit. *) -val spawn_set_delegate_parameters : string -> string -> string -> t -> Process.t +val spawn_set_delegate_parameters : + delegate:string -> limit:string -> edge:string -> t -> Process.t module RPC : sig (** Perform RPC calls using [octez-client]. *) -- GitLab From c7bb3771775ac60add8543e123eba1b0995ddeec Mon Sep 17 00:00:00 2001 From: Mathias Bourgoin Date: Thu, 18 Jan 2024 09:51:20 +0100 Subject: [PATCH 2/3] Tezt/Tezos: Add helpers to manipulate operation receipts --- tezt/lib_tezos/operation_receipt.ml | 111 ++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 tezt/lib_tezos/operation_receipt.ml diff --git a/tezt/lib_tezos/operation_receipt.ml b/tezt/lib_tezos/operation_receipt.ml new file mode 100644 index 000000000000..b256bf25f1e8 --- /dev/null +++ b/tezt/lib_tezos/operation_receipt.ml @@ -0,0 +1,111 @@ +(*****************************************************************************) +(* *) +(* SPDX-License-Identifier: MIT *) +(* Copyright (c) 2024 Nomadic Labs, *) +(* *) +(*****************************************************************************) + +(* Helper functions to handle operation results *) + +(** Returns the content of the "operation_result" field from the metadata of the + operation with hash [operation] in the [check_previous] blocks before the + current head using the 'get_chain_block_operations' RPC. +*) +let get_result_for operation ?(check_previous = 10) client = + let rec aux i = + if i >= check_previous then failwith "Operation not found" + else + let block = Format.sprintf "head~%d" i in + let* operations = + Client.RPC.call client + @@ RPC.get_chain_block_operations_validation_pass + ~block + ~force_metadata:true + ~validation_pass:3 + () + in + let manager_ops = JSON.as_list operations in + match + List.find_opt + (fun op -> JSON.(op |-> "hash" |> as_string = operation)) + manager_ops + with + | Some op -> + let operation_contents = JSON.(op |-> "contents" |> as_list) in + Log.info "Content of size %d" (List.length operation_contents) ; + let metadata = + List.map (fun op -> JSON.(op |-> "metadata")) operation_contents + in + return + @@ List.map (fun json -> JSON.(json |-> "operation_result")) metadata + | None -> aux (i + 1) + in + aux 0 + +let get_block_metadata client = + Client.RPC.call client @@ RPC.get_chain_block_metadata_raw () + +module Balance_updates = struct + type staker = + | Delegate of {delegate : string; contract : string option} + | Baker of {baker : string} + + type t = { + kind : string; + contract : string option; + change : int; + staker : staker option; + category : string option; + delayed_operation_hash : string option; + origin : string; + } + + let from_result operation_results = + let bu = + List.flatten + @@ List.map + (fun op -> + Log.info "result: %s" (JSON.encode op) ; + JSON.(op |-> "balance_updates" |> as_list)) + operation_results + in + return + @@ List.map + (fun json -> + let kind = JSON.(json |-> "kind" |> as_string) in + let category = JSON.(json |-> "category" |> as_string_opt) in + let contract = JSON.(json |-> "contract" |> as_string_opt) in + let change = JSON.(json |-> "change" |> as_int) in + let staker = + match JSON.(json |-> "staker" |> as_opt) with + | Some json -> ( + match JSON.(json |-> "delegate" |> as_string_opt) with + | Some delegate -> + Some + (Delegate + { + delegate; + contract = + JSON.(json |-> "contract" |> as_string_opt); + }) + | None -> ( + match JSON.(json |-> "baker" |> as_string_opt) with + | Some baker -> Some (Baker {baker}) + | None -> None)) + | None -> None + in + let delayed_operation_hash = + JSON.(json |-> "delayed_operation_hash" |> as_string_opt) + in + let origin = JSON.(json |-> "origin" |> as_string) in + { + kind; + contract; + change; + staker; + category; + delayed_operation_hash; + origin; + }) + bu +end -- GitLab From 761ab2a93f83a1c6f7573f39cee94a4de761b5d0 Mon Sep 17 00:00:00 2001 From: Mathias Bourgoin Date: Thu, 18 Jan 2024 09:51:39 +0100 Subject: [PATCH 3/3] Test/Tezt: Add UX test for AI --- tezt/tests/adaptive_issuance.ml | 1013 ++++++++++++++++++++++++++++++- 1 file changed, 996 insertions(+), 17 deletions(-) diff --git a/tezt/tests/adaptive_issuance.ml b/tezt/tests/adaptive_issuance.ml index 1b95d685c43a..7d026d4e730d 100644 --- a/tezt/tests/adaptive_issuance.ml +++ b/tezt/tests/adaptive_issuance.ml @@ -52,25 +52,78 @@ module Helpers = struct ~error_msg:"expected current_period = %R, got %L" ; unit - let exec_n_cycles f ?keys n client = + let bake_n_cycles bake ?keys n client = let* current_level = get_current_level client in let current_level = current_level.level in let nb_baked_blocks_in_cycle = current_level mod blocks_per_cycle in let nb_blocks_to_bake = (n * blocks_per_cycle) - nb_baked_blocks_in_cycle in - let rec loop n = - if n = 0 then unit - else - let* () = f (nb_blocks_to_bake - n) ?keys client in - loop (n - 1) - in Log.info "Bake %d cycle(s) (from level %d to %d)" n current_level (current_level + nb_blocks_to_bake) ; - loop nb_blocks_to_bake + repeat nb_blocks_to_bake (fun () -> bake ?keys client) end +let log_step counter msg = + let color = Log.Color.(bold ++ FG.blue) in + let prefix = "step" ^ string_of_int counter in + Log.info ~color ~prefix msg + +(* Matches events where the message is of the form: + "double baking evidence injected ". + For example: + + "event": { + "double_baking_denounced.v0": { + "hash": "onkfjSun49iRrGtuN9FwtiCqDAEgzPKzg1BSa7BSHnaAkButUxx", + "bytes": "..." + } + } +*) +let wait_for_denunciation accuser = + let filter json = JSON.(json |-> "hash" |> as_string_opt) in + Accuser.wait_for accuser "double_baking_denounced.v0" filter + +(* Matches events which contain an injection request. + For example: + + "event": { + "node_prevalidator.v0": [ + "2020-09-11T12:32:05.353-00:00", + { + "event": { + "request": { + "request": "inject", + "operation": { + "branch": "BM3J62AvjnjJKfinoq1op2uw5Hdn3YGMQmusnLdrfCd1yrpftG2", + "data": "030000...00000" + } + }, + "status": { + "pushed": "2020-09-11T12:32:05.343-00:00", + "treated": 4.5947e-05, + "completed": 0.009614550999999999 + } + }, + "level": "info" + } + ] + } +*) +let wait_for_denunciation_injection node client accuser = + let filter json = + match JSON.(json |-> "view" |-> "request" |> as_string_opt) with + | Some s when s = "inject" -> Some s + | Some _ | None -> None + in + let denunciation_event = wait_for_denunciation accuser in + let* _ = Node.wait_for node "request_completed_info.v0" filter in + let* oph = denunciation_event in + let* mempool = Mempool.get_mempool client in + if List.mem oph mempool.validated then return oph + else Test.fail "the denunciation operation was rejected by the mempool" + let default_overrides = [ (* Shorter cycles *) @@ -117,20 +170,21 @@ let activate_ai protocol sandbox_client sandbox_endpoint = in assert (JSON.is_null launch_cycle) ; (* Make delegate vote for AI activation*) - let bake _i ?keys client = + let bake ?keys client = Client.bake_for ~endpoint:sandbox_endpoint + ~minimal_timestamp:true ~protocol ?keys client ~ai_vote:On in - (* The vote should have pass during the first cycle *) + (* The vote should have passed during the first cycle *) let* () = - Helpers.exec_n_cycles + Helpers.bake_n_cycles bake 1 - ~keys:(List.map (fun x -> x.Account.alias) bootstrap_accounts) + ~keys:[Constant.bootstrap2.alias] sandbox_client in @@ -142,7 +196,12 @@ let activate_ai protocol sandbox_client sandbox_endpoint = Log.info "AI will activate in cycle %d" (JSON.as_int launch_cycle) ; (* Wait for to have AI fully activated *) - Helpers.exec_n_cycles bake (JSON.as_int launch_cycle) sandbox_client + let* current_level = Helpers.get_current_level sandbox_client in + + Helpers.bake_n_cycles + bake + (JSON.as_int launch_cycle - current_level.cycle) + sandbox_client (** This test starts from a protocol with AI feature flag enabled. It tests the correct activation of AI features behind the @@ -151,10 +210,63 @@ let test_AI_activation = Protocol.register_test ~__FILE__ ~title:"AI Activation - test AI activation after feature vote" - ~tags:["adaptive_issuance"] + ~tags:["adaptive_issuance"; "staking"] @@ fun protocol -> let* _proto_hash, endpoint, client, _node = init protocol in - let stake = Client.spawn_stake ~wait:"1" (Tez.of_int 1) "bootstrap2" client in + + let* staking_parameters = + Client.RPC.call client + @@ RPC.get_chain_block_context_delegate_active_staking_parameters + Constant.bootstrap3.public_key_hash + in + let limit_before = + JSON.( + staking_parameters |-> "limit_of_staking_over_baking_millionth" |> as_int) + in + let edge_before = + JSON.( + staking_parameters |-> "edge_of_baking_over_staking_billionth" |> as_int) + in + + assert (limit_before = 0 && edge_before = 1000000000) ; + + log_step 0 "Update staking parameters prior to AI activation" ; + (* set bootstrap3 parameters to accept stakers *) + let set_delegate_parameters = + Client.spawn_set_delegate_parameters + ~delegate:"bootstrap3" + ~limit:"5" + ~edge:"0.5" + client + in + + log_step 1 "Bake 3 cycles for new parameters to be taken into account" ; + let bake ?keys client = + Client.bake_for_and_wait ~endpoint ~protocol ?keys client + in + let* () = Helpers.bake_n_cycles bake 3 client in + let* () = set_delegate_parameters |> Process.check in + + log_step 2 "Check new staking parameters" ; + let* staking_parameters = + Client.RPC.call client + @@ RPC.get_chain_block_context_delegate_active_staking_parameters + Constant.bootstrap3.public_key_hash + in + let limit_after = + JSON.( + staking_parameters |-> "limit_of_staking_over_baking_millionth" |> as_int) + in + let edge_after = + JSON.( + staking_parameters |-> "edge_of_baking_over_staking_billionth" |> as_int) + in + + assert (limit_after = 5000000 && edge_after = 500000000) ; + log_step 3 "Check staking is not allowed before AI activation" ; + let stake = + Client.spawn_stake ~wait:"1" (Tez.of_int 1) ~staker:"bootstrap2" client + in let* () = Process.check_error ~msg: @@ -164,10 +276,12 @@ let test_AI_activation = stake in + log_step 4 "Activate AI" ; let* _ = activate_ai protocol client endpoint in + log_step 5 "Check staking is now possible" ; (* Make sure AI is activated by trying to explictly stake with one delegate *) - let stake = Client.spawn_stake (Tez.of_int 1) "bootstrap2" client in + let stake = Client.spawn_stake (Tez.of_int 1) ~staker:"bootstrap2" client in let* () = Client.bake_for_and_wait ~endpoint @@ -179,4 +293,869 @@ let test_AI_activation = let* () = Process.check stake in Lwt.return_unit -let register ~protocols = test_AI_activation protocols +let get_hash_of operation = + let* stdout = Process.check_and_read_stdout operation in + Log.info "%s" stdout ; + match stdout =~* rex "Operation hash is '?(o\\w{50})'" with + | None -> + Test.fail "Cannot extract operation hash from client_output: %s" stdout + | Some hash -> return hash + +let bake_n ~endpoint ~protocol client i = + repeat i (fun () -> + Client.bake_for_and_wait + ~endpoint + ~protocol + ~minimal_timestamp:true + ~keys:[Constant.bootstrap2.alias] + client + ~ai_vote:On) + +let check_balance_updates balance_updates predicates = + List.iter + (fun (pred, msg) -> + if not (List.exists pred balance_updates) then + Test.fail "Inconsistant balance update: %s" msg) + predicates + +let check_balance_updates_for operation_hash predicates client = + let* receipt = Operation_receipt.get_result_for operation_hash client in + let* balance_updates = + Operation_receipt.Balance_updates.from_result receipt + in + check_balance_updates balance_updates predicates ; + Lwt.return_unit + +(* This scenario tests the overall staking mechanism introduced with adaptive issuance: + - staking with a delegate + - staking with stakers + - unstaking + - receiving staking rewards + - slashing (because of double bake) +*) +let test_staking = + Protocol.register_test + ~__FILE__ + ~title: + "Staking - test staking with delegate and staker in a simple scenario" + ~tags: + [ + "adaptive_issuance"; + "staking"; + "double"; + "baking"; + "accuser"; + "node"; + "rewards"; + "slashing"; + ] + ~uses:(fun protocol -> [Protocol.accuser protocol]) + @@ fun protocol -> + let* _proto_hash, endpoint, client_1, node_1 = init protocol in + + log_step 0 "Check staking is not allowed before AI activation" ; + Log.info "Staking should fail before AI activation" ; + let stake = + Client.spawn_stake ~wait:"1" (Tez.of_int 1) ~staker:"bootstrap2" client_1 + in + let* () = + Process.check_error + ~msg: + (rex + "Manual staking operations are forbidden because staking is \ + currently automated.") + stake + in + + Log.info "Starting second node" ; + let* node_2 = Node.init [Synchronisation_threshold 0; Private_mode] in + let* () = Node.wait_for_ready node_2 in + let* client_2 = Client.init ~endpoint:(Node node_2) () in + + let* () = Client.Admin.trust_address client_1 ~peer:node_2 + and* () = Client.Admin.trust_address client_2 ~peer:node_1 in + let* () = Client.Admin.connect_address client_1 ~peer:node_2 in + + let stake_amount = Tez.of_int 600 in + let delegate = "bootstrap2" in + + log_step 1 "Activate AI" ; + let* _ = activate_ai protocol client_1 endpoint in + + log_step 2 "Create two stakers accounts" ; + let* staker0 = Client.gen_and_show_keys client_1 in + let* staker1 = Client.gen_and_show_keys client_1 in + + let transfer_to_staker0 = + Client.spawn_transfer + ~burn_cap:Tez.one + ~amount:(Tez.of_int 1000000) + ~giver:Constant.bootstrap1.alias + ~receiver:staker0.alias + client_1 + in + let transfer_to_staker1 = + Client.spawn_transfer + ~burn_cap:Tez.one + ~amount:(Tez.of_int 1000000) + ~giver:Constant.bootstrap3.alias + ~receiver:staker1.alias + client_1 + in + let* () = bake_n ~endpoint ~protocol client_1 1 in + let* () = transfer_to_staker0 |> Process.check in + let* () = transfer_to_staker1 |> Process.check in + let* () = bake_n ~endpoint ~protocol client_1 1 in + + (* check staker0 cannot stake *) + (* staker need a delegate to stake *) + log_step 3 "Check staker0 cannot (un)stake as its delegate is not set" ; + let stake = Client.spawn_stake Tez.one ~staker:staker0.alias client_1 in + let* () = + Process.check_error + ~msg:(rex ".*Stake operations are only allowed when delegate is set.") + stake + in + let unstake = Client.spawn_unstake Tez.one ~staker:staker0.alias client_1 in + let* () = + Process.check_error + ~msg:(rex ".*Stake operations are only allowed when delegate is set.") + unstake + in + log_step 4 "Set delegate for stakers" ; + let*! () = + Client.set_delegate ~src:staker0.alias ~delegate:"bootstrap2" client_1 + in + let*! () = + Client.set_delegate ~src:staker1.alias ~delegate:"bootstrap2" client_1 + in + let* () = bake_n ~endpoint ~protocol client_1 1 in + + (* delegate must accept staking *) + log_step + 5 + "Check staker0 cannot stake as its delegate does not accept staking" ; + let stake = Client.spawn_stake Tez.one ~staker:staker0.alias client_1 in + let* () = + Process.check_error + ~msg: + (rex + "The delegate currently does not accept staking operations from \ + sources other than itself: its `limit_of_staking_over_baking` \ + parameter is set to 0.") + stake + in + + log_step 6 "Changing delegate parameters to accept staking" ; + + (* set bootstrap2 parameters to accept stakers *) + let set_delegate_parameters = + Client.spawn_set_delegate_parameters + ~delegate:"bootstrap2" + ~limit:"5" + ~edge:"0.5" + client_1 + in + + log_step + 7 + "Check balances while waiting for parameters to be taken into account" ; + let* () = bake_n ~endpoint ~protocol client_1 2 in + let* () = set_delegate_parameters |> Process.check in + + let* balance = Client.get_balance_for ~account:delegate client_1 in + + let stake_error_balance_too_low = + Client.spawn_stake balance ~staker:delegate client_1 + in + let* () = + Process.check_error + ~msg:(rex "Balance of contract .* too low (.*) to spend .*") + stake_error_balance_too_low + in + + let stake_error_negative = + Client.spawn_stake (Tez.of_int (-1)) ~staker:delegate client_1 + in + let* () = Process.check_error stake_error_negative in + + let stake_error_negative = + Client.spawn_stake (Tez.of_int 0) ~staker:delegate client_1 + in + let* () = + Process.check_error + ~msg: + (rex + "Transactions of 0ęś© towards a contract without code are forbidden \ + (.*).") + stake_error_negative + in + + let stake = Client.spawn_stake stake_amount ~staker:delegate client_1 in + let* () = bake_n ~endpoint ~protocol client_1 2 in + let* operation_hash = get_hash_of stake in + Log.info "Stake operation hash: %s" operation_hash ; + + log_step 8 "Check balance updates and unstake requests" ; + let* () = + check_balance_updates_for + operation_hash + [ + ( (fun opr -> + opr.kind = "freezer" && opr.change = 600000000 + && opr.staker + = Some (Baker {baker = Constant.bootstrap2.public_key_hash})), + " Frozen balance deposit of 600tez" ); + ] + client_1 + in + let unstake = + Client.spawn_unstake (Tez.of_int 200) ~staker:delegate client_1 + in + let* () = bake_n ~endpoint ~protocol client_1 2 in + let* operation_hash = get_hash_of unstake in + Log.info "Unstake operation hash: %s" operation_hash ; + let* () = + check_balance_updates_for + operation_hash + [ + ( (fun opr -> + opr.kind = "freezer" + && opr.category = Some "deposits" + && opr.change = -200000000 + && opr.staker + = Some (Baker {baker = Constant.bootstrap2.public_key_hash})), + "Frozen deposits decreased by 200tez" ); + ( (fun opr -> + opr.kind = "freezer" + && opr.category = Some "unstaked_deposits" + && opr.change = 200000000 + && opr.staker + = Some + (Delegate + { + delegate = Constant.bootstrap2.public_key_hash; + contract = Some Constant.bootstrap2.public_key_hash; + })), + "Unstaked frozen increased by 200tez" ); + ] + client_1 + in + let* unstake_requests = + Client.RPC.call client_1 + @@ RPC.get_chain_block_context_contract_unstake_requests + Constant.bootstrap2.public_key_hash + in + let finalizable = JSON.(unstake_requests |-> "finalizable" |> as_list) in + let unfinalizable = + JSON.(unstake_requests |-> "unfinalizable" |-> "requests" |> as_list) + in + assert (List.length finalizable == 0) ; + assert (List.length unfinalizable > 0) ; + + let* unstaked_frozen_balance = + Client.RPC.call client_1 + @@ RPC.get_chain_block_context_contract_unstaked_frozen_balance + Constant.bootstrap2.public_key_hash + in + assert (unstaked_frozen_balance = 200000000) ; + + let* unstaked_finalizable_balance = + Client.RPC.call client_1 + @@ RPC.get_chain_block_context_contract_unstaked_finalizable_balance + Constant.bootstrap2.public_key_hash + in + assert (unstaked_finalizable_balance = 0) ; + let stake = Client.spawn_stake (Tez.of_int 600) ~staker:delegate client_1 in + let* () = bake_n ~endpoint ~protocol client_1 2 in + let* operation_hash = get_hash_of stake in + let* () = + check_balance_updates_for + operation_hash + [ + ( (fun opr -> + opr.kind = "freezer" + && opr.category = Some "unstaked_deposits" + && opr.change = -200000000 + && opr.staker + = Some + (Delegate + { + delegate = Constant.bootstrap2.public_key_hash; + contract = Some Constant.bootstrap2.public_key_hash; + })), + "Pending unstaked deposit decreased by 200tez" ); + ( (fun opr -> + opr.kind = "freezer" && opr.change = 400000000 + && opr.staker + = Some (Baker {baker = Constant.bootstrap2.public_key_hash})), + " Frozen balance deposit of 400tez" ); + ] + client_1 + in + + log_step 9 "Check set_deposit_limits is not allowed after AI activation" ; + let set_deposits_limit = + Client.spawn_set_deposits_limit + ~src:Constant.bootstrap1.alias + ~limit:"0" + client_1 + in + + (* lets bake 2 more blocks and delegate should accept staking *) + let* () = bake_n ~endpoint ~protocol client_1 2 in + + (* TODO https://gitlab.com/tezos/tezos/-/issues/6613 + set_deposit_limits should fail after AI activation *) + let* () = Process.check set_deposits_limit in + + let* numerator = + Client.RPC.call client_1 + @@ RPC.get_chain_block_context_contract_staking_numerator + Constant.bootstrap2.public_key_hash + in + let* denominator = + Client.RPC.call client_1 + @@ RPC.get_chain_block_context_contract_staking_numerator + Constant.bootstrap2.public_key_hash + in + Log.info "Numerator/denominator before: %d/%d " numerator denominator ; + + let bake ?keys client = + Client.bake_for ~endpoint ~minimal_timestamp:true ~protocol ?keys client + in + let* () = Helpers.bake_n_cycles bake 1 client_1 in + + let stake0 = + Client.spawn_stake (Tez.of_int 2000) ~staker:staker0.alias client_1 + in + let stake1 = + Client.spawn_stake (Tez.of_int 1000) ~staker:staker1.alias client_1 + in + let* () = bake_n ~endpoint ~protocol client_1 1 in + let* () = Process.check ~expect_failure:false stake0 in + let* () = Process.check ~expect_failure:false stake1 in + + let* () = Helpers.bake_n_cycles bake 3 client_1 in + + let* denominator = + Client.RPC.call client_1 + @@ RPC.get_chain_block_context_delegate_staking_denominator + Constant.bootstrap2.public_key_hash + in + + let* numerator0 = + Client.RPC.call client_1 + @@ RPC.get_chain_block_context_contract_staking_numerator + staker0.public_key_hash + in + Log.info + "Numerator/denominator after for %s: %d/%d " + staker0.alias + numerator0 + (JSON.as_int denominator) ; + + let* numerator1 = + Client.RPC.call client_1 + @@ RPC.get_chain_block_context_contract_staking_numerator + staker1.public_key_hash + in + + assert (numerator0 + numerator1 = JSON.as_int denominator) ; + + Log.info + "Numerator/denominator after for %s: %d/%d " + staker1.alias + numerator1 + (JSON.as_int denominator) ; + + log_step 10 "Unstake with staker 0" ; + let unstake0 = + Client.spawn_unstake (Tez.of_int 1000) ~staker:staker0.alias client_1 + in + let* () = bake_n ~endpoint ~protocol client_1 2 in + let* () = Process.check ~expect_failure:false unstake0 in + + log_step 11 "Check reward increase with each blocks" ; + let check_and_return_balances ?check contract = + let open Account in + let* balance = Client.get_balance_for ~account:contract.alias client_1 in + + let* staked_balance = + Client.RPC.call client_1 + @@ RPC.get_chain_block_context_contract_staked_balance + contract.public_key_hash + in + Log.info + "Balance of %s: spendable : %s, staked_balance : %d" + contract.alias + (Tez.to_string balance) + staked_balance ; + (match check with + | Some (pred_balance, pred_staked_balance) -> + assert (pred_balance <= balance) ; + assert (pred_staked_balance < staked_balance) + | None -> ()) ; + Lwt.return (balance, staked_balance) + in + let* balances0 = check_and_return_balances staker0 in + let* balances1 = check_and_return_balances staker1 in + let* balances_dlgt = check_and_return_balances Constant.bootstrap2 in + let balances0 = ref balances0 in + let balances1 = ref balances1 in + let balances_dlgt = ref balances_dlgt in + let* () = bake_n ~endpoint ~protocol client_1 1 in + + let* () = + repeat 7 (fun () -> + let* () = bake_n ~endpoint ~protocol client_1 1 in + let* b0 = check_and_return_balances ~check:!balances0 staker0 in + let* b1 = check_and_return_balances ~check:!balances1 staker1 in + let* b_dlgt = + check_and_return_balances ~check:!balances_dlgt Constant.bootstrap2 + in + balances0 := b0 ; + balances1 := b1 ; + balances_dlgt := b_dlgt ; + let* bu = Operation_receipt.get_block_metadata client_1 in + let* bu = Operation_receipt.Balance_updates.from_result [bu] in + + let amount_baker_share = 786 in + let amount_delegation = 7413 in + let amount_edge = 4 in + let amount_stakers = 3 in + (* check rewards *) + check_balance_updates + bu + [ + ( (fun opr -> + opr.kind = "minted" + && opr.category = Some "baking rewards" + && opr.change = -amount_baker_share), + "Minting baker share" ); + ( (fun opr -> + opr.kind = "freezer" + && opr.category = Some "deposits" + && opr.change = amount_baker_share + && opr.staker + = Some (Baker {baker = Constant.bootstrap2.public_key_hash})), + "Baker's frozen deposits increased by baker share" ); + ( (fun opr -> + opr.kind = "minted" + && opr.category = Some "baking rewards" + && opr.change = -amount_delegation), + "Minting from staking rights" ); + ( (fun opr -> + opr.kind = "contract" + && opr.contract = Some Constant.bootstrap2.public_key_hash + && opr.change = amount_delegation), + "Delegate's spendable balance increased" ); + ( (fun opr -> + opr.kind = "minted" + && opr.category = Some "baking rewards" + && opr.change = -amount_edge), + "Baker's edge on staker rewards" ); + ( (fun opr -> + opr.kind = "freezer" + && opr.category = Some "deposits" + && opr.change = amount_edge + && opr.staker + = Some (Baker {baker = Constant.bootstrap2.public_key_hash})), + "Baker's frozen deposits increased by its edge on staker rewards" + ); + ( (fun opr -> + opr.kind = "minted" + && opr.category = Some "baking rewards" + && opr.change = -amount_stakers), + "Minting staker rewards" ); + ( (fun opr -> + opr.kind = "freezer" + && opr.category = Some "deposits" + && opr.change = amount_stakers + && opr.staker + = Some + (Delegate + { + delegate = Constant.bootstrap2.public_key_hash; + contract = None; + })), + "Delegates frozen deposits increased by staker rewards" ); + ] ; + + Lwt.return_unit) + in + let* current_level = Helpers.get_current_level client_1 in + + (* unstake all *) + log_step 12 "Unstake all with staker 0" ; + let unstake0 = + Client.spawn_unstake (Tez.of_int 500000) ~staker:staker0.alias client_1 + in + + let* _ = Helpers.bake_n_cycles bake (current_level.cycle - 13) client_1 in + + let* () = Process.check ~expect_failure:false unstake0 in + + let is_operation_in_operations ops oph = + let open JSON in + let ops_list = ops |=> 2 |> as_list in + List.exists (fun e -> e |-> "hash" |> as_string = oph) ops_list + in + + (* Steps 13 to 20 are largely duplicated in [remote_tests/double_baking.ml] + and [double_bake.ml]. Any modification to this test should be reported there + too. *) + log_step 13 "Start setup for double baking" ; + let* current_level = Helpers.get_current_level client_1 in + + let common_ancestor = current_level.level in + let base_branch_size = 1 in + let node_2_branch_size = base_branch_size + 1 in + let node_1_branch_size = node_2_branch_size + 1 in + let node_3_first_catch_up_level = common_ancestor + node_2_branch_size in + let node_3_second_catch_up_level = common_ancestor + node_1_branch_size in + let node_3_final_level = node_3_second_catch_up_level + 1 in + + let* () = Client.Admin.trust_address client_1 ~peer:node_2 + and* () = Client.Admin.trust_address client_2 ~peer:node_1 in + let* () = Client.Admin.connect_address client_1 ~peer:node_2 in + let* _ = Node.wait_for_level node_1 common_ancestor + and* _ = Node.wait_for_level node_2 common_ancestor in + let* () = Node.terminate node_2 in + + log_step + 14 + "Bake %d blocks on Node 1 and terminate Node 1." + node_1_branch_size ; + (* Craft a branch from [common_ancestor] of size + [node_1_branch_size], baked by bootstrap1. *) + let* () = + (* Base branch is baked by bootstrap1. *) + repeat base_branch_size (fun () -> + Client.bake_for_and_wait + ~keys:[Constant.bootstrap2.public_key_hash] + client_1) + in + + (* Two other bake to make this branch longer than Node 2's one. *) + let* () = + repeat 2 (fun () -> + Client.bake_for_and_wait + ~keys:[Constant.bootstrap2.public_key_hash] + client_1) + in + + let* _ = Node.wait_for_level node_1 (common_ancestor + node_1_branch_size) in + let* () = Node.terminate node_1 in + + log_step 15 "Run Node 2 and bake %d blocks" node_2_branch_size ; + let* () = Node.run node_2 [Synchronisation_threshold 0; Private_mode] in + let* () = Node.wait_for_ready node_2 in + + (* Craft a branch from [common_ancestor] of size + [node_2_branch_size], the first block is baked by bootstrap2. *) + let* () = + (* Base branch is baked by bootstrap2. *) + repeat base_branch_size (fun () -> + Client.bake_for_and_wait + ~keys:[Constant.bootstrap1.public_key_hash] + client_2) + in + + (* The second block is baked by bootstrap2 to simulate a + double bake. *) + let* () = + Client.bake_for_and_wait + ~keys:[Constant.bootstrap2.public_key_hash] + client_2 + in + + let* _ = Node.wait_for_level node_2 (common_ancestor + node_2_branch_size) in + + log_step 16 "Run Node 3, bake one block and wait for the accuser to be ready." ; + let* node_3 = Node.init [Synchronisation_threshold 0; Private_mode] in + let* client_3 = Client.init ~endpoint:(Node node_3) () in + let* accuser_3 = Accuser.init ~protocol node_3 in + let denunciation_injection = + wait_for_denunciation_injection node_3 client_3 accuser_3 + in + + log_step 17 "Connect Node 3 with Node 2 and wait for Node 3 to catch up." ; + let* () = Client.Admin.trust_address client_2 ~peer:node_3 + and* () = Client.Admin.trust_address client_3 ~peer:node_2 in + let* () = Client.Admin.connect_address client_2 ~peer:node_3 in + let* _ = Node.wait_for_level node_3 node_3_first_catch_up_level in + + log_step 18 "Run and connect Node 1 to Node 3. Wait for Node 3 to catch up." ; + let* () = Node.run node_1 [Synchronisation_threshold 0; Private_mode] in + let* () = Node.wait_for_ready node_1 in + let* () = Client.Admin.trust_address client_1 ~peer:node_3 + and* () = Client.Admin.trust_address client_3 ~peer:node_1 in + let* () = Client.Admin.connect_address client_1 ~peer:node_3 in + let* _ = Node.wait_for_level node_3 node_3_second_catch_up_level in + (* Ensure that the denunciation is in node_3's mempool. *) + let* denunciation_oph = denunciation_injection in + + log_step 19 "Bake a block on Node 3 and wait for everybody to catch up." ; + let* () = + Client.bake_for_and_wait + ~keys:[Constant.bootstrap1.public_key_hash] + client_3 + in + let* _ = Node.wait_for_level node_1 node_3_final_level + and* _ = Node.wait_for_level node_2 node_3_final_level + and* _ = Node.wait_for_level node_3 node_3_final_level in + + log_step 20 "Check denunciation is in the last block." ; + (* Getting the operations of the current head. *) + let* ops = Client.RPC.call client_1 @@ RPC.get_chain_block_operations () in + let* () = Accuser.terminate accuser_3 in + let* () = + if is_operation_in_operations ops denunciation_oph then unit + else Test.fail "Double baking evidence was not found" + in + + let* bu = Operation_receipt.get_block_metadata client_1 in + let* bu = Operation_receipt.Balance_updates.from_result [bu] in + + (* check slashed and rewarded amounts *) + (* total amounts *) + let total_amount_rewarded = 1457144917 in + let total_amount_slashed = 8742869507 in + + (* slashed unstake deposit *) + let amount_slashed_from_unstake_deposits = 42857144 in + let amount_rewarded_from_unstake_deposits = 7142857 in + + (* slashed stake *) + let amount_slashed_from_stakers_deposits = 43069306 in + let amount_rewarded_from_stakers_deposits = 7178217 in + + (* slashing baker (bootstrap2) stake*) + let amount_rewarded_from_delegate_deposits = 1442823843 in + let amount_slashed_from_delegate_deposits = 8656943057 in + + assert ( + amount_rewarded_from_unstake_deposits + = int_of_float (float amount_slashed_from_unstake_deposits /. 6.)) ; + + assert ( + amount_rewarded_from_stakers_deposits + = int_of_float (float amount_slashed_from_stakers_deposits /. 6.)) ; + + assert ( + amount_rewarded_from_delegate_deposits + = int_of_float @@ ceil (float amount_slashed_from_delegate_deposits /. 6.)) ; + + assert ( + amount_slashed_from_unstake_deposits + amount_slashed_from_stakers_deposits + + amount_slashed_from_delegate_deposits + = total_amount_slashed) ; + assert ( + amount_rewarded_from_unstake_deposits + + amount_rewarded_from_stakers_deposits + + amount_rewarded_from_delegate_deposits + = total_amount_rewarded) ; + + (* some values might be slightly different (+-1 mutez) because of roundings and + randomness in baking rights that may affect the overall rewards coming from + previous blocks, to avoid flakyness we test the "rounded range" of those + values *) + let check_with_roundings got expected = + got >= expected - 1 && got <= expected + 1 + in + + let check_opr ~kind ~category ~change ~staker ~delayed_operation_hash opr = + let open Operation_receipt.Balance_updates in + opr.kind = kind && opr.category = category + && check_with_roundings opr.change change + && opr.staker = staker + && + match delayed_operation_hash with + | None -> true + | Some oph -> opr.delayed_operation_hash = Some oph + in + + check_balance_updates + bu + [ + ( check_opr + ~kind:"freezer" + ~category:(Some "deposits") + ~change:(-amount_slashed_from_delegate_deposits) + ~staker:(Some (Baker {baker = Constant.bootstrap2.public_key_hash})) + ~delayed_operation_hash:None, + "Slashed from delegate's deposits" ); + ( check_opr + ~kind:"freezer" + ~category:(Some "deposits") + ~change:(-amount_slashed_from_stakers_deposits) + ~staker: + (Some + (Delegate + { + delegate = Constant.bootstrap2.public_key_hash; + contract = None; + })) + ~delayed_operation_hash:None, + "Slashed from stakers deposits" ); + ( check_opr + ~kind:"freezer" + ~category:(Some "unstaked_deposits") + ~change:(-amount_slashed_from_unstake_deposits) + ~staker: + (Some + (Delegate + { + delegate = Constant.bootstrap2.public_key_hash; + contract = None; + })) + ~delayed_operation_hash:(Some denunciation_oph), + "Slashed from unstake deposits" ); + ( check_opr + ~kind:"burned" + ~category:(Some "punishments") + ~change:total_amount_slashed + ~staker:None + ~delayed_operation_hash:None, + "Punishment for double baking" ); + ( check_opr + ~kind:"freezer" + ~category:(Some "deposits") + ~change:(-amount_rewarded_from_delegate_deposits) + ~staker:(Some (Baker {baker = Constant.bootstrap2.public_key_hash})) + ~delayed_operation_hash:(Some denunciation_oph), + "Reward from delegate's deposits" ); + ( check_opr + ~kind:"freezer" + ~category:(Some "deposits") + ~change:(-amount_rewarded_from_stakers_deposits) + ~staker: + (Some + (Delegate + { + delegate = Constant.bootstrap2.public_key_hash; + contract = None; + })) + ~delayed_operation_hash:(Some denunciation_oph), + "Reward from stakers deposits" ); + ( check_opr + ~kind:"freezer" + ~category:(Some "unstaked_deposits") + ~change:(-amount_rewarded_from_unstake_deposits) + ~staker: + (Some + (Delegate + { + delegate = Constant.bootstrap2.public_key_hash; + contract = None; + })) + ~delayed_operation_hash:(Some denunciation_oph), + "Reward from unstake deposits" ); + ( (fun opr -> + opr.kind = "contract" + && opr.change = total_amount_rewarded + && opr.origin = "delayed_operation" + && opr.contract = Some Constant.bootstrap1.public_key_hash + && opr.delayed_operation_hash = Some denunciation_oph), + "Reward for denunciator" ); + ] ; + + log_step 21 "Test finalize_unstake" ; + let* balance = Client.get_balance_for ~account:staker0.alias client_1 in + Log.info + "Balance of %s before unstake: spendable : %s" + Constant.bootstrap2.alias + (Tez.to_string balance) ; + + let* unstake_requests = + Client.RPC.call client_1 + @@ RPC.get_chain_block_context_contract_unstake_requests + staker0.public_key_hash + in + let finalizable = JSON.(unstake_requests |-> "finalizable" |> as_list) in + let unfinalizable = + JSON.(unstake_requests |-> "unfinalizable" |-> "requests" |> as_list) + in + + assert (List.length finalizable == 1) ; + assert (List.length unfinalizable == 1) ; + + Log.info "Unstaked frozen balance: %d" unstaked_frozen_balance ; + let* unstaked_finalizable_balance = + Client.RPC.call client_1 + @@ RPC.get_chain_block_context_contract_unstaked_finalizable_balance + staker0.public_key_hash + in + Log.info "Unstaked finalizable balance: %d" unstaked_finalizable_balance ; + assert (unstaked_finalizable_balance = 1000000000) ; + + let finalize_unstake = + Client.spawn_finalize_unstake ~staker:staker0.public_key_hash client_1 + in + let* () = bake_n ~endpoint ~protocol client_1 2 in + let* finalise_unstake_hash = get_hash_of finalize_unstake in + + let* () = + check_balance_updates_for + finalise_unstake_hash + [ + ( check_opr + ~kind:"freezer" + ~category:(Some "unstaked_deposits") + ~change:(-unstaked_finalizable_balance) + ~staker: + (Some + (Delegate + { + delegate = Constant.bootstrap2.public_key_hash; + contract = Some staker0.public_key_hash; + })) + ~delayed_operation_hash:None, + "Retrieved from staker0 unstaked_deposits" ); + ( (fun opr -> + opr.kind = "contract" + && check_with_roundings opr.change unstaked_finalizable_balance + && opr.contract = Some staker0.public_key_hash), + "Added to staker0 spendable balance" ); + ] + client_1 + in + + let* unstake_requests = + Client.RPC.call client_1 + @@ RPC.get_chain_block_context_contract_unstake_requests + staker0.public_key_hash + in + + let finalizable = JSON.(unstake_requests |-> "finalizable" |> as_list) in + let unfinalizable = + JSON.(unstake_requests |-> "unfinalizable" |-> "requests" |> as_list) + in + + assert (List.length finalizable == 0) ; + assert (List.length unfinalizable == 1) ; + + let* balance2 = Client.get_balance_for ~account:staker0.alias client_1 in + Log.info + "Balance of %s after unstake: spendable : %s" + staker0.alias + (Tez.to_string balance2) ; + + Log.info "Balance change = %s" (Tez.to_string Tez.(balance2 - balance)) ; + (* spendable balance should have increased from the finalized unstaked tokens + (minus the fees associated to the finalize_unstake call) *) + assert (Tez.(balance2 - balance > of_mutez_int 999999000)) ; + + let* unstaked_finalizable_balance = + Client.RPC.call client_1 + @@ RPC.get_chain_block_context_contract_unstaked_finalizable_balance + staker0.public_key_hash + in + Log.info "Unstaked finalizable balance: %d" unstaked_finalizable_balance ; + assert (unstaked_finalizable_balance = 0) ; + unit + +let register ~protocols = + test_AI_activation protocols ; + test_staking protocols -- GitLab