Revision 5c0277382a82ab3642ddd1de790a362e8c047837 authored by Pierre-Louis on 11 September 2023, 13:01:33 UTC, committed by Pierre-Louis on 25 September 2023, 12:16:22 UTC
1 parent 97eebdf
voting.ml
(*****************************************************************************)
(* *)
(* 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. *)
(* *)
(*****************************************************************************)
(* Testing
-------
Component: Amendment process
Invocation: dune exec tezt/tests/main.exe -- --file voting.ml
Subject: Test the voting process from proposal to adoption.
This test activates a protocol [from_protocol]. Then it uses the
client's [submit ballot] command on a protocol [to_protocol] that will
win the election, and on other protocols [loser_protocols].
Then the [bake for] command is used to move to the exploration period.
Another vote using [submit ballot] is made and the [bake for] command
is used to move to the cooldown period, then to the promotion period.
Another vote using [submit ballot] is made and the [bake for] command
is used to move to the adoption period.
RPCs are used regularly to check the current level and the current period
information match what we expect.
Optionally, [from_protocol] can be an injected test protocol.
In this scenario, another node is started at the end of the adoption period.
This is to check that injected protocols are propagated to other nodes
after the migration. *)
(* Protocol to inject when testing injection. *)
let test_proto_dir = "src/bin_client/test/proto_test_injection"
(* Files that are to be copied from [test_proto_dir].
We do not copy [TEZOS_PROTOCOL] because it declares an environment version
which is too old when trying to adopt protocol Alpha *)
let test_proto_files = ["main.ml"; "main.mli"]
let test_proto_TEZOS_PROTOCOL =
{|{
"modules": ["Main"],
"expected_env_version": 11
}
|}
type period_kind = Proposal | Exploration | Cooldown | Promotion | Adoption
let periods = [Proposal; Exploration; Cooldown; Promotion; Adoption]
let period_kind_to_string = function
| Proposal -> "proposal"
| Exploration -> "exploration"
| Cooldown -> "cooldown"
| Promotion -> "promotion"
| Adoption -> "adoption"
let period_kind_of_string = function
| "proposal" -> Proposal
| "exploration" -> Exploration
| "cooldown" -> Cooldown
| "promotion" -> Promotion
| "adoption" -> Adoption
| p ->
Test.fail
"Unexpected period kind %s, expected one of: %s"
p
(String.concat ", " (List.map period_kind_to_string periods))
let period_kind_type : period_kind Check.typ =
Check.convert period_kind_to_string Check.string
type period = {
index : int;
kind : period_kind;
start_position : int;
position : int;
remaining : int;
}
let pp_period fmt period =
Format.fprintf
fmt
"{ index: %d; kind: %s; start_position: %d; position: %d; remaining: %d }"
period.index
(period_kind_to_string period.kind)
period.start_position
period.position
period.remaining
let period_type : period Check.typ =
Check.convert
(fun {index; kind; start_position; position; remaining} ->
(index, kind, start_position, position, remaining))
Check.(tuple5 int period_kind_type int int int)
let decode_period json =
let index = JSON.(json |-> "voting_period" |-> "index" |> as_int) in
let kind =
JSON.(
json |-> "voting_period" |-> "kind" |> as_string |> period_kind_of_string)
in
let start_position =
JSON.(json |-> "voting_period" |-> "start_position" |> as_int)
in
let position = JSON.(json |-> "position" |> as_int) in
let remaining = JSON.(json |-> "remaining" |> as_int) in
{index; kind; start_position; position; remaining}
let get_current_period ?level client =
let* json =
RPC.Client.call client
@@ RPC.get_chain_block_votes_current_period
?block:(Option.map string_of_int level)
()
in
return (decode_period json)
let get_successor_period ?level client =
let* json =
RPC.Client.call client
@@ RPC.get_chain_block_votes_successor_period
?block:(Option.map string_of_int level)
()
in
return (decode_period json)
let check_current_period ?level client expected_period =
let* period = get_current_period ?level client in
Check.((period = expected_period) period_type)
~error_msg:"expected current_period = %R, got %L" ;
unit
let check_successor_period ?level client expected_period =
let* period = get_successor_period ?level client in
Check.((period = expected_period) period_type)
~error_msg:"expected successor_period = %R, got %L" ;
unit
let level_type : RPC.level Check.typ =
Check.convert
(fun RPC.{level; level_position; cycle; cycle_position; expected_commitment} ->
(level, level_position, cycle, cycle_position, expected_commitment))
Check.(tuple5 int int int int bool)
let get_current_level client =
RPC.Client.call client @@ RPC.get_chain_block_helper_current_level ()
let check_current_level client expected_level =
let* level = get_current_level client in
Check.((level = expected_level) level_type)
~error_msg:"expected current_period = %R, got %L" ;
unit
let get_proposals ?level client =
let* proposals =
RPC.Client.call client
@@ RPC.get_chain_block_votes_proposals
?block:(Option.map string_of_int level)
()
in
JSON.as_list proposals |> List.map JSON.as_list
|> List.map (function
| [hash; _] -> JSON.as_string hash
| _ ->
Test.fail
"invalid proposal in JSON response: expected a list with 2 items")
|> return
let get_current_proposal ?level client =
let* proposal =
RPC.Client.call client
@@ RPC.get_chain_block_votes_current_proposal
?block:(Option.map string_of_int level)
()
in
if JSON.is_null proposal then return None
else return (Some JSON.(proposal |> as_string))
let check_current_proposal ?level client expected_proposal_hash =
let* current_proposal = get_current_proposal ?level client in
Check.((current_proposal = Some expected_proposal_hash) (option string))
~error_msg:"expected current_proposal = %R, got %L" ;
unit
let check_protocols ?level client expected_protocols =
let* block_metadata =
RPC.Client.call client
@@ RPC.get_chain_block_metadata ?block:(Option.map string_of_int level) ()
in
let protocols_got = (block_metadata.protocol, block_metadata.next_protocol) in
Check.((protocols_got = expected_protocols) (tuple2 string string))
~error_msg:"expected (protocol, next_protocol) = %R, got %L" ;
unit
let check_listings_not_empty client =
let* listings =
RPC.Client.call client @@ RPC.get_chain_block_votes_listings ()
in
match JSON.as_list listings with
| [] ->
Test.fail "Expected GET .../votes/listing RPC to return a non-empty list"
| _ :: _ -> unit
type target_protocol = Known of Protocol.t | Injected_test | Demo
let target_protocol_tag = function
| Known protocol -> Protocol.tag protocol
| Injected_test -> "injected_test"
| Demo -> "demo_counter"
let inject_test_protocol client =
let protocol_path = Temp.dir "test_proto" in
let* () =
Process.run
"cp"
(List.map (fun filename -> test_proto_dir // filename) test_proto_files
@ [protocol_path])
in
( with_open_out (protocol_path // "TEZOS_PROTOCOL") @@ fun ch ->
output_string ch test_proto_TEZOS_PROTOCOL ) ;
let* protocols_before = Client.Admin.list_protocols client in
let* test_proto_hash = Client.Admin.inject_protocol client ~protocol_path in
let* protocols_after = Client.Admin.list_protocols client in
if List.mem test_proto_hash protocols_before then
(* Note that [inject_protocol] normally fails if the protocol is already known.
So this check is only here in case the behavior of [inject_protocol] changes,
to prevent running this test with an already-known protocol. *)
Test.fail
"Test protocol %s was already known, I can't test injection with it."
test_proto_hash ;
if not (List.mem test_proto_hash protocols_after) then
Test.fail
"Test protocol %s is not known even though it was injected successfully."
test_proto_hash ;
Log.info "Injected: %s" test_proto_hash ;
return test_proto_hash
let test_voting ~from_protocol ~(to_protocol : target_protocol) ~loser_protocols
=
Test.register
~__FILE__
~title:
(sf
"amendment: %s -> %s (losers: %s)"
(Protocol.tag from_protocol)
(target_protocol_tag to_protocol)
(if loser_protocols = [] then "none"
else String.concat ", " (List.map Protocol.tag loser_protocols)))
~tags:
("amendment"
:: ("from_" ^ Protocol.tag from_protocol)
:: ("to_" ^ target_protocol_tag to_protocol)
:: List.map (fun p -> "loser_" ^ Protocol.tag p) loser_protocols
@ [
(match to_protocol with
| Known _ | Demo -> "known"
| Injected_test -> "injected");
])
@@ fun () ->
(* Prepare protocol parameters such that voting periods are shorter
to make the test run faster. *)
(* Protocol Alpha uses [cycles_per_voting_period]
instead of [blocks_per_voting_period],
we use the values [blocks_per_cycle] = 4 and
[cycles_for_voting_period] = 1 for Alpha, and we set
[blocks_per_cycle] = 4 and [blocks_per_voting_period] = 4
for other protocols. This way periods have the same number
of blocks for all protocols, and we do not need to handle
different cases when checking levels and periods in tests. *)
let parameters =
[
(["blocks_per_cycle"], `Int 4);
(["nonce_revelation_threshold"], `Int 2);
(["cycles_per_voting_period"], `Int 1);
]
in
let* parameter_file =
Protocol.write_parameter_file ~base:(Right (from_protocol, None)) parameters
in
(* Start a node and activate [from_protocol]. *)
let* node = Node.init [Synchronisation_threshold 0] in
let* client = Client.init ~endpoint:(Node node) () in
let* () =
Client.activate_protocol ~protocol:from_protocol ~parameter_file client
in
let* () =
check_current_level
client
{
level = 1;
level_position = 0;
cycle = 0;
cycle_position = 0;
expected_commitment = false;
}
in
let* () =
check_current_period
client
{
index = 0;
kind = Proposal;
start_position = 0;
position = 0;
remaining = 3;
}
in
let* () =
check_successor_period
client
{
index = 0;
kind = Proposal;
start_position = 0;
position = 1;
remaining = 2;
}
in
Log.info "Current period: proposal." ;
(* Bake while performing more RPC checks. *)
let* () = repeat 2 (fun () -> Client.bake_for_and_wait client) in
let* () =
check_current_level
client
{
level = 3;
level_position = 2;
cycle = 0;
cycle_position = 2;
expected_commitment = false;
}
in
let* () =
check_current_period
client
{
index = 0;
kind = Proposal;
start_position = 0;
position = 2;
remaining = 1;
}
in
let* () = Client.bake_for_and_wait client in
let* () =
check_current_period
client
{
index = 0;
kind = Proposal;
start_position = 0;
position = 3;
remaining = 0;
}
in
let* proposals = get_proposals client in
Check.((proposals = []) (list string))
~error_msg:"expected proposal list to be empty, got %L" ;
let* () = check_listings_not_empty client in
let* period = Client.show_voting_period client in
Check.((period = "proposal") string)
~error_msg:"expected octez-client show voting period to return %R, got %L" ;
(* Inject test protocol, or use known protocol. *)
let* to_protocol_hash =
match to_protocol with
| Known protocol -> return (Protocol.hash protocol)
| Injected_test -> inject_test_protocol client
| Demo -> return Protocol.demo_counter_hash
in
(* Submit proposals: [to_protocol] and [loser_protocols]. *)
let* () =
Client.submit_proposals
~proto_hashes:(to_protocol_hash :: List.map Protocol.hash loser_protocols)
client
in
(* Bake and check that the proposals are registered. *)
let* () = Client.bake_for_and_wait client in
let* proposals = get_proposals client in
let expected_proposals =
to_protocol_hash :: List.map Protocol.hash loser_protocols
in
let sort = List.sort String.compare in
Check.((sort proposals = sort expected_proposals) (list string))
~error_msg:"expected proposals = %R, got %L" ;
(* Have another baker vote for [to_protocol] to make [to_protocol] win. *)
let* () =
Client.submit_proposals
~key:Constant.bootstrap2.alias
~proto_hash:to_protocol_hash
client
in
(* Bake and check that the proposals are still registered. *)
let* () = Client.bake_for_and_wait client in
let* proposals = get_proposals client in
Check.((sort proposals = sort expected_proposals) (list string))
~error_msg:"expected proposals = %R, got %L" ;
(* Bake until exploration period while checking RPC behavior. *)
let* () = repeat 2 (fun () -> Client.bake_for_and_wait client) in
let expected_level =
(* level_position = 7, 4 blocks per cycle =>
cycle = 7/4 = 1,
cycle_position = 7 % 4 = 3
*)
RPC.
{
level = 8;
level_position = 7;
cycle = 1;
cycle_position = 3;
expected_commitment = true;
}
in
let* () = check_current_level client expected_level in
let* () =
check_current_period
client
{
index = 1;
kind = Proposal;
start_position = 4;
position = 3;
remaining = 0;
}
in
let* () = check_listings_not_empty client in
let* () = check_current_proposal client to_protocol_hash in
let* () = Client.bake_for_and_wait client in
let* () =
check_current_period
client
{
index = 2;
kind = Exploration;
start_position = 8;
position = 0;
remaining = 3;
}
in
let* () = check_current_proposal client to_protocol_hash in
Log.info
"Current period: exploration, with current proposal: %s."
(target_protocol_tag to_protocol) ;
(* Vote for [to_protocol]. *)
let vote (account : Account.key) ballot =
Client.submit_ballot
~key:account.alias
~proto_hash:to_protocol_hash
ballot
client
in
let* () = vote Constant.bootstrap1 Yay in
let* () = vote Constant.bootstrap2 Yay in
let* () = vote Constant.bootstrap3 Yay in
(* Bake until cooldown period while checking RPCs. *)
let* () = Client.bake_for_and_wait client in
let* () =
let expected_level =
(* level_position = 9, 4 blocks_per_cycle =>
cycle = 9/4 = 2
cycle_position = 9 % 4 = 1
*)
RPC.
{
level = 10;
level_position = 9;
cycle = 2;
cycle_position = 1;
expected_commitment = false;
}
in
check_current_level client expected_level
in
let* () =
check_current_period
client
{
index = 2;
kind = Exploration;
start_position = 8;
position = 1;
remaining = 2;
}
in
let* () = repeat 2 (fun () -> Client.bake_for_and_wait client) in
let* () =
check_current_period
client
{
index = 2;
kind = Exploration;
start_position = 8;
position = 3;
remaining = 0;
}
in
let* () = Client.bake_for_and_wait client in
let* () =
check_current_period
client
{
index = 3;
kind = Cooldown;
start_position = 12;
position = 0;
remaining = 3;
}
in
(* Check current proposal again now that we switched to the cooldown period. *)
let* current_proposal = get_current_proposal client in
Check.((current_proposal = Some to_protocol_hash) (option string))
~error_msg:"expected current_proposal = %R, got %L" ;
Log.info
"Current period: cooldown, with current proposal: %s."
(target_protocol_tag to_protocol) ;
(* Bake until promotion period while checking RPCs. *)
let* () = Client.bake_for_and_wait client in
let* () =
let expected_level =
(* level_position = 13, 4 blocks_per_cycle =>
cycle = 13/4 = 3
cycle_position = 13 % 4 = 1
*)
RPC.
{
level = 14;
level_position = 13;
cycle = 3;
cycle_position = 1;
expected_commitment = false;
}
in
check_current_level client expected_level
in
let* () =
check_current_period
client
{
index = 3;
kind = Cooldown;
start_position = 12;
position = 1;
remaining = 2;
}
in
let* () = repeat 2 (fun () -> Client.bake_for_and_wait client) in
let* () =
check_current_period
client
{
index = 3;
kind = Cooldown;
start_position = 12;
position = 3;
remaining = 0;
}
in
let* () = Client.bake_for_and_wait client in
let* () =
check_current_period
client
{
index = 4;
kind = Promotion;
start_position = 16;
position = 0;
remaining = 3;
}
in
(* Check current proposal again now that we switched to the promotion period. *)
let* current_proposal = get_current_proposal client in
Check.((current_proposal = Some to_protocol_hash) (option string))
~error_msg:"expected current_proposal = %R, got %L" ;
Log.info
"Current period: promotion, with current proposal: %s."
(target_protocol_tag to_protocol) ;
(* Vote for [to_protocol]. Put in a Nay for good measure (we need more Yay as a result). *)
let* () = vote Constant.bootstrap1 Yay in
let* () = vote Constant.bootstrap2 Yay in
let* () = vote Constant.bootstrap3 Yay in
let* () = vote Constant.bootstrap4 Yay in
let* () = vote Constant.bootstrap5 Nay in
(* Bake until adoption period while checking RPCs. *)
let* () = Client.bake_for_and_wait client in
let* () =
let expected_level =
(* level_position = 17, 4 blocks_per_cycle =>
cycle = 17/4 = 4
cycle_position = 17 % 4 = 1
*)
RPC.
{
level = 18;
level_position = 17;
cycle = 4;
cycle_position = 1;
expected_commitment = false;
}
in
check_current_level client expected_level
in
let* () =
check_current_period
client
{
index = 4;
kind = Promotion;
start_position = 16;
position = 1;
remaining = 2;
}
in
let* () = repeat 2 (fun () -> Client.bake_for_and_wait client) in
let* () =
check_current_period
client
{
index = 4;
kind = Promotion;
start_position = 16;
position = 3;
remaining = 0;
}
in
let* () = Client.bake_for_and_wait client in
let* () =
check_current_period
client
{
index = 5;
kind = Adoption;
start_position = 20;
position = 0;
remaining = 3;
}
in
(* Check current proposal again now that we switched to the adoption period. *)
let* current_proposal = get_current_proposal client in
Check.((current_proposal = Some to_protocol_hash) (option string))
~error_msg:"expected current_proposal = %R, got %L" ;
Log.info
"Current period: adoption, with current proposal: %s."
(target_protocol_tag to_protocol) ;
(* Bake until next_protocol changes. *)
let* () = Client.bake_for_and_wait client in
let* () =
let expected_level =
(* level_position = 21, 4 blocks_per_cycle =>
cycle = 21/4 = 5
cycle_position = 21 % 4 = 1
*)
RPC.
{
level = 22;
level_position = 21;
cycle = 5;
cycle_position = 1;
expected_commitment = false;
}
in
check_current_level client expected_level
in
let* () =
check_current_period
client
{
index = 5;
kind = Adoption;
start_position = 20;
position = 1;
remaining = 2;
}
in
let* () = Client.bake_for_and_wait client in
let* () =
check_protocols
client
(Protocol.hash from_protocol, Protocol.hash from_protocol)
in
Log.info "Baking last block of adoption period..." ;
let* () = Client.bake_for_and_wait client in
match to_protocol with
| Injected_test ->
(* The test protocol has no amendment process and no baker. We can't go further.
Instead, we check that another node can fetch the injected protocol. *)
Log.info "Creating a second node..." ;
(* FIXME https://gitlab.com/tezos/tezos/-/issues/4837
We use the "Singleprocess" option because this test relies on
events which are emitted by the validator. The event mechanism
of Tezt is currently not compatible with the validator events if it
is run as an external process by the octez-node (which is the
case by default). *)
let* node2 = Node.init [Synchronisation_threshold 1; Singleprocess] in
let check_event_fetching_protocol =
(* Example:
fetching_protocol.v0 = {
"hash": "PtW12iP7VWYi4jz24Rwn5wrm5wtZkXuGYbEDC6U4RU4RV3ekWSe",
"source": null
} *)
Node.wait_for node2 "fetching_protocol.v0" @@ fun json ->
let hash = JSON.(json |-> "hash" |> as_string) in
if hash = Protocol.hash from_protocol then
(* Continue waiting for the other protocol. *)
None
else (
Check.((hash = to_protocol_hash) string)
~error_msg:"expected node2 to fetch protocol %R, got %L" ;
Log.info "Node 2 is fetching the injected protocol." ;
Some ())
in
let check_event_new_protocol_initialisation =
(* Example:
new_protocol_initialisation.v0 =
"PtW12iP7VWYi4jz24Rwn5wrm5wtZkXuGYbEDC6U4RU4RV3ekWSe" *)
Node.wait_for node2 "new_protocol_initialisation.v0" @@ fun json ->
let hash = JSON.as_string json in
if hash = Protocol.hash from_protocol then
(* Continue waiting for the other protocol. *)
None
else (
Check.((hash = to_protocol_hash) string)
~error_msg:"expected node2 to initialize protocol %R, got %L" ;
Log.info "Node 2 initialized the injected protocol." ;
Some ())
in
let check_event_update_protocol_table =
(* Example:
update_protocol_table.v0 = {
"proto_hash": "PtW12iP7VWYi4jz24Rwn5wrm5wtZkXuGYbEDC6U4RU4RV3ekWSe",
"proto_level": 2,
"block_hash": "BMD8dVEivzWp3zu4mFSWnJG98g61kus3t7oJqK5L2d9k71PxkQi",
"block_level": 24
} *)
Node.wait_for node2 "update_protocol_table.v0" @@ fun json ->
let proto_hash = JSON.(json |-> "proto_hash" |> as_string) in
if proto_hash = Protocol.hash from_protocol then
(* Continue waiting for the other protocol. *)
None
else
let proto_level = JSON.(json |-> "proto_level" |> as_int) in
Check.(
((proto_hash, proto_level) = (to_protocol_hash, 2))
(tuple2 string int))
~error_msg:
"expected node 2 to update its protocol table with %R, got %L" ;
Log.info
"Node 2 updated its protocol table with the injected protocol." ;
Some ()
in
let* () = Client.Admin.connect_address ~peer:node2 client in
Log.info
"Node 2 initialized and connected to node1, waiting for it to sync..." ;
let* level =
let* node_level = Node.get_level node in
Node.wait_for_level node2 node_level
in
Log.info "Both nodes are at level %d." level ;
let all_checks =
let* () = check_event_fetching_protocol
and* () = check_event_new_protocol_initialisation
and* () = check_event_update_protocol_table in
unit
in
let timeout =
(* We expect those three events to trigger *before* node 2 reaches the level
of node 1. So a small timeout of 1 minute is fine.
No timeout would mean that we could wait forever if the events are
not emitted by the node or not received correctly,
and a timeout of 0 could cause the Lwt scheduler to choose to wake up
the timeout first. *)
let* () = Lwt_unix.sleep 60. in
Test.fail
"timeout while waiting for fetching_protocol, \
new_protocol_initialisation, or update_protocol_table"
in
let* () = Lwt.pick [all_checks; timeout] in
unit
| Known _ ->
let* () =
check_current_period
client
{
index = 5;
kind = Adoption;
start_position = 20;
position = 3;
remaining = 0;
}
in
let* () =
check_protocols client (Protocol.hash from_protocol, to_protocol_hash)
in
Log.info
"Current period: adoption, with next protocol: %s."
(target_protocol_tag to_protocol) ;
(* Bake the transition. *)
Log.info "Baking transition block..." ;
let everybody =
Array.map (fun x -> x.Account.public_key_hash) Account.Bootstrap.keys
|> Array.to_list
in
let* () = Client.bake_for_and_wait ~keys:everybody client in
let* () =
check_current_period
client
{
index = 6;
kind = Proposal;
start_position = 24;
position = 0;
remaining = 3;
}
in
let* () = check_protocols client (to_protocol_hash, to_protocol_hash) in
Log.info
"Current period: proposal, with protocol: %s."
(target_protocol_tag to_protocol) ;
(* Bake one last block to ensure we can still bake after the transition. *)
let* () = Client.bake_for_and_wait ~keys:everybody client in
let* () =
check_current_period
client
{
index = 6;
kind = Proposal;
start_position = 24;
position = 1;
remaining = 2;
}
in
check_protocols client (to_protocol_hash, to_protocol_hash)
| Demo ->
let* () =
check_protocols client (Protocol.hash from_protocol, to_protocol_hash)
in
Log.info
"Current period: adoption, with next protocol: %s."
(target_protocol_tag to_protocol) ;
Log.info "Baking transition block..." ;
let* () = Demo_client.bake client in
check_protocols client (to_protocol_hash, to_protocol_hash)
let test_user_activated_protocol_override_baker_vote ~from_protocol ~to_protocol
=
Test.register
~__FILE__
~title:
(sf
"user-activated protocol override after vote with bakers: %s -> %s"
(Protocol.tag from_protocol)
(Protocol.tag to_protocol))
~tags:
[
"amendment";
"protocol_override";
"baker";
"voting";
"from_" ^ Protocol.tag from_protocol;
"to_" ^ Protocol.tag to_protocol;
]
@@ fun () ->
let node_arguments = [Node.Synchronisation_threshold 0] in
let to_protocol_hash = Protocol.hash to_protocol in
(* Prepare protocol parameters such that voting periods are shorter
to make the test run faster. *)
let blocks_per_voting_period = 8 in
let blocks_per_cycle = 2 * blocks_per_voting_period in
(* The proposal that will be voted and overridden is submitted in
the second proposal period. We wait to the very start of this to
have more time to submit the proposal *)
let target_proposal_period_start = 1 + blocks_per_voting_period in
(* Pre-calculate the voting cycle based on the level of the submission.
[start_of_period] maps periods to level at which they start, assuming that:
- Proposal starts at [target_proposal_period_start]
- Each period continues to the following period (i.e. does not move
back to Proposal until after Adoption.
*)
let start_of_period = function
| Proposal -> target_proposal_period_start
| Exploration ->
target_proposal_period_start + (1 * blocks_per_voting_period)
| Cooldown -> target_proposal_period_start + (2 * blocks_per_voting_period)
| Promotion -> target_proposal_period_start + (3 * blocks_per_voting_period)
| Adoption -> target_proposal_period_start + (4 * blocks_per_voting_period)
in
(* Check whether the operations at a level contains a given protocol
submission.
Checks whether the operations of the block at [level]
contains a proposal for protocol [proto_hash].
*)
let proposal_in_level ~proto_hash client level =
let* ops =
RPC.Client.call client
@@ RPC.get_chain_block_operations ~block:(string_of_int level) ()
in
let proposals =
List.concat_map
(fun op ->
let contents = JSON.(op |-> "contents" |> as_list) in
let proposals =
List.concat
@@ List.filter_map
(fun op ->
if JSON.(op |-> "kind" |> as_string) = "proposals" then
Some
JSON.(
op |-> "proposals" |> as_list |> List.map as_string)
else None)
contents
in
proposals)
JSON.(ops |=> 1 |> as_list)
in
return (List.mem proto_hash proposals)
in
(* Perform an [action] before the node reaches a [level_limit], or fail.
- [~limit_description] describes the limit (e.g. "last block where action can be formed")
- [~action_description] describes the action (e.g. "submit proposal")
*)
let do_before_level ?__LOC__ ~limit_description ~action_description node
limit_level action =
let wait_for_successor =
let* (current_level : int) = Node.wait_for_level node limit_level in
Test.fail
?__LOC__
"Node (%s) reached block %d, beyond the level limit %d (%s) before it \
had time to %s"
(Node.name node)
current_level
limit_level
limit_description
action_description
in
let action =
let* r = action in
let* current_level = Node.get_level node in
Log.info
"Action `%s` executed before or at level %d (limit %d, %s)"
action_description
current_level
limit_level
limit_description ;
return r
in
Lwt.pick [wait_for_successor; action]
in
(* Perform an [action] (described by [action_description])
before the the last block of period [kind]. *)
let do_in_period ?__LOC__ ~action_description node kind action =
let kind_limit_level =
start_of_period kind + blocks_per_voting_period - 1
in
let limit_description =
Format.asprintf "the last block of period %s" (period_kind_to_string kind)
in
do_before_level
?__LOC__
~action_description
~limit_description
node
kind_limit_level
action
in
(* Submit a proposal then wait for its inclusion and return the level thereof.
Propose protocol [proto_hash], and then wait for the head to switch to the
block in which it is included. Returns the level of that block.
*)
let submit_and_wait_for_proposal ?(level_out = 5) node client proto_hash =
let* start_level = Node.get_level node in
(* We have to perform the submissions sufficiently early in the
proposal period:
- it seems that proposals included in the last block of the
proposal period are *not* accounted for -- unclear whether this is a
bug or not
- I'm not sure whether it's a concurrency issue in the implementation of
[do_before_level] but it seems that it does not suffice that the
submission is performed before the node reaches the
predecessor of the last block. Therefore the limit is set one block
lower to have more margin:
Block (Proposal +0)
Block (Proposal +1)
Block (Proposal +blocks_per_voting_period-3):
we try to perform the submission here at latest
Block (Proposal +blocks_per_voting_period-2):
limit it set here, if the node reaches this level before the
submission was performed the test fails.
Block (Proposal +blocks_per_voting_period-1):
proposals included here are ignored
Block (Exploration +0): exploration period starts if everything went well
*)
let* () =
do_before_level
~__LOC__
~action_description:"submit proposal"
~limit_description:"second to last block of target proposal period"
node
(target_proposal_period_start + blocks_per_voting_period - 2)
(Client.submit_proposals ~proto_hash client)
in
let rec loop level =
if level - start_level >= level_out then
Test.fail
"Could not find the proposal operation after waiting %d blocks from \
level %d"
level_out
start_level
else
let* (_ : int) = Node.wait_for_level node level in
let* in_level = proposal_in_level ~proto_hash client level in
if in_level then return level else loop (level + 1)
in
loop (start_level + 1)
in
(* Wait for the level at which the period of [kind] should start according to
the [start_of_period]. *)
let wait_for_period_kind node client kind =
let expected_level = start_of_period kind in
let* (_ : int) = Node.wait_for_level node expected_level in
(* Sanity checks *)
let* period = get_current_period ~level:expected_level client in
let* previous_period =
get_current_period ~level:(expected_level - 1) client
in
Check.(previous_period.kind <> kind)
period_kind_type
~error_msg:
("Did not expect to find period kind %R at level "
^ string_of_int (expected_level - 1)) ;
Check.(period.kind = kind)
period_kind_type
~error_msg:
("Expected to find period kind %R but found %L in level "
^ string_of_int expected_level) ;
return expected_level
in
(* Wait for last level at which the period of [kind] should be
active according to the [start_of_period]. *)
let wait_for_last_block_of_period_kind node client kind expected_level =
let* (_ : int) = Node.wait_for_level node expected_level in
(* Sanity checks *)
let* period = get_current_period ~level:expected_level client in
let* successor_period = get_successor_period ~level:expected_level client in
Check.(successor_period.kind <> kind)
period_kind_type
~error_msg:
("Did not expect to find period kind %L, it should be the first block \
of the following period, in level "
^ string_of_int (expected_level + 1)) ;
Check.(period.kind = kind)
period_kind_type
~error_msg:
("Found period kind %L but expected %R (should be last block of that \
kind), in level"
^ string_of_int expected_level) ;
return expected_level
in
let* parameter_file =
(* See notes above in [test_voting].
In addition, should this test turn out to be flaky, some pointers to make
it more robust (at the cost of speed), are:
- at more blocks to each voting period by increasing
[blocks_per_voting_period].
- (in tenderbake) increasing the delay between blocks, by increasing
the constants [minimal_block_delay] and [delay_increment_per_round]
In conjunction, they will give the test more time to perform actions and
assertions in between blocks.
The parts of the test that are inherently flaky are wrapped in calls to
[do_before_level] and [do_in_period]. These are:
- Submitting the protocol proposal before the end of the second
proposal period (we ignore the first proposal period).
- Casting votes before the end of the Exploration period
- Recasting votes before the end of the Promotion period
If the test turns out flaky, I suggest investigating there.
*)
Protocol.write_parameter_file
~base:(Right (from_protocol, None))
[
(["blocks_per_cycle"], `Int blocks_per_cycle);
(["blocks_per_voting_period"], `Int blocks_per_voting_period);
]
in
(* Start the actual test *)
Log.info "Start a node and inject a test protocol that will be overriden" ;
let* node = Node.init node_arguments in
let* client = Client.init ~endpoint:(Node node) () in
(* Curry some functions for brevity *)
let submit_and_wait_for_proposal ?level_out =
submit_and_wait_for_proposal ?level_out node client
in
let wait_for_period_kind = wait_for_period_kind node client in
let do_in_period ?__LOC__ = do_in_period ?__LOC__ node in
let wait_for_last_block_of_period_kind =
wait_for_last_block_of_period_kind node client
in
let* test_proto_hash = inject_test_protocol client in
let* () = Node.terminate node in
Log.info
"Restart the node with a protocol override configuration for %s"
test_proto_hash ;
Node.Config_file.(
update
node
(set_sandbox_network_with_user_activated_overrides
[(test_proto_hash, to_protocol_hash)])) ;
let* () = Node.terminate node in
let* () = Node.run node node_arguments in
let* () = Node.wait_for_ready node in
let* () =
(* By default, [Client.activate_protocol] activates the protocols
with a back-dated timestamp. This allows for rapid baking of
blocks, which is desirable in synchronous tests. In this test
we bake asynchronously with our assertions. We prefer the
default delay between blocks, to give us larger
margins. Therefore, we activate the protocol at the current
timestamp. *)
Client.activate_protocol
~protocol:from_protocol
~timestamp:Now
~parameter_file
client
in
let* () =
check_protocols
client
( "ProtoGenesisGenesisGenesisGenesisGenesisGenesk612im",
Protocol.hash from_protocol )
in
Log.info "Protocol set to activate, starting daemons" ;
(* Register event handlers before starting the baker to avoid flakiness *)
let wait_for_second_proposal_period_start_p =
Node.wait_for_level node target_proposal_period_start
in
let wait_for_period_kind_exploration_p = wait_for_period_kind Exploration in
let wait_for_period_kind_cooldown_p = wait_for_period_kind Cooldown in
let wait_for_period_kind_promotion_p = wait_for_period_kind Promotion in
let wait_for_period_kind_adoption_p = wait_for_period_kind Adoption in
let expected_level_of_last_block_adoption =
start_of_period Adoption + blocks_per_voting_period - 1
in
let wait_for_last_block_of_period_kind_adoption_p =
wait_for_last_block_of_period_kind
Adoption
expected_level_of_last_block_adoption
in
let expected_level_of_next_proposal =
expected_level_of_last_block_adoption + 1
in
let wait_for_next_proposal_p =
Node.wait_for_level node expected_level_of_next_proposal
in
let good_measure = 3 in
let wait_for_final_bakes_p =
Node.wait_for_level node (expected_level_of_next_proposal + good_measure)
in
let* _ = Baker.init ~protocol:from_protocol node client in
(* The baker for [to_protocol] is not ready until that protocol
activates, therefore we do not resolve yet. *)
let _to_protocol_baker_p = Baker.init ~protocol:to_protocol node client in
(* We also start an accuser for the [to_protocol] protocol. After the
protocol switch, we verify that it starts registering blocks. *)
let to_protocol_accuser = Accuser.create ~protocol:to_protocol node in
let to_protocol_accuser_received_block =
Accuser.wait_for
to_protocol_accuser
"accuser_processed_block.v0"
(fun json -> Some (json |> JSON.as_string))
in
let* _ = Accuser.run to_protocol_accuser in
Log.info "Waiting for the start of the second proposal period" ;
let* (_ : int) = wait_for_second_proposal_period_start_p in
let* () =
check_protocols
~level:2
client
(Protocol.hash from_protocol, Protocol.hash from_protocol)
in
Log.info "Submitting injected proposal: %s" test_proto_hash ;
let* proposal_level = submit_and_wait_for_proposal test_proto_hash in
let* proposal_period = get_current_period ~level:proposal_level client in
Log.info
"Found the protocol submission in level %d (remaining: %d)"
proposal_level
proposal_period.remaining ;
let* proposals = get_proposals ~level:proposal_level client in
Check.((proposals = [test_proto_hash]) (list string))
~error_msg:"expected proposals = %R, got %L" ;
Log.info "Wait until exploration period" ;
let* exploration_level = wait_for_period_kind_exploration_p in
let* () =
check_current_proposal ~level:exploration_level client test_proto_hash
in
Log.info "Vote for %s" test_proto_hash ;
let vote (account : Account.key) ballot =
Client.submit_ballot
~key:account.alias
~proto_hash:test_proto_hash
ballot
client
in
let votes accts ballot =
Lwt_list.iter_s (fun acct -> vote acct ballot) accts
in
let vote_participants =
[Constant.bootstrap1; Constant.bootstrap2; Constant.bootstrap3]
in
let* () =
do_in_period ~__LOC__ ~action_description:"cast votes" Exploration
@@ votes vote_participants Yay
in
Log.info "Wait until cooldown period" ;
let* cooldown_level = wait_for_period_kind_cooldown_p in
let* () =
check_current_proposal ~level:cooldown_level client test_proto_hash
in
Log.info "Wait until promotion period, revote for %s" test_proto_hash ;
let* promotion_level = wait_for_period_kind_promotion_p in
let* () =
check_current_proposal ~level:promotion_level client test_proto_hash
in
let* () =
do_in_period ~__LOC__ ~action_description:"recast votes" Promotion
@@ votes vote_participants Yay
in
Log.info "Wait until adoption period" ;
let* adoption_level = wait_for_period_kind_adoption_p in
let* () =
check_current_proposal ~level:adoption_level client test_proto_hash
in
Log.info "Wait for the last block of the adoption period" ;
let* (_ : int) = wait_for_last_block_of_period_kind_adoption_p in
Log.info
"Check that replacement protocol %s will activate instead of voted %s"
to_protocol_hash
test_proto_hash ;
let* () =
check_protocols
~level:expected_level_of_last_block_adoption
client
(Protocol.hash from_protocol, to_protocol_hash)
in
Log.info
"Bake until the first block of the next proposal period at level %d"
expected_level_of_next_proposal ;
let* (_ : int) = wait_for_next_proposal_p in
Log.info "Activate the replacement protocol baker" ;
let* _ = _to_protocol_baker_p in
Log.info
"Check that replacement protocol %s is active"
(Protocol.hash to_protocol) ;
let* () =
check_protocols
~level:expected_level_of_next_proposal
client
(Protocol.hash to_protocol, Protocol.hash to_protocol)
in
(* Bake a few blocks for good measure *)
let* (_ : int) = wait_for_final_bakes_p in
Log.info
"Verify that the replacement accuser has registered at least one block" ;
let* accuser_first_block_hash = to_protocol_accuser_received_block in
let* proposal_first_block_hash =
RPC.Client.call client
@@ RPC.get_chain_block_hash
~block:(string_of_int expected_level_of_next_proposal)
()
in
Check.((accuser_first_block_hash = proposal_first_block_hash) string)
~error_msg:"Expected the accuser to find %R, found %L" ;
unit
let register ~from_protocol ~(to_protocol : target_protocol) ~loser_protocols =
test_voting ~from_protocol ~to_protocol ~loser_protocols ;
(* The test_user_activated_protocol_override_baker_vote test only
supports a known target protocol. *)
match to_protocol with
| Known to_protocol ->
test_user_activated_protocol_override_baker_vote
~from_protocol
~to_protocol
| Demo | Injected_test -> ()
![swh spinner](/static/img/swh-spinner.gif)
Computing file changes ...