From 2b6c195777df86d388b1c5467ef205136248abff Mon Sep 17 00:00:00 2001 From: Arvid Jakobsson Date: Thu, 24 Nov 2022 13:48:29 +0100 Subject: [PATCH 1/3] Tezt/Client: trim results of [get_contract_hash, hash_script] --- tezt/lib_tezos/client.ml | 14 ++++++++++---- tezt/tests/bad_indentation.ml | 6 ++---- tezt/tests/contract_hash_with_origination.ml | 4 +--- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/tezt/lib_tezos/client.ml b/tezt/lib_tezos/client.ml index 3038b4f7602c..485c747998fb 100644 --- a/tezt/lib_tezos/client.ml +++ b/tezt/lib_tezos/client.ml @@ -1669,8 +1669,11 @@ let spawn_hash_script ?hooks ?for_script ~script client = @ optional_arg "for-script" show_hash_script_format for_script let hash_script ?hooks ?for_script ~script client = - spawn_hash_script ?hooks ?for_script ~script client - |> Process.check_and_read_stdout + let* client_output = + spawn_hash_script ?hooks ?for_script ~script client + |> Process.check_and_read_stdout + in + return (String.trim client_output) let spawn_get_contract_hash ?hooks ~contract client = spawn_command @@ -1679,8 +1682,11 @@ let spawn_get_contract_hash ?hooks ~contract client = ["get"; "contract"; "script"; "hash"; "for"; contract] let get_contract_hash ?hooks ~contract client = - spawn_get_contract_hash ?hooks ~contract client - |> Process.check_and_read_stdout + let* client_output = + spawn_get_contract_hash ?hooks ~contract client + |> Process.check_and_read_stdout + in + return (String.trim client_output) let spawn_hash_scripts ?hooks ?(display_names = false) ?for_script scripts client = diff --git a/tezt/tests/bad_indentation.ml b/tezt/tests/bad_indentation.ml index 70f9df42933e..747f2113558f 100644 --- a/tezt/tests/bad_indentation.ml +++ b/tezt/tests/bad_indentation.ml @@ -59,8 +59,7 @@ let test_bad_indentation_hash = let script = Michelson_script.(find ["ill_typed"; "badly_indented"] protocol |> path) in - let* received = Client.hash_script ~script client in - let returned_hash = String.trim received in + let* returned_hash = Client.hash_script ~script client in Check.( (returned_hash = script_hash) string @@ -101,8 +100,7 @@ let test_formatted_hash = ~dst_format:`Michelson client in - let* received = Client.hash_script ~script:formatted_script client in - let returned_hash = String.trim received in + let* returned_hash = Client.hash_script ~script:formatted_script client in Check.( (returned_hash = script_hash) string diff --git a/tezt/tests/contract_hash_with_origination.ml b/tezt/tests/contract_hash_with_origination.ml index 2bb936b92964..85fcf23de1e9 100644 --- a/tezt/tests/contract_hash_with_origination.ml +++ b/tezt/tests/contract_hash_with_origination.ml @@ -54,11 +54,9 @@ let test_contract_hash_with_origination ~protocol () = client in let* received_script_hash = Client.hash_script client ~script in - let returned_script_hash = String.trim received_script_hash in let* received_contract_hash = Client.get_contract_hash client ~contract in - let returned_contract_hash = String.trim received_contract_hash in Check.( - (returned_script_hash = returned_contract_hash) + (received_script_hash = received_contract_hash) string ~__LOC__ ~error_msg:"Expected contract hash %R, got %L") ; -- GitLab From 12ac7c9e0ac05f0ebe7a9a03b6789bcd3cd9b4c5 Mon Sep 17 00:00:00 2001 From: Arvid Jakobsson Date: Thu, 24 Nov 2022 13:50:16 +0100 Subject: [PATCH 2/3] Tezt/Client: add multisig commands --- tezt/lib_tezos/client.ml | 538 ++++++++++++++++++++++++++++++++++++++ tezt/lib_tezos/client.mli | 333 +++++++++++++++++++++++ 2 files changed, 871 insertions(+) diff --git a/tezt/lib_tezos/client.ml b/tezt/lib_tezos/client.ml index 485c747998fb..15397a078c8b 100644 --- a/tezt/lib_tezos/client.ml +++ b/tezt/lib_tezos/client.ml @@ -3345,3 +3345,541 @@ let compute_chain_id_from_seed ?endpoint client seed = |> Process.check_and_read_stdout in return (String.trim output) + +let spawn_show_supported_multisig_hashes client = + spawn_command client @@ ["show"; "supported"; "multisig"; "hashes"] + +let show_supported_multisig_hashes client = + spawn_show_supported_multisig_hashes client |> Process.check + +let spawn_show_multisig_script client = + spawn_command client @@ ["show"; "multisig"; "script"] + +let show_multisig_script client = + spawn_show_multisig_script client |> Process.check + +let spawn_deploy_multisig ~new_multisig ~qty ~src ~threshold ~keys ?delegate + ?(force = false) ?burn_cap client = + spawn_command client + @@ [ + "deploy"; + "multisig"; + new_multisig; + "transferring"; + Tez.to_string qty; + "from"; + src; + "with"; + "threshold"; + string_of_int threshold; + "on"; + "public"; + "keys"; + ] + @ keys + @ optional_arg "delegate" Fun.id delegate + @ optional_switch "force" force + @ optional_arg "burn-cap" Tez.to_string burn_cap + +let deploy_multisig ~new_multisig ~qty ~src ~threshold ~keys ?delegate ?force + ?burn_cap client = + spawn_deploy_multisig + ~new_multisig + ~qty + ~src + ~threshold + ~keys + ?delegate + ?force + ?burn_cap + client + |> Process.check + +let spawn_sign_multisig_transaction_transfer ~multisig ~qty ~dst ~key ?arg + ?entrypoint client = + spawn_command client + @@ [ + "sign"; + "multisig"; + "transaction"; + "on"; + multisig; + "transferring"; + Tez.to_string qty; + "to"; + dst; + "using"; + "secret"; + "key"; + key; + ] + @ optional_arg "arg" Fun.id arg + @ optional_arg "entrypoint" Fun.id entrypoint + +let sign_multisig_transaction_transfer ~multisig ~qty ~dst ~key ?arg ?entrypoint + client = + let* client_output = + spawn_sign_multisig_transaction_transfer + ~multisig + ~qty + ~dst + ~key + ?arg + ?entrypoint + client + |> Process.check_and_read_stdout + in + return (String.trim client_output) + +let spawn_sign_multisig_transaction_run_lambda ~multisig ~lambda ~key client = + spawn_command client + @@ [ + "sign"; + "multisig"; + "transaction"; + "on"; + multisig; + "running"; + "lambda"; + lambda; + "using"; + "secret"; + "key"; + key; + ] + +let sign_multisig_transaction_run_lambda ~multisig ~lambda ~key client = + let* client_output = + spawn_sign_multisig_transaction_run_lambda ~multisig ~lambda ~key client + |> Process.check_and_read_stdout + in + return (String.trim client_output) + +let spawn_sign_multisig_transaction_set_delegate ~multisig ~dlgt ~key client = + spawn_command client + @@ [ + "sign"; + "multisig"; + "transaction"; + "on"; + multisig; + "setting"; + "delegate"; + "to"; + dlgt; + "using"; + "secret"; + "key"; + key; + ] + +let sign_multisig_transaction_set_delegate ~multisig ~dlgt ~key client = + let* client_output = + spawn_sign_multisig_transaction_set_delegate ~multisig ~dlgt ~key client + |> Process.check_and_read_stdout + in + return (String.trim client_output) + +let spawn_sign_multisig_transaction_withdraw_delegate ~multisig ~key client = + spawn_command client + @@ [ + "sign"; + "multisig"; + "transaction"; + "on"; + multisig; + "withdrawing"; + "delegate"; + "using"; + "secret"; + "key"; + key; + ] + +let sign_multisig_transaction_withdraw_delegate ~multisig ~key client = + let* client_output = + spawn_sign_multisig_transaction_withdraw_delegate ~multisig ~key client + |> Process.check_and_read_stdout + in + return (String.trim client_output) + +let spawn_sign_multisig_transaction_set_threshold_and_public_keys ~multisig + ~signing_key ~threshold ~public_keys client = + spawn_command client + @@ [ + "sign"; + "multisig"; + "transaction"; + "on"; + multisig; + "using"; + "secret"; + "key"; + signing_key; + "setting"; + "threshold"; + "to"; + string_of_int threshold; + "and"; + "public"; + "keys"; + "to"; + ] + @ public_keys + +let sign_multisig_transaction_set_threshold_and_public_keys ~multisig + ~signing_key ~threshold ~public_keys client = + let* client_output = + spawn_sign_multisig_transaction_set_threshold_and_public_keys + ~multisig + ~signing_key + ~threshold + ~public_keys + client + |> Process.check_and_read_stdout + in + return (String.trim client_output) + +let spawn_from_multisig_transfer ~multisig ~qty ~dst ~src ~signatures ?arg + ?burn_cap ?entrypoint client = + spawn_command client + @@ [ + "from"; + "multisig"; + "contract"; + multisig; + "transfer"; + Tez.to_string qty; + "to"; + dst; + "on"; + "behalf"; + "of"; + src; + "with"; + "signatures"; + ] + @ signatures + @ optional_arg "arg" Fun.id arg + @ optional_arg "burn-cap" Tez.to_string burn_cap + @ optional_arg "entrypoint" Fun.id entrypoint + +let from_multisig_transfer ~multisig ~qty ~dst ~src ~signatures ?arg ?burn_cap + ?entrypoint client = + spawn_from_multisig_transfer + ~multisig + ~qty + ~dst + ~src + ~signatures + ?arg + ?burn_cap + ?entrypoint + client + |> Process.check + +let spawn_from_multisig_run_lambda ~multisig ~lambda ~src ~signatures ?burn_cap + client = + spawn_command client + @@ [ + "from"; + "multisig"; + "contract"; + multisig; + "run"; + "lambda"; + lambda; + "on"; + "behalf"; + "of"; + src; + "with"; + "signatures"; + ] + @ signatures + @ optional_arg "burn-cap" Tez.to_string burn_cap + +let from_multisig_run_lambda ~multisig ~lambda ~src ~signatures ?burn_cap client + = + spawn_from_multisig_run_lambda + ~multisig + ~lambda + ~src + ~signatures + ?burn_cap + client + |> Process.check + +let spawn_set_delegate_of_multisig ~multisig ~dlgt ~src ~signatures ?burn_cap + client = + spawn_command client + @@ [ + "set"; + "delegate"; + "of"; + "multisig"; + "contract"; + multisig; + "to"; + dlgt; + "on"; + "behalf"; + "of"; + src; + "with"; + "signatures"; + ] + @ signatures + @ optional_arg "burn-cap" Tez.to_string burn_cap + +let set_delegate_of_multisig ~multisig ~dlgt ~src ~signatures ?burn_cap client = + spawn_set_delegate_of_multisig + ~multisig + ~dlgt + ~src + ~signatures + ?burn_cap + client + |> Process.check + +let spawn_withdraw_delegate_of_multisig ~multisig ~src ~signatures ?burn_cap + client = + spawn_command client + @@ [ + "withdraw"; + "delegate"; + "of"; + "multisig"; + "contract"; + multisig; + "on"; + "behalf"; + "of"; + src; + "with"; + "signatures"; + ] + @ signatures + @ optional_arg "burn-cap" Tez.to_string burn_cap + +let withdraw_delegate_of_multisig ~multisig ~src ~signatures ?burn_cap client = + spawn_withdraw_delegate_of_multisig + ~multisig + ~src + ~signatures + ?burn_cap + client + |> Process.check + +let spawn_set_threshold_of_multisig ~multisig ~threshold ~public_keys ~src + ~signatures ?burn_cap client = + spawn_command client + @@ [ + "set"; + "threshold"; + "of"; + "multisig"; + "contract"; + multisig; + "to"; + string_of_int threshold; + "and"; + "public"; + "keys"; + "to"; + ] + @ public_keys + @ ["on"; "behalf"; "of"; src; "with"; "signatures"] + @ signatures + @ optional_arg "burn-cap" Tez.to_string burn_cap + +let set_threshold_of_multisig ~multisig ~threshold ~public_keys ~src ~signatures + ?burn_cap client = + spawn_set_threshold_of_multisig + ~multisig + ~threshold + ~public_keys + ~src + ~signatures + ?burn_cap + client + |> Process.check + +let spawn_run_transaction_on_multisig ~bytes ~multisig ~src ~signatures + ?burn_cap client = + spawn_command client + @@ [ + "run"; + "transaction"; + bytes; + "on"; + "multisig"; + "contract"; + multisig; + "on"; + "behalf"; + "of"; + src; + "with"; + "signatures"; + ] + @ signatures + @ optional_arg "burn-cap" Tez.to_string burn_cap + +let run_transaction_on_multisig ~bytes ~multisig ~src ~signatures ?burn_cap + client = + spawn_run_transaction_on_multisig + ~bytes + ~multisig + ~src + ~signatures + ?burn_cap + client + |> Process.check + +let spawn_prepare_multisig_transaction ~multisig ~qty ~dst ?arg ?entrypoint + ?(bytes_only = false) client = + spawn_command client + @@ [ + "prepare"; + "multisig"; + "transaction"; + "on"; + multisig; + "transferring"; + Tez.to_string qty; + "to"; + dst; + ] + @ optional_arg "arg" Fun.id arg + @ optional_arg "entrypoint" Fun.id entrypoint + @ optional_switch "bytes-only" bytes_only + +let prepare_multisig_transaction ~multisig ~qty ~dst ?arg ?entrypoint + ?bytes_only client = + let* client_output = + spawn_prepare_multisig_transaction + ~multisig + ~qty + ~dst + ?arg + ?entrypoint + ?bytes_only + client + |> Process.check_and_read_stdout + in + return (String.trim client_output) + +let spawn_prepare_multisig_transaction_run_lambda ~multisig ~lambda + ?(bytes_only = false) client = + spawn_command client + @@ [ + "prepare"; + "multisig"; + "transaction"; + "on"; + multisig; + "running"; + "lambda"; + lambda; + ] + @ optional_switch "bytes-only" bytes_only + +let prepare_multisig_transaction_run_lambda ~multisig ~lambda ?bytes_only client + = + let* client_output = + spawn_prepare_multisig_transaction_run_lambda + ~multisig + ~lambda + ?bytes_only + client + |> Process.check_and_read_stdout + in + return (String.trim client_output) + +let spawn_prepare_multisig_transaction_set_delegate ~multisig ~dlgt + ?(bytes_only = false) client = + spawn_command client + @@ [ + "prepare"; + "multisig"; + "transaction"; + "on"; + multisig; + "setting"; + "delegate"; + "to"; + dlgt; + ] + @ optional_switch "bytes-only" bytes_only + +let prepare_multisig_transaction_set_delegate ~multisig ~dlgt ?bytes_only client + = + let* client_output = + spawn_prepare_multisig_transaction_set_delegate + ~multisig + ~dlgt + ?bytes_only + client + |> Process.check_and_read_stdout + in + return (String.trim client_output) + +let spawn_prepare_multisig_transaction_withdraw_delegate ~multisig + ?(bytes_only = false) client = + spawn_command client + @@ [ + "prepare"; + "multisig"; + "transaction"; + "on"; + multisig; + "withdrawing"; + "delegate"; + ] + @ optional_switch "bytes-only" bytes_only + +let prepare_multisig_transaction_withdraw_delegate ~multisig ?bytes_only client + = + let* client_output = + spawn_prepare_multisig_transaction_withdraw_delegate + ~multisig + ?bytes_only + client + |> Process.check_and_read_stdout + in + return (String.trim client_output) + +let spawn_prepare_multisig_transaction_set_threshold_and_public_keys ~multisig + ~threshold ~public_keys ?(bytes_only = false) client = + spawn_command client + @@ [ + "prepare"; + "multisig"; + "transaction"; + "on"; + multisig; + "setting"; + "threshold"; + "to"; + string_of_int threshold; + "and"; + "public"; + "keys"; + "to"; + ] + @ public_keys + @ optional_switch "bytes-only" bytes_only + +let prepare_multisig_transaction_set_threshold_and_public_keys ~multisig + ~threshold ~public_keys ?bytes_only client = + let* client_output = + spawn_prepare_multisig_transaction_set_threshold_and_public_keys + ~multisig + ~threshold + ~public_keys + ?bytes_only + client + |> Process.check_and_read_stdout + in + return (String.trim client_output) diff --git a/tezt/lib_tezos/client.mli b/tezt/lib_tezos/client.mli index 64d578bfb396..c626e767fdd0 100644 --- a/tezt/lib_tezos/client.mli +++ b/tezt/lib_tezos/client.mli @@ -2470,3 +2470,336 @@ val compute_chain_id_from_seed : (** Same as [compute_chain_id_from_seed], but do not wait for the process to exit. *) val spawn_compute_chain_id_from_seed : ?endpoint:endpoint -> t -> string -> Process.t + +(** {2 Commands for managing a multisig smart contract} *) + +(** Run [octez-client show supported multisig hashes]. *) +val show_supported_multisig_hashes : t -> unit Lwt.t + +(** Same as [show_supported_multisig_hashes], but do not wait for the + process to exit. *) +val spawn_show_supported_multisig_hashes : t -> Process.t + +(** Run [octez-client show multisig script]. *) +val show_multisig_script : t -> unit Lwt.t + +(** Same as [show_multisig_script], but do not wait for the process to + exit. *) +val spawn_show_multisig_script : t -> Process.t + +(** Run [octez-client deploy multisig transferring + from with threshold on public keys + ]. *) +val deploy_multisig : + new_multisig:string -> + qty:Tez.t -> + src:string -> + threshold:int -> + keys:string list -> + ?delegate:string -> + ?force:bool -> + ?burn_cap:Tez.t -> + t -> + unit Lwt.t + +(** Same as [deploy_multisig], but do not wait for the process to exit. *) +val spawn_deploy_multisig : + new_multisig:string -> + qty:Tez.t -> + src:string -> + threshold:int -> + keys:string list -> + ?delegate:string -> + ?force:bool -> + ?burn_cap:Tez.t -> + t -> + Process.t + +(** Run [octez-client sign multisig transaction on + transferring to using secret key ]. *) +val sign_multisig_transaction_transfer : + multisig:string -> + qty:Tez.t -> + dst:string -> + key:string -> + ?arg:string -> + ?entrypoint:string -> + t -> + string Lwt.t + +(** Same as [sign_multisig_transaction_transfer], + but do not wait for the process to exit. *) +val spawn_sign_multisig_transaction_transfer : + multisig:string -> + qty:Tez.t -> + dst:string -> + key:string -> + ?arg:string -> + ?entrypoint:string -> + t -> + Process.t + +(** Run [octez-client sign multisig transaction on running + lambda using secret key ]. *) +val sign_multisig_transaction_run_lambda : + multisig:string -> lambda:string -> key:string -> t -> string Lwt.t + +(** Same as [sign_multisig_transaction_run_lambda], + but do not wait for the process to exit. *) +val spawn_sign_multisig_transaction_run_lambda : + multisig:string -> lambda:string -> key:string -> t -> Process.t + +(** Run [octez-client sign multisig transaction on setting + delegate to using secret key ]. *) +val sign_multisig_transaction_set_delegate : + multisig:string -> dlgt:string -> key:string -> t -> string Lwt.t + +(** Same as [sign_multisig_transaction_set_delegate], + but do not wait for the process to exit. *) +val spawn_sign_multisig_transaction_set_delegate : + multisig:string -> dlgt:string -> key:string -> t -> Process.t + +(** Run [octez-client sign multisig transaction on + withdrawing delegate using secret key ]. *) +val sign_multisig_transaction_withdraw_delegate : + multisig:string -> key:string -> t -> string Lwt.t + +(** Same as [sign_multisig_transaction_withdraw_delegate], + but do not wait for the process to exit. *) +val spawn_sign_multisig_transaction_withdraw_delegate : + multisig:string -> key:string -> t -> Process.t + +(** Run [octez-client sign multisig transaction on using + secret key setting threshold to and public keys + to ]. *) +val sign_multisig_transaction_set_threshold_and_public_keys : + multisig:string -> + signing_key:string -> + threshold:int -> + public_keys:string list -> + t -> + string Lwt.t + +(** Same as [sign_multisig_transaction_set_threshold_and_public_keys], + but do not wait for the process to exit. *) +val spawn_sign_multisig_transaction_set_threshold_and_public_keys : + multisig:string -> + signing_key:string -> + threshold:int -> + public_keys:string list -> + t -> + Process.t + +(** Run [octez-client from multisig contract transfer + to on behalf of with signatures ]. *) +val from_multisig_transfer : + multisig:string -> + qty:Tez.t -> + dst:string -> + src:string -> + signatures:string list -> + ?arg:string -> + ?burn_cap:Tez.t -> + ?entrypoint:string -> + t -> + unit Lwt.t + +(** Same as [from_multisig_transfer], + but do not wait for the process to exit. *) +val spawn_from_multisig_transfer : + multisig:string -> + qty:Tez.t -> + dst:string -> + src:string -> + signatures:string list -> + ?arg:string -> + ?burn_cap:Tez.t -> + ?entrypoint:string -> + t -> + Process.t + +(** Run [octez-client from multisig contract run lambda + on behalf of with signatures ]. *) +val from_multisig_run_lambda : + multisig:string -> + lambda:string -> + src:string -> + signatures:string list -> + ?burn_cap:Tez.t -> + t -> + unit Lwt.t + +(** Same as [from_multisig_run_lambda], + but do not wait for the process to exit. *) +val spawn_from_multisig_run_lambda : + multisig:string -> + lambda:string -> + src:string -> + signatures:string list -> + ?burn_cap:Tez.t -> + t -> + Process.t + +(** Run [octez-client set delegate of multisig contract to + on behalf of with signatures ]. *) +val set_delegate_of_multisig : + multisig:string -> + dlgt:string -> + src:string -> + signatures:string list -> + ?burn_cap:Tez.t -> + t -> + unit Lwt.t + +(** Same as [set_delegate_of_multisig], + but do not wait for the process to exit. *) +val spawn_set_delegate_of_multisig : + multisig:string -> + dlgt:string -> + src:string -> + signatures:string list -> + ?burn_cap:Tez.t -> + t -> + Process.t + +(** Run [octez-client withdraw delegate of multisig contract + on behalf of with signatures ]. *) +val withdraw_delegate_of_multisig : + multisig:string -> + src:string -> + signatures:string list -> + ?burn_cap:Tez.t -> + t -> + unit Lwt.t + +(** Same as [withdraw_delegate_of_multisig], + but do not wait for the process to exit. *) +val spawn_withdraw_delegate_of_multisig : + multisig:string -> + src:string -> + signatures:string list -> + ?burn_cap:Tez.t -> + t -> + Process.t + +(** Run [octez-client set threshold of multisig contract to + and public keys to on behalf of with + signatures ]. *) +val set_threshold_of_multisig : + multisig:string -> + threshold:int -> + public_keys:string list -> + src:string -> + signatures:string list -> + ?burn_cap:Tez.t -> + t -> + unit Lwt.t + +(** Same as [set_threshold_of_multisig], + but do not wait for the process to exit. *) +val spawn_set_threshold_of_multisig : + multisig:string -> + threshold:int -> + public_keys:string list -> + src:string -> + signatures:string list -> + ?burn_cap:Tez.t -> + t -> + Process.t + +(** Run [octez-client run transaction on multisig contract + on behalf of with signatures ]. *) +val run_transaction_on_multisig : + bytes:string -> + multisig:string -> + src:string -> + signatures:string list -> + ?burn_cap:Tez.t -> + t -> + unit Lwt.t + +(** Same as [run_transaction_on_multisig], + but do not wait for the process to exit. *) +val spawn_run_transaction_on_multisig : + bytes:string -> + multisig:string -> + src:string -> + signatures:string list -> + ?burn_cap:Tez.t -> + t -> + Process.t + +(** Run [octez-client prepare multisig transaction on + transferring to ]. *) +val prepare_multisig_transaction : + multisig:string -> + qty:Tez.t -> + dst:string -> + ?arg:string -> + ?entrypoint:string -> + ?bytes_only:bool -> + t -> + string Lwt.t + +(** Same as [prepare_multisig_transaction], but do + not wait for the process to exit. *) +val spawn_prepare_multisig_transaction : + multisig:string -> + qty:Tez.t -> + dst:string -> + ?arg:string -> + ?entrypoint:string -> + ?bytes_only:bool -> + t -> + Process.t + +(** Run [octez-client prepare multisig transaction on + running lambda ]. *) +val prepare_multisig_transaction_run_lambda : + multisig:string -> lambda:string -> ?bytes_only:bool -> t -> string Lwt.t + +(** Same as [prepare_multisig_transaction_run_lambda], but do + not wait for the process to exit. *) +val spawn_prepare_multisig_transaction_run_lambda : + multisig:string -> lambda:string -> ?bytes_only:bool -> t -> Process.t + +(** Run [octez-client prepare multisig transaction on + setting delegate to ]. *) +val prepare_multisig_transaction_set_delegate : + multisig:string -> dlgt:string -> ?bytes_only:bool -> t -> string Lwt.t + +(** Same as [prepare_multisig_transaction_set_delegate], but + do not wait for the process to exit. *) +val spawn_prepare_multisig_transaction_set_delegate : + multisig:string -> dlgt:string -> ?bytes_only:bool -> t -> Process.t + +(** Run [octez-client prepare multisig transaction on + withdrawing delegate]. *) +val prepare_multisig_transaction_withdraw_delegate : + multisig:string -> ?bytes_only:bool -> t -> string Lwt.t + +(** Same as [prepare_multisig_transaction_withdraw_delegate], + but do not wait for the process to exit. *) +val spawn_prepare_multisig_transaction_withdraw_delegate : + multisig:string -> ?bytes_only:bool -> t -> Process.t + +(** Run [octez-client prepare multisig transaction on + setting threshold to and public keys to ]. *) +val prepare_multisig_transaction_set_threshold_and_public_keys : + multisig:string -> + threshold:int -> + public_keys:string list -> + ?bytes_only:bool -> + t -> + string Lwt.t + +(** Same as + [prepare_multisig_transaction_set_threshold_and_public_keys], + but do not wait for the process to exit. *) +val spawn_prepare_multisig_transaction_set_threshold_and_public_keys : + multisig:string -> + threshold:int -> + public_keys:string list -> + ?bytes_only:bool -> + t -> + Process.t -- GitLab From c3e0fde0a87b38ff0503f162087b0c80c396ac52 Mon Sep 17 00:00:00 2001 From: Arvid Jakobsson Date: Thu, 24 Nov 2022 13:51:53 +0100 Subject: [PATCH 3/3] Tezt: migrate [test_multisig.py] --- .../multisig_dest_entrypoint.tz | 3 + .../multisig_dest_entrypoint_arg.tz | 3 + tests_python/tests_015/test_multisig.py | 617 -------------- tests_python/tests_016/test_multisig.py | 617 -------------- tests_python/tests_alpha/test_multisig.py | 617 -------------- tezt/tests/main.ml | 1 + tezt/tests/multisig.ml | 791 ++++++++++++++++++ 7 files changed, 798 insertions(+), 1851 deletions(-) create mode 100644 michelson_test_scripts/mini_scenarios/multisig_dest_entrypoint.tz create mode 100644 michelson_test_scripts/mini_scenarios/multisig_dest_entrypoint_arg.tz delete mode 100644 tests_python/tests_015/test_multisig.py delete mode 100644 tests_python/tests_016/test_multisig.py delete mode 100644 tests_python/tests_alpha/test_multisig.py create mode 100644 tezt/tests/multisig.ml diff --git a/michelson_test_scripts/mini_scenarios/multisig_dest_entrypoint.tz b/michelson_test_scripts/mini_scenarios/multisig_dest_entrypoint.tz new file mode 100644 index 000000000000..0c604a4355c0 --- /dev/null +++ b/michelson_test_scripts/mini_scenarios/multisig_dest_entrypoint.tz @@ -0,0 +1,3 @@ +parameter (or (unit %a) (string %b)); +storage unit; +code {CDR; NIL operation; PAIR} diff --git a/michelson_test_scripts/mini_scenarios/multisig_dest_entrypoint_arg.tz b/michelson_test_scripts/mini_scenarios/multisig_dest_entrypoint_arg.tz new file mode 100644 index 000000000000..ea276d39ff8b --- /dev/null +++ b/michelson_test_scripts/mini_scenarios/multisig_dest_entrypoint_arg.tz @@ -0,0 +1,3 @@ +parameter (or (int %a) (string %b)); +storage unit; +code {CDR; NIL operation; PAIR} diff --git a/tests_python/tests_015/test_multisig.py b/tests_python/tests_015/test_multisig.py deleted file mode 100644 index cc234f4260b1..000000000000 --- a/tests_python/tests_015/test_multisig.py +++ /dev/null @@ -1,617 +0,0 @@ -# octez-client has builtin support for multisig smart contracts. See -# docs/user/multisig.rst for more details about it. - -# This file tests the client multisig support; more precisely it tests -# that both the generic and the legacy versions of the multisig smart -# contract behave as intended. For all commands, we check that we can -# interact with the multisig contract when invoking it by its address -# or by its alias. - -import re -from typing import List -import pytest -from tools import utils, constants -from client.client import Client -from .contract_paths import find_script - - -def get_keys(client): - """Generate 3 pairs of keys using various schemes and return the list - of aliases.""" - keys = ['foo', 'bar', 'boo'] - sigs = [None, 'secp256k1', 'ed25519'] - for key, sig in zip(keys, sigs): - args = [] if sig is None else ['--sig', sig] - client.gen_key(key, args) - return keys - - -@pytest.fixture(scope="class") -def keys(client): - return get_keys(client) - - -def msig_path(msig_version: str) -> str: - return find_script(['mini_scenarios', f'{msig_version}_multisig']) - - -MSIG_PARAMS = [ - {'by_address': by_address, 'msig_version': msig_version} - for msig_version in ['generic', 'legacy'] - for by_address in [True, False] -] - - -def parse_msig_storage(storage: str): - """Parse the storage of a multisig contract to get its counter (as a - number), threshold (as a number), and the keys of the signers (as - Micheline sequence in a string).""" - # put everything on a single line - storage = ' '.join(storage.split('\n')) - storage_regexp = r'Pair\s+?([0-9]+)\s+?([0-9]+)\s+?(.*)\s*' - match = re.search(storage_regexp, storage) - assert match is not None - return { - 'counter': int(match[1]), - 'threshold': int(match[2]), - 'keys': match[3], - } - - -def resolve_key_alias(client: Client, alias: str) -> str: - """Convert a key alias into a public key that can be understood in - Michelson.""" - ret = client.show_address(alias).public_key - assert ret is not None - return ret - - -def build_michelson_key_list(client: Client, keys: List[str]): - """From a list of key aliases, build a Michelson list of public keys.""" - keys = [resolve_key_alias(client, k) for k in keys] - return '{"' + '"; "'.join(keys) + '"}' - - -def build_msig_storage( - client: Client, counter: int, threshold: int, keys: List[str] -): - """Build a multisig storage from its components: a counter, a - threshold and the list of signer public keys.""" - keys = build_michelson_key_list(client, keys) - return f'Pair {counter} {threshold} {keys}' - - -def assert_michelson_eq(client, data1, data2, typ): - """Check that two Michelson expressions of the same type are equal by - normalizing them.""" - normalized1 = client.normalize(data=data1, typ=typ) - normalized2 = client.normalize(data=data2, typ=typ) - assert normalized1 == normalized2 - - -def assert_msig_storage_eq(client, data1, data2): - """Check that two multisig storages are equal.""" - assert_michelson_eq(client, data1, data2, 'pair nat nat (list key)') - - -def assert_msig_counter_incr(current_storage, new_storage): - """Check that [new_storage] is the same multisig storage than - [current_storage] except that it uses the next counter.""" - current_storage = parse_msig_storage(current_storage) - new_storage = parse_msig_storage(new_storage) - assert new_storage['counter'] == 1 + current_storage['counter'] - assert new_storage['threshold'] == current_storage['threshold'] - assert new_storage['keys'] == current_storage['keys'] - - -@pytest.fixture(scope="class", params=MSIG_PARAMS) -def msig(client: Client, keys, request): - """This fixture originates a multisig contract with a threshold of 2 - and the keys given as parameter. The version of the script is given by - the msig_version parameter, it can be either 'generic' or - 'legacy'. This fixture returns a dictionary containing: - - - a [handle] that can be used to interact with the contract: - either the address of the script or an alias, depending on the - [by_address] parameter - - - the list of [keys] that are stored in the contract which is a - copy of the [keys] parameter - - - the [version], which is a copy of the [msig_version] parameter - - """ - - # We use the same alias for all multisig originations, this makes - # testing simpler but requires using the '--force' option. - msig_alias = 'msig' - msig_version = request.param['msig_version'] - by_address = request.param['by_address'] - initial_storage = build_msig_storage( - client=client, counter=0, threshold=2, keys=keys - ) - deployment = client.originate( - msig_alias, - 100, - 'bootstrap1', - msig_path(msig_version), - # Initialize with empty key list and null threshold - ['--init', initial_storage, '--burn-cap', '100', '--force'], - ) - utils.bake(client) - handle = deployment.contract if by_address else msig_alias - return {'handle': handle, 'keys': keys, 'version': msig_version} - - -@pytest.mark.incremental -class TestMultisig: - def test_deploy_multisig(self, msig, client: Client): - """Test that: - - the script originated by the "deploy multisig" command, - - the generic_multisig.tz script found in the mini_scenarios - directory, and - - the script printed by the "show multisig script" - are the same.""" - keys = msig['keys'] - - # The command cannot originate the legacy contract so there is - # nothing to test in the legacy case. - if msig['version'] == 'generic': - client.deploy_msig( - 'dummy_msig', - 100, - 'bootstrap1', - 2, - keys, - ['--burn-cap', '100', '--force'], - ) - utils.bake(client) - expected_hash = ( - 'exprub9UzpxmhedNQnsv1J1DazWGJnj1dLhtG1fxkUoWSdFLBGLqJ4' - ) - assert expected_hash in client.run( - ['show', 'supported', 'multisig', 'hashes'] - ) - assert client.get_script_hash(msig['handle']) == expected_hash - assert client.get_script_hash('dummy_msig') == expected_hash - assert client.hash_script( - [client.run(['show', 'multisig', 'script'])] - ) == [(expected_hash, None)] - assert client.get_balance('dummy_msig') == 100 - - def test_transfer(self, msig, client: Client, session: dict): - """Test the client command for signing a multisig transfer from key - number 0 and store the signature in the session.""" - keys = msig['keys'] - key = keys[0] - session['sig0'] = client.msig_sign_transfer( - msig['handle'], 10, 'bootstrap2', key - ) - - def test_prepare_msig_transfer(self, msig, client: Client): - """Test the client command for preparing a transfer. The result of the - command is ignored in this test, we only test that the command - succeeds.""" - client.msig_prepare_transfer(msig['handle'], 10, 'bootstrap2') - - def test_prepare_msig_sign(self, msig, client: Client, session: dict): - """Produce signatures for keys number 1 and 2 using the the - preparation command together with the sign_bytes client command. The - signatures are stored in the session.""" - to_sign = client.msig_prepare_transfer( - msig['handle'], 10, 'bootstrap2', ['--bytes-only'] - ) - session['sig1'] = client.sign_bytes_of_string(to_sign, msig['keys'][1]) - session['sig2'] = client.sign_bytes_of_string(to_sign, msig['keys'][2]) - - def test_transfer_failure(self, msig, client: Client, session: dict): - """Test transfer failure when there are too few signatures.""" - error_pattern = ( - r"Not enough signatures: " - + r"only 1 signatures were given " - + r"but the threshold is currently 2" - ) - - with utils.assert_run_failure(error_pattern): - client.msig_transfer( - msig['handle'], - 10, - 'bootstrap2', - 'bootstrap1', - [session['sig2']], - ) - - def test_transfer_success(self, msig, client: Client, session: dict): - """Test a successful transfer using signatures obtained by different - methods. The signatures are taken from the session.""" - current_storage = client.get_storage(msig['handle']) - current_balance = client.get_balance(msig['handle']) - - client.msig_transfer( - msig['handle'], - 10, - 'bootstrap2', - 'bootstrap1', - [session['sig0'], session['sig2']], - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - assert_msig_counter_incr(current_storage, new_storage) - new_balance = client.get_balance(msig['handle']) - assert new_balance == current_balance - 10 - - def test_default_entrypoint(self, msig, client): - """The generic multisig contract features an unauthorized default - entrypoint to receive donations but the legacy one does not.""" - - def cmd(): - client.transfer( - amount=100, giver='bootstrap1', receiver=msig['handle'] - ) - - if msig['version'] == 'legacy': - error_pattern = r'Invalid argument passed to contract' - with utils.assert_run_failure(error_pattern): - cmd() - else: - current_storage = client.get_storage(msig['handle']) - current_balance = client.get_balance(msig['handle']) - cmd() - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - new_balance = client.get_balance(msig['handle']) - assert new_storage == current_storage - assert new_balance == current_balance + 100 - - def test_transfer_with_entrypoint(self, msig, client: Client): - """Both versions of the contract can call arbitrary entrypoints of - type unit. This test uses the two possible methods to produce the - signatures.""" - current_storage = client.get_storage(msig['handle']) - current_balance = client.get_balance(msig['handle']) - contract = ( - 'parameter (or (unit %a) (string %b)); ' - 'storage unit; ' - 'code {CDR; NIL operation; PAIR}' - ) - client.originate( - 'dest_entrypoint', - 0, - 'bootstrap1', - contract, - args=['--burn-cap', '10.0', '--force'], - ) - args = ['--entrypoint', 'a'] - utils.bake(client) - to_sign = client.msig_prepare_transfer( - msig_name=msig['handle'], - amount=10, - dest='dest_entrypoint', - args=args + ['--bytes-only'], - ) - sig0 = client.sign_bytes_of_string(to_sign, msig['keys'][0]) - sig2 = client.msig_sign_transfer( - msig_name=msig['handle'], - amount=10, - dest='dest_entrypoint', - secret_key=msig['keys'][2], - args=args, - ) - client.msig_transfer( - msig_name=msig['handle'], - amount=10, - dest='dest_entrypoint', - src='bootstrap1', - signatures=[sig0, sig2], - args=args, - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - new_balance = client.get_balance(msig['handle']) - assert_msig_counter_incr(current_storage, new_storage) - assert new_balance == current_balance - 10 - - def test_transfer_with_arg(self, msig, client: Client): - """The generic multisig contract can call other contracts with - arbitrary parameters but the legacy one can only send Unit.""" - contract = ( - 'parameter (or (int %a) (string %b)); ' - 'storage unit; ' - 'code {CDR; NIL operation; PAIR}' - ) - client.originate( - 'dest', - 0, - 'bootstrap1', - contract, - args=['--burn-cap', '10.0', '--force'], - ) - args = ['--entrypoint', 'a', '--arg', '42'] - utils.bake(client) - - def cmd(): - return client.msig_prepare_transfer( - msig_name=msig['handle'], - amount=10, - dest='dest', - args=args + ['--bytes-only'], - ) - - if msig['version'] == 'legacy': - error_pattern = ( - r'This multisig contract can only transfer tokens to' - ' contracts of type unit; calling a contract with argument 42' - ' is not supported.' - ) - with utils.assert_run_failure(error_pattern): - cmd() - else: - current_storage = client.get_storage(msig['handle']) - current_balance = client.get_balance(msig['handle']) - to_sign = cmd() - utils.bake(client) - sig0 = client.sign_bytes_of_string(to_sign, msig['keys'][0]) - sig2 = client.msig_sign_transfer( - msig_name=msig['handle'], - amount=10, - dest='dest', - secret_key=msig['keys'][2], - args=args, - ) - client.msig_transfer( - msig_name=msig['handle'], - amount=10, - dest='dest', - src='bootstrap1', - signatures=[sig0, sig2], - args=args, - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - new_balance = client.get_balance(msig['handle']) - assert_msig_counter_incr(current_storage, new_storage) - assert new_balance == current_balance - 10 - - def test_transfer_ill_typed(self, msig, client: Client): - """Test that the multisig transfer preparation command type checks the - parameter.""" - error_pattern = ( - ( - r'The entrypoint b of contract .* ' - 'called from a multisig contract is of type string; ' - 'the provided parameter 42 is ill-typed.' - ) - if msig['version'] == 'generic' - else ( - r'This multisig contract can only transfer tokens to' - ' contracts of type unit; calling a contract with argument 42' - ' is not supported.' - ) - ) - - args = ['--entrypoint', 'b', '--arg', '42'] - - def cmd(): - client.msig_prepare_transfer( - msig_name=msig['handle'], - amount=10, - dest='dest', - args=args + ['--bytes-only'], - ) - - with utils.assert_run_failure(error_pattern): - cmd() - - def test_transfer_too_high(self, msig, client: Client): - """Test that the multisig transfer preparation command checks the - balance.""" - expected_warning = ( - 'Transferred amount is bigger than current multisig balance' - ) - - client.msig_prepare_transfer( - msig_name=msig['handle'], - amount=1000, - dest='bootstrap1', - args=['--bytes-only'], - expected_warning=expected_warning, - ) - - def test_multiple_operations(self, msig, client: Client): - """The generic multisig client can run lambdas, this can be used to - atomically run several operations.""" - bootstrap1_address = constants.IDENTITIES['bootstrap1']['identity'] - bootstrap2_address = constants.IDENTITIES['bootstrap2']['identity'] - bootstrap3_address = constants.IDENTITIES['bootstrap3']['identity'] - lam = ( - '{ DROP; NIL operation; ' - f'PUSH key_hash "{bootstrap1_address}"; IMPLICIT_ACCOUNT; ' - 'PUSH mutez 1000000; UNIT; TRANSFER_TOKENS; CONS; ' - f'PUSH key_hash "{bootstrap2_address}"; IMPLICIT_ACCOUNT; ' - 'PUSH mutez 2000000; UNIT; TRANSFER_TOKENS; CONS; ' - f'PUSH key_hash "{bootstrap3_address}"; SOME; ' - 'SET_DELEGATE; CONS}' - ) - lam = client.normalize( - lam, typ='lambda unit (list operation)', mode='Optimized' - ) - - def cmd(): - return client.msig_prepare_lambda( - msig_name=msig['handle'], lam=lam, args=['--bytes-only'] - ) - - if msig['version'] == 'legacy': - error_pattern = 'This multisig contract has a fixed set of actions' - with utils.assert_run_failure(error_pattern): - cmd() - else: - current_storage = client.get_storage(msig['handle']) - current_balance = client.get_balance(msig['handle']) - to_sign = cmd() - sig0 = client.sign_bytes_of_string(to_sign, msig['keys'][0]) - sig2 = client.msig_sign_lambda( - msig_name=msig['handle'], lam=lam, secret_key=msig['keys'][2] - ) - client.msig_run_lambda( - msig_name=msig['handle'], - lam=lam, - src='bootstrap1', - signatures=[sig0, sig2], - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - new_balance = client.get_balance(msig['handle']) - assert_msig_counter_incr(current_storage, new_storage) - assert new_balance == current_balance - 3 - # TODO: check the delegate change - - def test_multiple_operations_failure(self, msig, client: Client): - """Test for the error message for ill-typed lambdas.""" - lam = '{ DROP }' - - def cmd(): - client.msig_prepare_lambda( - msig_name=msig['handle'], lam=lam, args=['--bytes-only'] - ) - - error_pattern = ( - ( - r'The provided lambda .* for multisig contract' - r' is ill-typed; .* is expected.' - ) - if msig['version'] == 'generic' - else 'This multisig contract has a fixed set of actions' - ) - - with utils.assert_run_failure(error_pattern): - cmd() - - def test_delegate_change(self, msig, client: Client): - """Test the multisig command for changing delegate.""" - current_storage = client.get_storage(msig['handle']) - current_balance = client.get_balance(msig['handle']) - sig0 = client.msig_sign_set_delegate( - msig['handle'], 'bootstrap5', msig['keys'][0] - ) - to_sign = client.msig_prepare_set_delegate( - msig['handle'], 'bootstrap5', ['--bytes-only'] - ) - sig2 = client.sign_bytes_of_string(to_sign, msig['keys'][2]) - client.msig_set_delegate( - msig['handle'], 'bootstrap5', 'bootstrap1', [sig0, sig2] - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - new_balance = client.get_balance(msig['handle']) - assert_msig_counter_incr(current_storage, new_storage) - assert new_balance == current_balance - - def test_delegate_withdraw(self, msig, client: Client): - """Test the multisig command for removing delegation.""" - current_storage = client.get_storage(msig['handle']) - current_balance = client.get_balance(msig['handle']) - sig0 = client.msig_sign_withdrawing_delegate( - msig['handle'], msig['keys'][0] - ) - to_sign = client.msig_prepare_withdrawing_delegate( - msig['handle'], ['--bytes-only'] - ) - - sig1 = client.sign_bytes_of_string(to_sign, msig['keys'][1]) - client.msig_withdrawing_delegate( - msig['handle'], 'bootstrap1', [sig0, sig1] - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - new_balance = client.get_balance(msig['handle']) - assert_msig_counter_incr(current_storage, new_storage) - assert new_balance == current_balance - - def test_run_transaction_change_keys_and_threshold( - self, msig, client: Client - ): - """Test changing the keys and threshold with the `run transaction` - command.""" - current_storage = client.get_storage(msig['handle']) - current_counter = parse_msig_storage(storage=current_storage)['counter'] - current_balance = client.get_balance(msig['handle']) - keys = msig['keys'] - sig0 = client.msig_sign_setting_threshold( - msig['handle'], keys[0], 2, [keys[0], keys[2]] - ) - to_sign = client.msig_prepare_setting_threshold( - msig['handle'], 2, [keys[0], keys[2]], ['--bytes-only'] - ) - sig2 = client.sign_bytes_of_string(to_sign, msig['keys'][2]) - client.msig_run_transaction( - msig['handle'], to_sign, 'bootstrap1', [sig0, sig2] - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - expected_counter = 1 + current_counter - expected_storage = build_msig_storage( - client=client, - counter=expected_counter, - threshold=2, - keys=[keys[0], keys[2]], - ) - assert_msig_storage_eq(client, new_storage, expected_storage) - new_balance = client.get_balance(msig['handle']) - assert new_balance == current_balance - - def test_change_keys_and_threshold(self, msig, client: Client): - """Test changing the keys and threshold with `set threshold of - multisig` command.""" - current_storage = client.get_storage(msig['handle']) - current_counter = parse_msig_storage(storage=current_storage)['counter'] - current_balance = client.get_balance(msig['handle']) - keys = msig['keys'] - new_keys = [keys[0], keys[2]] - sig0 = client.msig_sign_setting_threshold( - msig['handle'], keys[0], 2, new_keys - ) - to_sign = client.msig_prepare_setting_threshold( - msig['handle'], 2, new_keys, ['--bytes-only'] - ) - sig2 = client.sign_bytes_of_string(to_sign, msig['keys'][2]) - client.msig_set_threshold( - msig['handle'], 2, new_keys, 'bootstrap1', [sig0, sig2] - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - expected_counter = 1 + current_counter - expected_storage = build_msig_storage( - client=client, - counter=expected_counter, - threshold=2, - keys=[keys[0], keys[2]], - ) - assert_msig_storage_eq(client, new_storage, expected_storage) - new_balance = client.get_balance(msig['handle']) - assert new_balance == current_balance - - -class TestUnsupportedMultisig: - """Verify that non-multisig contracts are rejected""" - - def test_deploy_nonmultisig(self, client: Client): - contract = find_script(['attic', 'id']) - client.originate( - 'id', - 0, - 'bootstrap1', - contract, - args=['--burn-cap', '10.0', '--force', '--init', '""'], - ) - utils.bake(client) - - error_pattern = ( - 'The hash of this script is ' - 'exprv8K6ceBpFH5SFjQm4BRYSLJCHQBFeQU6BFTdvQSRPaPkzdLyAL, ' - 'it was not found among in the list of known multisig ' - 'script hashes.' - ) - - with utils.assert_run_failure(error_pattern): - client.msig_transfer('id', 10, 'bootstrap2', 'bootstrap1', []) diff --git a/tests_python/tests_016/test_multisig.py b/tests_python/tests_016/test_multisig.py deleted file mode 100644 index cc234f4260b1..000000000000 --- a/tests_python/tests_016/test_multisig.py +++ /dev/null @@ -1,617 +0,0 @@ -# octez-client has builtin support for multisig smart contracts. See -# docs/user/multisig.rst for more details about it. - -# This file tests the client multisig support; more precisely it tests -# that both the generic and the legacy versions of the multisig smart -# contract behave as intended. For all commands, we check that we can -# interact with the multisig contract when invoking it by its address -# or by its alias. - -import re -from typing import List -import pytest -from tools import utils, constants -from client.client import Client -from .contract_paths import find_script - - -def get_keys(client): - """Generate 3 pairs of keys using various schemes and return the list - of aliases.""" - keys = ['foo', 'bar', 'boo'] - sigs = [None, 'secp256k1', 'ed25519'] - for key, sig in zip(keys, sigs): - args = [] if sig is None else ['--sig', sig] - client.gen_key(key, args) - return keys - - -@pytest.fixture(scope="class") -def keys(client): - return get_keys(client) - - -def msig_path(msig_version: str) -> str: - return find_script(['mini_scenarios', f'{msig_version}_multisig']) - - -MSIG_PARAMS = [ - {'by_address': by_address, 'msig_version': msig_version} - for msig_version in ['generic', 'legacy'] - for by_address in [True, False] -] - - -def parse_msig_storage(storage: str): - """Parse the storage of a multisig contract to get its counter (as a - number), threshold (as a number), and the keys of the signers (as - Micheline sequence in a string).""" - # put everything on a single line - storage = ' '.join(storage.split('\n')) - storage_regexp = r'Pair\s+?([0-9]+)\s+?([0-9]+)\s+?(.*)\s*' - match = re.search(storage_regexp, storage) - assert match is not None - return { - 'counter': int(match[1]), - 'threshold': int(match[2]), - 'keys': match[3], - } - - -def resolve_key_alias(client: Client, alias: str) -> str: - """Convert a key alias into a public key that can be understood in - Michelson.""" - ret = client.show_address(alias).public_key - assert ret is not None - return ret - - -def build_michelson_key_list(client: Client, keys: List[str]): - """From a list of key aliases, build a Michelson list of public keys.""" - keys = [resolve_key_alias(client, k) for k in keys] - return '{"' + '"; "'.join(keys) + '"}' - - -def build_msig_storage( - client: Client, counter: int, threshold: int, keys: List[str] -): - """Build a multisig storage from its components: a counter, a - threshold and the list of signer public keys.""" - keys = build_michelson_key_list(client, keys) - return f'Pair {counter} {threshold} {keys}' - - -def assert_michelson_eq(client, data1, data2, typ): - """Check that two Michelson expressions of the same type are equal by - normalizing them.""" - normalized1 = client.normalize(data=data1, typ=typ) - normalized2 = client.normalize(data=data2, typ=typ) - assert normalized1 == normalized2 - - -def assert_msig_storage_eq(client, data1, data2): - """Check that two multisig storages are equal.""" - assert_michelson_eq(client, data1, data2, 'pair nat nat (list key)') - - -def assert_msig_counter_incr(current_storage, new_storage): - """Check that [new_storage] is the same multisig storage than - [current_storage] except that it uses the next counter.""" - current_storage = parse_msig_storage(current_storage) - new_storage = parse_msig_storage(new_storage) - assert new_storage['counter'] == 1 + current_storage['counter'] - assert new_storage['threshold'] == current_storage['threshold'] - assert new_storage['keys'] == current_storage['keys'] - - -@pytest.fixture(scope="class", params=MSIG_PARAMS) -def msig(client: Client, keys, request): - """This fixture originates a multisig contract with a threshold of 2 - and the keys given as parameter. The version of the script is given by - the msig_version parameter, it can be either 'generic' or - 'legacy'. This fixture returns a dictionary containing: - - - a [handle] that can be used to interact with the contract: - either the address of the script or an alias, depending on the - [by_address] parameter - - - the list of [keys] that are stored in the contract which is a - copy of the [keys] parameter - - - the [version], which is a copy of the [msig_version] parameter - - """ - - # We use the same alias for all multisig originations, this makes - # testing simpler but requires using the '--force' option. - msig_alias = 'msig' - msig_version = request.param['msig_version'] - by_address = request.param['by_address'] - initial_storage = build_msig_storage( - client=client, counter=0, threshold=2, keys=keys - ) - deployment = client.originate( - msig_alias, - 100, - 'bootstrap1', - msig_path(msig_version), - # Initialize with empty key list and null threshold - ['--init', initial_storage, '--burn-cap', '100', '--force'], - ) - utils.bake(client) - handle = deployment.contract if by_address else msig_alias - return {'handle': handle, 'keys': keys, 'version': msig_version} - - -@pytest.mark.incremental -class TestMultisig: - def test_deploy_multisig(self, msig, client: Client): - """Test that: - - the script originated by the "deploy multisig" command, - - the generic_multisig.tz script found in the mini_scenarios - directory, and - - the script printed by the "show multisig script" - are the same.""" - keys = msig['keys'] - - # The command cannot originate the legacy contract so there is - # nothing to test in the legacy case. - if msig['version'] == 'generic': - client.deploy_msig( - 'dummy_msig', - 100, - 'bootstrap1', - 2, - keys, - ['--burn-cap', '100', '--force'], - ) - utils.bake(client) - expected_hash = ( - 'exprub9UzpxmhedNQnsv1J1DazWGJnj1dLhtG1fxkUoWSdFLBGLqJ4' - ) - assert expected_hash in client.run( - ['show', 'supported', 'multisig', 'hashes'] - ) - assert client.get_script_hash(msig['handle']) == expected_hash - assert client.get_script_hash('dummy_msig') == expected_hash - assert client.hash_script( - [client.run(['show', 'multisig', 'script'])] - ) == [(expected_hash, None)] - assert client.get_balance('dummy_msig') == 100 - - def test_transfer(self, msig, client: Client, session: dict): - """Test the client command for signing a multisig transfer from key - number 0 and store the signature in the session.""" - keys = msig['keys'] - key = keys[0] - session['sig0'] = client.msig_sign_transfer( - msig['handle'], 10, 'bootstrap2', key - ) - - def test_prepare_msig_transfer(self, msig, client: Client): - """Test the client command for preparing a transfer. The result of the - command is ignored in this test, we only test that the command - succeeds.""" - client.msig_prepare_transfer(msig['handle'], 10, 'bootstrap2') - - def test_prepare_msig_sign(self, msig, client: Client, session: dict): - """Produce signatures for keys number 1 and 2 using the the - preparation command together with the sign_bytes client command. The - signatures are stored in the session.""" - to_sign = client.msig_prepare_transfer( - msig['handle'], 10, 'bootstrap2', ['--bytes-only'] - ) - session['sig1'] = client.sign_bytes_of_string(to_sign, msig['keys'][1]) - session['sig2'] = client.sign_bytes_of_string(to_sign, msig['keys'][2]) - - def test_transfer_failure(self, msig, client: Client, session: dict): - """Test transfer failure when there are too few signatures.""" - error_pattern = ( - r"Not enough signatures: " - + r"only 1 signatures were given " - + r"but the threshold is currently 2" - ) - - with utils.assert_run_failure(error_pattern): - client.msig_transfer( - msig['handle'], - 10, - 'bootstrap2', - 'bootstrap1', - [session['sig2']], - ) - - def test_transfer_success(self, msig, client: Client, session: dict): - """Test a successful transfer using signatures obtained by different - methods. The signatures are taken from the session.""" - current_storage = client.get_storage(msig['handle']) - current_balance = client.get_balance(msig['handle']) - - client.msig_transfer( - msig['handle'], - 10, - 'bootstrap2', - 'bootstrap1', - [session['sig0'], session['sig2']], - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - assert_msig_counter_incr(current_storage, new_storage) - new_balance = client.get_balance(msig['handle']) - assert new_balance == current_balance - 10 - - def test_default_entrypoint(self, msig, client): - """The generic multisig contract features an unauthorized default - entrypoint to receive donations but the legacy one does not.""" - - def cmd(): - client.transfer( - amount=100, giver='bootstrap1', receiver=msig['handle'] - ) - - if msig['version'] == 'legacy': - error_pattern = r'Invalid argument passed to contract' - with utils.assert_run_failure(error_pattern): - cmd() - else: - current_storage = client.get_storage(msig['handle']) - current_balance = client.get_balance(msig['handle']) - cmd() - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - new_balance = client.get_balance(msig['handle']) - assert new_storage == current_storage - assert new_balance == current_balance + 100 - - def test_transfer_with_entrypoint(self, msig, client: Client): - """Both versions of the contract can call arbitrary entrypoints of - type unit. This test uses the two possible methods to produce the - signatures.""" - current_storage = client.get_storage(msig['handle']) - current_balance = client.get_balance(msig['handle']) - contract = ( - 'parameter (or (unit %a) (string %b)); ' - 'storage unit; ' - 'code {CDR; NIL operation; PAIR}' - ) - client.originate( - 'dest_entrypoint', - 0, - 'bootstrap1', - contract, - args=['--burn-cap', '10.0', '--force'], - ) - args = ['--entrypoint', 'a'] - utils.bake(client) - to_sign = client.msig_prepare_transfer( - msig_name=msig['handle'], - amount=10, - dest='dest_entrypoint', - args=args + ['--bytes-only'], - ) - sig0 = client.sign_bytes_of_string(to_sign, msig['keys'][0]) - sig2 = client.msig_sign_transfer( - msig_name=msig['handle'], - amount=10, - dest='dest_entrypoint', - secret_key=msig['keys'][2], - args=args, - ) - client.msig_transfer( - msig_name=msig['handle'], - amount=10, - dest='dest_entrypoint', - src='bootstrap1', - signatures=[sig0, sig2], - args=args, - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - new_balance = client.get_balance(msig['handle']) - assert_msig_counter_incr(current_storage, new_storage) - assert new_balance == current_balance - 10 - - def test_transfer_with_arg(self, msig, client: Client): - """The generic multisig contract can call other contracts with - arbitrary parameters but the legacy one can only send Unit.""" - contract = ( - 'parameter (or (int %a) (string %b)); ' - 'storage unit; ' - 'code {CDR; NIL operation; PAIR}' - ) - client.originate( - 'dest', - 0, - 'bootstrap1', - contract, - args=['--burn-cap', '10.0', '--force'], - ) - args = ['--entrypoint', 'a', '--arg', '42'] - utils.bake(client) - - def cmd(): - return client.msig_prepare_transfer( - msig_name=msig['handle'], - amount=10, - dest='dest', - args=args + ['--bytes-only'], - ) - - if msig['version'] == 'legacy': - error_pattern = ( - r'This multisig contract can only transfer tokens to' - ' contracts of type unit; calling a contract with argument 42' - ' is not supported.' - ) - with utils.assert_run_failure(error_pattern): - cmd() - else: - current_storage = client.get_storage(msig['handle']) - current_balance = client.get_balance(msig['handle']) - to_sign = cmd() - utils.bake(client) - sig0 = client.sign_bytes_of_string(to_sign, msig['keys'][0]) - sig2 = client.msig_sign_transfer( - msig_name=msig['handle'], - amount=10, - dest='dest', - secret_key=msig['keys'][2], - args=args, - ) - client.msig_transfer( - msig_name=msig['handle'], - amount=10, - dest='dest', - src='bootstrap1', - signatures=[sig0, sig2], - args=args, - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - new_balance = client.get_balance(msig['handle']) - assert_msig_counter_incr(current_storage, new_storage) - assert new_balance == current_balance - 10 - - def test_transfer_ill_typed(self, msig, client: Client): - """Test that the multisig transfer preparation command type checks the - parameter.""" - error_pattern = ( - ( - r'The entrypoint b of contract .* ' - 'called from a multisig contract is of type string; ' - 'the provided parameter 42 is ill-typed.' - ) - if msig['version'] == 'generic' - else ( - r'This multisig contract can only transfer tokens to' - ' contracts of type unit; calling a contract with argument 42' - ' is not supported.' - ) - ) - - args = ['--entrypoint', 'b', '--arg', '42'] - - def cmd(): - client.msig_prepare_transfer( - msig_name=msig['handle'], - amount=10, - dest='dest', - args=args + ['--bytes-only'], - ) - - with utils.assert_run_failure(error_pattern): - cmd() - - def test_transfer_too_high(self, msig, client: Client): - """Test that the multisig transfer preparation command checks the - balance.""" - expected_warning = ( - 'Transferred amount is bigger than current multisig balance' - ) - - client.msig_prepare_transfer( - msig_name=msig['handle'], - amount=1000, - dest='bootstrap1', - args=['--bytes-only'], - expected_warning=expected_warning, - ) - - def test_multiple_operations(self, msig, client: Client): - """The generic multisig client can run lambdas, this can be used to - atomically run several operations.""" - bootstrap1_address = constants.IDENTITIES['bootstrap1']['identity'] - bootstrap2_address = constants.IDENTITIES['bootstrap2']['identity'] - bootstrap3_address = constants.IDENTITIES['bootstrap3']['identity'] - lam = ( - '{ DROP; NIL operation; ' - f'PUSH key_hash "{bootstrap1_address}"; IMPLICIT_ACCOUNT; ' - 'PUSH mutez 1000000; UNIT; TRANSFER_TOKENS; CONS; ' - f'PUSH key_hash "{bootstrap2_address}"; IMPLICIT_ACCOUNT; ' - 'PUSH mutez 2000000; UNIT; TRANSFER_TOKENS; CONS; ' - f'PUSH key_hash "{bootstrap3_address}"; SOME; ' - 'SET_DELEGATE; CONS}' - ) - lam = client.normalize( - lam, typ='lambda unit (list operation)', mode='Optimized' - ) - - def cmd(): - return client.msig_prepare_lambda( - msig_name=msig['handle'], lam=lam, args=['--bytes-only'] - ) - - if msig['version'] == 'legacy': - error_pattern = 'This multisig contract has a fixed set of actions' - with utils.assert_run_failure(error_pattern): - cmd() - else: - current_storage = client.get_storage(msig['handle']) - current_balance = client.get_balance(msig['handle']) - to_sign = cmd() - sig0 = client.sign_bytes_of_string(to_sign, msig['keys'][0]) - sig2 = client.msig_sign_lambda( - msig_name=msig['handle'], lam=lam, secret_key=msig['keys'][2] - ) - client.msig_run_lambda( - msig_name=msig['handle'], - lam=lam, - src='bootstrap1', - signatures=[sig0, sig2], - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - new_balance = client.get_balance(msig['handle']) - assert_msig_counter_incr(current_storage, new_storage) - assert new_balance == current_balance - 3 - # TODO: check the delegate change - - def test_multiple_operations_failure(self, msig, client: Client): - """Test for the error message for ill-typed lambdas.""" - lam = '{ DROP }' - - def cmd(): - client.msig_prepare_lambda( - msig_name=msig['handle'], lam=lam, args=['--bytes-only'] - ) - - error_pattern = ( - ( - r'The provided lambda .* for multisig contract' - r' is ill-typed; .* is expected.' - ) - if msig['version'] == 'generic' - else 'This multisig contract has a fixed set of actions' - ) - - with utils.assert_run_failure(error_pattern): - cmd() - - def test_delegate_change(self, msig, client: Client): - """Test the multisig command for changing delegate.""" - current_storage = client.get_storage(msig['handle']) - current_balance = client.get_balance(msig['handle']) - sig0 = client.msig_sign_set_delegate( - msig['handle'], 'bootstrap5', msig['keys'][0] - ) - to_sign = client.msig_prepare_set_delegate( - msig['handle'], 'bootstrap5', ['--bytes-only'] - ) - sig2 = client.sign_bytes_of_string(to_sign, msig['keys'][2]) - client.msig_set_delegate( - msig['handle'], 'bootstrap5', 'bootstrap1', [sig0, sig2] - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - new_balance = client.get_balance(msig['handle']) - assert_msig_counter_incr(current_storage, new_storage) - assert new_balance == current_balance - - def test_delegate_withdraw(self, msig, client: Client): - """Test the multisig command for removing delegation.""" - current_storage = client.get_storage(msig['handle']) - current_balance = client.get_balance(msig['handle']) - sig0 = client.msig_sign_withdrawing_delegate( - msig['handle'], msig['keys'][0] - ) - to_sign = client.msig_prepare_withdrawing_delegate( - msig['handle'], ['--bytes-only'] - ) - - sig1 = client.sign_bytes_of_string(to_sign, msig['keys'][1]) - client.msig_withdrawing_delegate( - msig['handle'], 'bootstrap1', [sig0, sig1] - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - new_balance = client.get_balance(msig['handle']) - assert_msig_counter_incr(current_storage, new_storage) - assert new_balance == current_balance - - def test_run_transaction_change_keys_and_threshold( - self, msig, client: Client - ): - """Test changing the keys and threshold with the `run transaction` - command.""" - current_storage = client.get_storage(msig['handle']) - current_counter = parse_msig_storage(storage=current_storage)['counter'] - current_balance = client.get_balance(msig['handle']) - keys = msig['keys'] - sig0 = client.msig_sign_setting_threshold( - msig['handle'], keys[0], 2, [keys[0], keys[2]] - ) - to_sign = client.msig_prepare_setting_threshold( - msig['handle'], 2, [keys[0], keys[2]], ['--bytes-only'] - ) - sig2 = client.sign_bytes_of_string(to_sign, msig['keys'][2]) - client.msig_run_transaction( - msig['handle'], to_sign, 'bootstrap1', [sig0, sig2] - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - expected_counter = 1 + current_counter - expected_storage = build_msig_storage( - client=client, - counter=expected_counter, - threshold=2, - keys=[keys[0], keys[2]], - ) - assert_msig_storage_eq(client, new_storage, expected_storage) - new_balance = client.get_balance(msig['handle']) - assert new_balance == current_balance - - def test_change_keys_and_threshold(self, msig, client: Client): - """Test changing the keys and threshold with `set threshold of - multisig` command.""" - current_storage = client.get_storage(msig['handle']) - current_counter = parse_msig_storage(storage=current_storage)['counter'] - current_balance = client.get_balance(msig['handle']) - keys = msig['keys'] - new_keys = [keys[0], keys[2]] - sig0 = client.msig_sign_setting_threshold( - msig['handle'], keys[0], 2, new_keys - ) - to_sign = client.msig_prepare_setting_threshold( - msig['handle'], 2, new_keys, ['--bytes-only'] - ) - sig2 = client.sign_bytes_of_string(to_sign, msig['keys'][2]) - client.msig_set_threshold( - msig['handle'], 2, new_keys, 'bootstrap1', [sig0, sig2] - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - expected_counter = 1 + current_counter - expected_storage = build_msig_storage( - client=client, - counter=expected_counter, - threshold=2, - keys=[keys[0], keys[2]], - ) - assert_msig_storage_eq(client, new_storage, expected_storage) - new_balance = client.get_balance(msig['handle']) - assert new_balance == current_balance - - -class TestUnsupportedMultisig: - """Verify that non-multisig contracts are rejected""" - - def test_deploy_nonmultisig(self, client: Client): - contract = find_script(['attic', 'id']) - client.originate( - 'id', - 0, - 'bootstrap1', - contract, - args=['--burn-cap', '10.0', '--force', '--init', '""'], - ) - utils.bake(client) - - error_pattern = ( - 'The hash of this script is ' - 'exprv8K6ceBpFH5SFjQm4BRYSLJCHQBFeQU6BFTdvQSRPaPkzdLyAL, ' - 'it was not found among in the list of known multisig ' - 'script hashes.' - ) - - with utils.assert_run_failure(error_pattern): - client.msig_transfer('id', 10, 'bootstrap2', 'bootstrap1', []) diff --git a/tests_python/tests_alpha/test_multisig.py b/tests_python/tests_alpha/test_multisig.py deleted file mode 100644 index cc234f4260b1..000000000000 --- a/tests_python/tests_alpha/test_multisig.py +++ /dev/null @@ -1,617 +0,0 @@ -# octez-client has builtin support for multisig smart contracts. See -# docs/user/multisig.rst for more details about it. - -# This file tests the client multisig support; more precisely it tests -# that both the generic and the legacy versions of the multisig smart -# contract behave as intended. For all commands, we check that we can -# interact with the multisig contract when invoking it by its address -# or by its alias. - -import re -from typing import List -import pytest -from tools import utils, constants -from client.client import Client -from .contract_paths import find_script - - -def get_keys(client): - """Generate 3 pairs of keys using various schemes and return the list - of aliases.""" - keys = ['foo', 'bar', 'boo'] - sigs = [None, 'secp256k1', 'ed25519'] - for key, sig in zip(keys, sigs): - args = [] if sig is None else ['--sig', sig] - client.gen_key(key, args) - return keys - - -@pytest.fixture(scope="class") -def keys(client): - return get_keys(client) - - -def msig_path(msig_version: str) -> str: - return find_script(['mini_scenarios', f'{msig_version}_multisig']) - - -MSIG_PARAMS = [ - {'by_address': by_address, 'msig_version': msig_version} - for msig_version in ['generic', 'legacy'] - for by_address in [True, False] -] - - -def parse_msig_storage(storage: str): - """Parse the storage of a multisig contract to get its counter (as a - number), threshold (as a number), and the keys of the signers (as - Micheline sequence in a string).""" - # put everything on a single line - storage = ' '.join(storage.split('\n')) - storage_regexp = r'Pair\s+?([0-9]+)\s+?([0-9]+)\s+?(.*)\s*' - match = re.search(storage_regexp, storage) - assert match is not None - return { - 'counter': int(match[1]), - 'threshold': int(match[2]), - 'keys': match[3], - } - - -def resolve_key_alias(client: Client, alias: str) -> str: - """Convert a key alias into a public key that can be understood in - Michelson.""" - ret = client.show_address(alias).public_key - assert ret is not None - return ret - - -def build_michelson_key_list(client: Client, keys: List[str]): - """From a list of key aliases, build a Michelson list of public keys.""" - keys = [resolve_key_alias(client, k) for k in keys] - return '{"' + '"; "'.join(keys) + '"}' - - -def build_msig_storage( - client: Client, counter: int, threshold: int, keys: List[str] -): - """Build a multisig storage from its components: a counter, a - threshold and the list of signer public keys.""" - keys = build_michelson_key_list(client, keys) - return f'Pair {counter} {threshold} {keys}' - - -def assert_michelson_eq(client, data1, data2, typ): - """Check that two Michelson expressions of the same type are equal by - normalizing them.""" - normalized1 = client.normalize(data=data1, typ=typ) - normalized2 = client.normalize(data=data2, typ=typ) - assert normalized1 == normalized2 - - -def assert_msig_storage_eq(client, data1, data2): - """Check that two multisig storages are equal.""" - assert_michelson_eq(client, data1, data2, 'pair nat nat (list key)') - - -def assert_msig_counter_incr(current_storage, new_storage): - """Check that [new_storage] is the same multisig storage than - [current_storage] except that it uses the next counter.""" - current_storage = parse_msig_storage(current_storage) - new_storage = parse_msig_storage(new_storage) - assert new_storage['counter'] == 1 + current_storage['counter'] - assert new_storage['threshold'] == current_storage['threshold'] - assert new_storage['keys'] == current_storage['keys'] - - -@pytest.fixture(scope="class", params=MSIG_PARAMS) -def msig(client: Client, keys, request): - """This fixture originates a multisig contract with a threshold of 2 - and the keys given as parameter. The version of the script is given by - the msig_version parameter, it can be either 'generic' or - 'legacy'. This fixture returns a dictionary containing: - - - a [handle] that can be used to interact with the contract: - either the address of the script or an alias, depending on the - [by_address] parameter - - - the list of [keys] that are stored in the contract which is a - copy of the [keys] parameter - - - the [version], which is a copy of the [msig_version] parameter - - """ - - # We use the same alias for all multisig originations, this makes - # testing simpler but requires using the '--force' option. - msig_alias = 'msig' - msig_version = request.param['msig_version'] - by_address = request.param['by_address'] - initial_storage = build_msig_storage( - client=client, counter=0, threshold=2, keys=keys - ) - deployment = client.originate( - msig_alias, - 100, - 'bootstrap1', - msig_path(msig_version), - # Initialize with empty key list and null threshold - ['--init', initial_storage, '--burn-cap', '100', '--force'], - ) - utils.bake(client) - handle = deployment.contract if by_address else msig_alias - return {'handle': handle, 'keys': keys, 'version': msig_version} - - -@pytest.mark.incremental -class TestMultisig: - def test_deploy_multisig(self, msig, client: Client): - """Test that: - - the script originated by the "deploy multisig" command, - - the generic_multisig.tz script found in the mini_scenarios - directory, and - - the script printed by the "show multisig script" - are the same.""" - keys = msig['keys'] - - # The command cannot originate the legacy contract so there is - # nothing to test in the legacy case. - if msig['version'] == 'generic': - client.deploy_msig( - 'dummy_msig', - 100, - 'bootstrap1', - 2, - keys, - ['--burn-cap', '100', '--force'], - ) - utils.bake(client) - expected_hash = ( - 'exprub9UzpxmhedNQnsv1J1DazWGJnj1dLhtG1fxkUoWSdFLBGLqJ4' - ) - assert expected_hash in client.run( - ['show', 'supported', 'multisig', 'hashes'] - ) - assert client.get_script_hash(msig['handle']) == expected_hash - assert client.get_script_hash('dummy_msig') == expected_hash - assert client.hash_script( - [client.run(['show', 'multisig', 'script'])] - ) == [(expected_hash, None)] - assert client.get_balance('dummy_msig') == 100 - - def test_transfer(self, msig, client: Client, session: dict): - """Test the client command for signing a multisig transfer from key - number 0 and store the signature in the session.""" - keys = msig['keys'] - key = keys[0] - session['sig0'] = client.msig_sign_transfer( - msig['handle'], 10, 'bootstrap2', key - ) - - def test_prepare_msig_transfer(self, msig, client: Client): - """Test the client command for preparing a transfer. The result of the - command is ignored in this test, we only test that the command - succeeds.""" - client.msig_prepare_transfer(msig['handle'], 10, 'bootstrap2') - - def test_prepare_msig_sign(self, msig, client: Client, session: dict): - """Produce signatures for keys number 1 and 2 using the the - preparation command together with the sign_bytes client command. The - signatures are stored in the session.""" - to_sign = client.msig_prepare_transfer( - msig['handle'], 10, 'bootstrap2', ['--bytes-only'] - ) - session['sig1'] = client.sign_bytes_of_string(to_sign, msig['keys'][1]) - session['sig2'] = client.sign_bytes_of_string(to_sign, msig['keys'][2]) - - def test_transfer_failure(self, msig, client: Client, session: dict): - """Test transfer failure when there are too few signatures.""" - error_pattern = ( - r"Not enough signatures: " - + r"only 1 signatures were given " - + r"but the threshold is currently 2" - ) - - with utils.assert_run_failure(error_pattern): - client.msig_transfer( - msig['handle'], - 10, - 'bootstrap2', - 'bootstrap1', - [session['sig2']], - ) - - def test_transfer_success(self, msig, client: Client, session: dict): - """Test a successful transfer using signatures obtained by different - methods. The signatures are taken from the session.""" - current_storage = client.get_storage(msig['handle']) - current_balance = client.get_balance(msig['handle']) - - client.msig_transfer( - msig['handle'], - 10, - 'bootstrap2', - 'bootstrap1', - [session['sig0'], session['sig2']], - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - assert_msig_counter_incr(current_storage, new_storage) - new_balance = client.get_balance(msig['handle']) - assert new_balance == current_balance - 10 - - def test_default_entrypoint(self, msig, client): - """The generic multisig contract features an unauthorized default - entrypoint to receive donations but the legacy one does not.""" - - def cmd(): - client.transfer( - amount=100, giver='bootstrap1', receiver=msig['handle'] - ) - - if msig['version'] == 'legacy': - error_pattern = r'Invalid argument passed to contract' - with utils.assert_run_failure(error_pattern): - cmd() - else: - current_storage = client.get_storage(msig['handle']) - current_balance = client.get_balance(msig['handle']) - cmd() - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - new_balance = client.get_balance(msig['handle']) - assert new_storage == current_storage - assert new_balance == current_balance + 100 - - def test_transfer_with_entrypoint(self, msig, client: Client): - """Both versions of the contract can call arbitrary entrypoints of - type unit. This test uses the two possible methods to produce the - signatures.""" - current_storage = client.get_storage(msig['handle']) - current_balance = client.get_balance(msig['handle']) - contract = ( - 'parameter (or (unit %a) (string %b)); ' - 'storage unit; ' - 'code {CDR; NIL operation; PAIR}' - ) - client.originate( - 'dest_entrypoint', - 0, - 'bootstrap1', - contract, - args=['--burn-cap', '10.0', '--force'], - ) - args = ['--entrypoint', 'a'] - utils.bake(client) - to_sign = client.msig_prepare_transfer( - msig_name=msig['handle'], - amount=10, - dest='dest_entrypoint', - args=args + ['--bytes-only'], - ) - sig0 = client.sign_bytes_of_string(to_sign, msig['keys'][0]) - sig2 = client.msig_sign_transfer( - msig_name=msig['handle'], - amount=10, - dest='dest_entrypoint', - secret_key=msig['keys'][2], - args=args, - ) - client.msig_transfer( - msig_name=msig['handle'], - amount=10, - dest='dest_entrypoint', - src='bootstrap1', - signatures=[sig0, sig2], - args=args, - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - new_balance = client.get_balance(msig['handle']) - assert_msig_counter_incr(current_storage, new_storage) - assert new_balance == current_balance - 10 - - def test_transfer_with_arg(self, msig, client: Client): - """The generic multisig contract can call other contracts with - arbitrary parameters but the legacy one can only send Unit.""" - contract = ( - 'parameter (or (int %a) (string %b)); ' - 'storage unit; ' - 'code {CDR; NIL operation; PAIR}' - ) - client.originate( - 'dest', - 0, - 'bootstrap1', - contract, - args=['--burn-cap', '10.0', '--force'], - ) - args = ['--entrypoint', 'a', '--arg', '42'] - utils.bake(client) - - def cmd(): - return client.msig_prepare_transfer( - msig_name=msig['handle'], - amount=10, - dest='dest', - args=args + ['--bytes-only'], - ) - - if msig['version'] == 'legacy': - error_pattern = ( - r'This multisig contract can only transfer tokens to' - ' contracts of type unit; calling a contract with argument 42' - ' is not supported.' - ) - with utils.assert_run_failure(error_pattern): - cmd() - else: - current_storage = client.get_storage(msig['handle']) - current_balance = client.get_balance(msig['handle']) - to_sign = cmd() - utils.bake(client) - sig0 = client.sign_bytes_of_string(to_sign, msig['keys'][0]) - sig2 = client.msig_sign_transfer( - msig_name=msig['handle'], - amount=10, - dest='dest', - secret_key=msig['keys'][2], - args=args, - ) - client.msig_transfer( - msig_name=msig['handle'], - amount=10, - dest='dest', - src='bootstrap1', - signatures=[sig0, sig2], - args=args, - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - new_balance = client.get_balance(msig['handle']) - assert_msig_counter_incr(current_storage, new_storage) - assert new_balance == current_balance - 10 - - def test_transfer_ill_typed(self, msig, client: Client): - """Test that the multisig transfer preparation command type checks the - parameter.""" - error_pattern = ( - ( - r'The entrypoint b of contract .* ' - 'called from a multisig contract is of type string; ' - 'the provided parameter 42 is ill-typed.' - ) - if msig['version'] == 'generic' - else ( - r'This multisig contract can only transfer tokens to' - ' contracts of type unit; calling a contract with argument 42' - ' is not supported.' - ) - ) - - args = ['--entrypoint', 'b', '--arg', '42'] - - def cmd(): - client.msig_prepare_transfer( - msig_name=msig['handle'], - amount=10, - dest='dest', - args=args + ['--bytes-only'], - ) - - with utils.assert_run_failure(error_pattern): - cmd() - - def test_transfer_too_high(self, msig, client: Client): - """Test that the multisig transfer preparation command checks the - balance.""" - expected_warning = ( - 'Transferred amount is bigger than current multisig balance' - ) - - client.msig_prepare_transfer( - msig_name=msig['handle'], - amount=1000, - dest='bootstrap1', - args=['--bytes-only'], - expected_warning=expected_warning, - ) - - def test_multiple_operations(self, msig, client: Client): - """The generic multisig client can run lambdas, this can be used to - atomically run several operations.""" - bootstrap1_address = constants.IDENTITIES['bootstrap1']['identity'] - bootstrap2_address = constants.IDENTITIES['bootstrap2']['identity'] - bootstrap3_address = constants.IDENTITIES['bootstrap3']['identity'] - lam = ( - '{ DROP; NIL operation; ' - f'PUSH key_hash "{bootstrap1_address}"; IMPLICIT_ACCOUNT; ' - 'PUSH mutez 1000000; UNIT; TRANSFER_TOKENS; CONS; ' - f'PUSH key_hash "{bootstrap2_address}"; IMPLICIT_ACCOUNT; ' - 'PUSH mutez 2000000; UNIT; TRANSFER_TOKENS; CONS; ' - f'PUSH key_hash "{bootstrap3_address}"; SOME; ' - 'SET_DELEGATE; CONS}' - ) - lam = client.normalize( - lam, typ='lambda unit (list operation)', mode='Optimized' - ) - - def cmd(): - return client.msig_prepare_lambda( - msig_name=msig['handle'], lam=lam, args=['--bytes-only'] - ) - - if msig['version'] == 'legacy': - error_pattern = 'This multisig contract has a fixed set of actions' - with utils.assert_run_failure(error_pattern): - cmd() - else: - current_storage = client.get_storage(msig['handle']) - current_balance = client.get_balance(msig['handle']) - to_sign = cmd() - sig0 = client.sign_bytes_of_string(to_sign, msig['keys'][0]) - sig2 = client.msig_sign_lambda( - msig_name=msig['handle'], lam=lam, secret_key=msig['keys'][2] - ) - client.msig_run_lambda( - msig_name=msig['handle'], - lam=lam, - src='bootstrap1', - signatures=[sig0, sig2], - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - new_balance = client.get_balance(msig['handle']) - assert_msig_counter_incr(current_storage, new_storage) - assert new_balance == current_balance - 3 - # TODO: check the delegate change - - def test_multiple_operations_failure(self, msig, client: Client): - """Test for the error message for ill-typed lambdas.""" - lam = '{ DROP }' - - def cmd(): - client.msig_prepare_lambda( - msig_name=msig['handle'], lam=lam, args=['--bytes-only'] - ) - - error_pattern = ( - ( - r'The provided lambda .* for multisig contract' - r' is ill-typed; .* is expected.' - ) - if msig['version'] == 'generic' - else 'This multisig contract has a fixed set of actions' - ) - - with utils.assert_run_failure(error_pattern): - cmd() - - def test_delegate_change(self, msig, client: Client): - """Test the multisig command for changing delegate.""" - current_storage = client.get_storage(msig['handle']) - current_balance = client.get_balance(msig['handle']) - sig0 = client.msig_sign_set_delegate( - msig['handle'], 'bootstrap5', msig['keys'][0] - ) - to_sign = client.msig_prepare_set_delegate( - msig['handle'], 'bootstrap5', ['--bytes-only'] - ) - sig2 = client.sign_bytes_of_string(to_sign, msig['keys'][2]) - client.msig_set_delegate( - msig['handle'], 'bootstrap5', 'bootstrap1', [sig0, sig2] - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - new_balance = client.get_balance(msig['handle']) - assert_msig_counter_incr(current_storage, new_storage) - assert new_balance == current_balance - - def test_delegate_withdraw(self, msig, client: Client): - """Test the multisig command for removing delegation.""" - current_storage = client.get_storage(msig['handle']) - current_balance = client.get_balance(msig['handle']) - sig0 = client.msig_sign_withdrawing_delegate( - msig['handle'], msig['keys'][0] - ) - to_sign = client.msig_prepare_withdrawing_delegate( - msig['handle'], ['--bytes-only'] - ) - - sig1 = client.sign_bytes_of_string(to_sign, msig['keys'][1]) - client.msig_withdrawing_delegate( - msig['handle'], 'bootstrap1', [sig0, sig1] - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - new_balance = client.get_balance(msig['handle']) - assert_msig_counter_incr(current_storage, new_storage) - assert new_balance == current_balance - - def test_run_transaction_change_keys_and_threshold( - self, msig, client: Client - ): - """Test changing the keys and threshold with the `run transaction` - command.""" - current_storage = client.get_storage(msig['handle']) - current_counter = parse_msig_storage(storage=current_storage)['counter'] - current_balance = client.get_balance(msig['handle']) - keys = msig['keys'] - sig0 = client.msig_sign_setting_threshold( - msig['handle'], keys[0], 2, [keys[0], keys[2]] - ) - to_sign = client.msig_prepare_setting_threshold( - msig['handle'], 2, [keys[0], keys[2]], ['--bytes-only'] - ) - sig2 = client.sign_bytes_of_string(to_sign, msig['keys'][2]) - client.msig_run_transaction( - msig['handle'], to_sign, 'bootstrap1', [sig0, sig2] - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - expected_counter = 1 + current_counter - expected_storage = build_msig_storage( - client=client, - counter=expected_counter, - threshold=2, - keys=[keys[0], keys[2]], - ) - assert_msig_storage_eq(client, new_storage, expected_storage) - new_balance = client.get_balance(msig['handle']) - assert new_balance == current_balance - - def test_change_keys_and_threshold(self, msig, client: Client): - """Test changing the keys and threshold with `set threshold of - multisig` command.""" - current_storage = client.get_storage(msig['handle']) - current_counter = parse_msig_storage(storage=current_storage)['counter'] - current_balance = client.get_balance(msig['handle']) - keys = msig['keys'] - new_keys = [keys[0], keys[2]] - sig0 = client.msig_sign_setting_threshold( - msig['handle'], keys[0], 2, new_keys - ) - to_sign = client.msig_prepare_setting_threshold( - msig['handle'], 2, new_keys, ['--bytes-only'] - ) - sig2 = client.sign_bytes_of_string(to_sign, msig['keys'][2]) - client.msig_set_threshold( - msig['handle'], 2, new_keys, 'bootstrap1', [sig0, sig2] - ) - utils.bake(client) - new_storage = client.get_storage(msig['handle']) - expected_counter = 1 + current_counter - expected_storage = build_msig_storage( - client=client, - counter=expected_counter, - threshold=2, - keys=[keys[0], keys[2]], - ) - assert_msig_storage_eq(client, new_storage, expected_storage) - new_balance = client.get_balance(msig['handle']) - assert new_balance == current_balance - - -class TestUnsupportedMultisig: - """Verify that non-multisig contracts are rejected""" - - def test_deploy_nonmultisig(self, client: Client): - contract = find_script(['attic', 'id']) - client.originate( - 'id', - 0, - 'bootstrap1', - contract, - args=['--burn-cap', '10.0', '--force', '--init', '""'], - ) - utils.bake(client) - - error_pattern = ( - 'The hash of this script is ' - 'exprv8K6ceBpFH5SFjQm4BRYSLJCHQBFeQU6BFTdvQSRPaPkzdLyAL, ' - 'it was not found among in the list of known multisig ' - 'script hashes.' - ) - - with utils.assert_run_failure(error_pattern): - client.msig_transfer('id', 10, 'bootstrap2', 'bootstrap1', []) diff --git a/tezt/tests/main.ml b/tezt/tests/main.ml index 420629ad1cf5..b01051c2c260 100644 --- a/tezt/tests/main.ml +++ b/tezt/tests/main.ml @@ -142,6 +142,7 @@ let register_protocol_tests_that_use_supports_correctly () = Monitor_operations.register ~protocols ; Multinode_snapshot.register ~protocols ; Multiple_transfers.register ~protocols ; + Multisig.register ~protocols ; Node_cors.register ~protocols ; Node_event_level.register ~protocols ; Nonce_seed_revelation.register ~protocols ; diff --git a/tezt/tests/multisig.ml b/tezt/tests/multisig.ml new file mode 100644 index 000000000000..18fcee247e59 --- /dev/null +++ b/tezt/tests/multisig.ml @@ -0,0 +1,791 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2020 Nomadic Labs *) +(* *) +(* 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: Client/Multisig + Invocation: dune exec tezt/tests/main.exe -- --file multisig.ml + Subject: Tests octez-client multisig interaction +*) + +(* octez-client has builtin support for multisig smart contracts. See + docs/user/multisig.rst for more details about it. + + This file tests the client multisig support; more precisely it tests + that both the generic and the legacy versions of the multisig smart + contract behave as intended. For all commands, we check that we can + interact with the multisig contract when invoking it by its address or + by its alias. *) + +type msig_version = Legacy | Generic + +let show_msig_version = function Legacy -> "legacy" | Generic -> "generic" + +(* Generate 3 pairs of keys using various schemes and return the list + of keys. *) +let get_keys client (sig_alg0, sig_alg1, sig_alg2) = + let ((key0, key1, key2) as keys) = ("foo", "bar", "boo") in + let* (_ : string) = Client.gen_keys ~alias:key0 ?sig_alg:sig_alg0 client in + let* (_ : string) = Client.gen_keys ~alias:key1 ?sig_alg:sig_alg1 client in + let* (_ : string) = Client.gen_keys ~alias:key2 ?sig_alg:sig_alg2 client in + return keys + +let contract_path protocol kind contract = + sf + "tests_python/contracts_%s/%s/%s" + (match protocol with + | Protocol.Alpha -> "alpha" + | _ -> sf "%03d" @@ Protocol.number protocol) + kind + contract + +let msig_path protocol msig_version = + contract_path + protocol + "mini_scenarios" + (show_msig_version msig_version ^ "_multisig.tz") + +type msig_storage = {counter : int; threshold : int; keys : string list} + +let msig_storage_typ = + Check.( + convert + (fun {counter; threshold; keys} -> (counter, threshold, keys)) + (tuple3 int int (list string))) + +module Msig_state = struct + type t = {balance : Tez.t; delegate : string option; storage : msig_storage} + + let typ = + Check.( + convert + (fun {balance; delegate; storage} -> (balance, delegate, storage)) + (tuple3 Tez.typ (option string) msig_storage_typ)) + + let incr_counter {balance; delegate; storage} = + {balance; delegate; storage = {storage with counter = storage.counter + 1}} + + let incr_balance diff state = + {state with balance = Tez.(state.balance + diff)} + + let decr_balance diff state = + {state with balance = Tez.(state.balance - diff)} + + let set_delegate delegate state = {state with delegate = Some delegate} + + let withdraw_delegate state = {state with delegate = None} + + let set_threshold_and_public_keys threshold keys state = + let storage = {state.storage with threshold; keys} in + {state with storage} + + let ( <$> ) f1 f2 st = f2 (f1 st) +end + +(* Parse the storage of a multisig contract to get its counter (as a + number), threshold (as a number), and the keys of the signers (as + Micheline sequence in a string). *) +let parse_msig_storage storage = + let storage = replace_string ~all:true (rex "\\s+") ~by:" " storage in + let storage_regexp = rex "Pair ([0-9]+) ([0-9]+) \\{ (.*) \\}" in + match storage =~*** storage_regexp with + | Some (counter, threshold, keys) -> + { + counter = int_of_string counter; + threshold = int_of_string threshold; + keys = + keys |> String.split_on_char ';' + |> List.map (fun s -> + match String.split_on_char '"' s with + | [_; key; _] -> key + | _ -> + Test.fail + "[parse_msig_storage] Could not parse keys in multisig \ + storage %S" + storage); + } + | None -> + Test.fail + "[parse_msig_storage] Could not parse multisig storage: %S" + storage + +(* Build a multisig storage from its components: a counter, a + threshold and the list of signer public keys. *) +let build_msig_storage client {counter; threshold; keys} = + let* keys = + let* keys = + Lwt_list.map_s (fun key -> Client.show_address ~alias:key client) keys + in + return + ("{" + ^ String.concat + ";" + (List.map (fun key -> sf "%S" key.Account.public_key) keys) + ^ "}") + in + return (sf "Pair %d %d %s" counter threshold keys) + +type multisig = {handle : string; keys : string list; version : msig_version} + +(* [deploy_msig protocol client ~keys ~version ~by_address] originates a + multisig contract with a threshold of 2 and the [keys] given as parameter. + The version of the script is given by the [version] parameter, it can be + either {Generic} or {Legacy}. This function returns a dictionary + containing: + - a [handle] that can be used to interact with the contract: + either the address of the script or an alias, depending on the + [by_address] parameter + - the list of [keys] that are stored in the contract which is a + copy of the [keys] parameter + - the [version], which is a copy of the [msig_version] parameter *) +let deploy_msig protocol client ~keys ~version ~by_address = + let msig_alias = "msig" in + let* initial_storage = + build_msig_storage client {counter = 0; threshold = 2; keys} + in + let* deployment = + Client.originate_contract + ~alias:msig_alias + ~src:"bootstrap1" + ~amount:(Tez.of_int 100) + ~prg:(msig_path protocol version) + ~init:initial_storage + ~burn_cap:(Tez.of_int 100) + client + in + let handle = if by_address then deployment else msig_alias in + return {handle; keys; version} + +let test_multisig ~sig_algs ~supports protocols = + let iter xs f = List.iter f xs in + iter [true; false] @@ fun by_address -> + iter [Generic; Legacy] @@ fun version -> + Protocol.register_test + ~__FILE__ + ~supports + ~title: + (sf + "Multisig [by_address:%b, %s, sig_algs:%s]" + by_address + (show_msig_version version) + (String.concat "," + @@ List.map + (function None -> "None" | Some s -> s) + (let sig0, sig1, sig2 = sig_algs in + [sig0; sig1; sig2]))) + ~tags: + [ + "multisig"; + (if by_address then "by_address" else "by_alias"); + show_msig_version version; + ] + (fun protocol -> + let* client = Client.init_mockup ~protocol () in + let bootstrap1 = Constant.bootstrap1.alias in + let bootstrap2 = Constant.bootstrap2.alias in + let bootstrap5 = Constant.bootstrap5.alias in + let qty = Tez.of_int 10 in + let color = Log.Color.(bold ++ FG.cyan) in + (* Test that: + - the script originated by the "deploy multisig" command, + - the generic_multisig.tz script found in the mini_scenarios + directory, and + - the script printed by the "show multisig script" + are the same. *) + Log.info ~color "Deploy multisig" ; + let* key0, key1, key2 = get_keys client sig_algs in + let keys = [key0; key1; key2] in + let* msig = deploy_msig protocol ~version ~keys ~by_address client in + let multisig = msig.handle in + let get_state ?(contract = msig.handle) () = + let* balance = Client.get_balance_for ~account:contract client + and* storage = Client.contract_storage contract client + and* delegate = Client.get_delegate ~src:contract client in + return + Msig_state.{balance; storage = parse_msig_storage storage; delegate} + in + let check_state_change ?__LOC__ ?(error_msg = "Expected state %R, got %L") + f_state f_body = + let* current_state = get_state () in + let* () = f_body () in + let* new_state = get_state () in + Check.( + (new_state = f_state current_state) Msig_state.typ ?__LOC__ ~error_msg) ; + unit + in + let* () = + match version with + | Legacy -> unit + | Generic -> + let qty = Tez.of_int 100 in + let* () = + Client.deploy_multisig + ~burn_cap:Tez.one + ~new_multisig:"dummy_msig" + ~src:bootstrap1 + ~threshold:2 + ~keys:msig.keys + ~qty + client + in + let expected_hash = + "exprub9UzpxmhedNQnsv1J1DazWGJnj1dLhtG1fxkUoWSdFLBGLqJ4" + in + let* handle_hash = + Client.get_contract_hash ~contract:msig.handle client + in + Check.( + (handle_hash = expected_hash) + string + ~__LOC__ + ~error_msg:"Expected hash of handle to be %R, got %L") ; + let* alias_hash = + Client.get_contract_hash ~contract:"dummy_msig" client + in + Check.( + (alias_hash = expected_hash) + string + ~__LOC__ + ~error_msg:"Expected hash of handle to be %R, got %L") ; + let* balance = + Client.get_balance_for ~account:"dummy_msig" client + in + Check.( + (balance = qty) + Tez.typ + ~__LOC__ + ~error_msg:"Expected balance of dummy_sig to be %R, got %L.") ; + unit + in + (* Test the client command for signing a multisig transfer from key + number 0. *) + Log.info ~color "Transfer" ; + let* sig0 = + Client.sign_multisig_transaction_transfer + ~multisig + ~qty + ~dst:bootstrap2 + ~key:key0 + client + in + (* Test the client command for preparing a transfer. The result of the + command is ignored in this test, we only test that the command + succeeds. *) + Log.info ~color "Prepare msig transfer" ; + let* _data = + Client.prepare_multisig_transaction + ~multisig + ~qty + ~dst:bootstrap2 + client + in + (* Produce signatures for keys number 1 and 2 using the the + preparation command together with the sign_bytes client command. *) + Log.info ~color "Prepare msig sign" ; + let* data = + Client.prepare_multisig_transaction + ~multisig + ~qty + ~dst:bootstrap2 + ~bytes_only:true + client + in + let* (_ : string) = Client.sign_bytes ~signer:key1 ~data client + and* sig2 = Client.sign_bytes ~signer:key2 ~data client in + (* Test transfer failure when there are too few signatures. *) + Log.info ~color "Transfer failure" ; + let* () = + Client.spawn_from_multisig_transfer + ~multisig + ~qty + ~src:bootstrap1 + ~dst:bootstrap2 + ~signatures:[sig2] + client + |> Process.check_error + ~msg: + (rex + "Not enough signatures: only 1 signatures were given but the \ + threshold is currently 2") + in + (* Test a successful transfer using signatures obtained by different + methods. *) + Log.info ~color "Transfer success" ; + let* () = + check_state_change + ~__LOC__ + Msig_state.(incr_counter <$> decr_balance qty) + @@ fun () -> + Client.from_multisig_transfer + ~multisig + ~qty + ~src:bootstrap1 + ~dst:bootstrap2 + ~signatures:[sig0; sig2] + client + in + (* The generic multisig contract features an unauthorized default + entrypoint to receive donations but the legacy one does not. *) + Log.info ~color "Default entrypoint" ; + let amount = Tez.of_int 100 in + let spawn_transfer () = + Client.spawn_transfer + ~amount + ~giver:bootstrap1 + ~receiver:msig.handle + client + in + let* () = + match msig.version with + | Legacy -> + spawn_transfer () + |> Process.check_error + ~msg:(rex "Invalid argument passed to contract") + | Generic -> + check_state_change ~__LOC__ Msig_state.(incr_balance amount) + @@ fun () -> spawn_transfer () |> Process.check + in + Log.info ~color "Transfer with entrypoint" ; + (* Both versions of the contract can call arbitrary entrypoints of + type unit. This test uses the two possible methods to produce the + signatures. *) + let* () = + check_state_change + ~__LOC__ + Msig_state.(incr_counter <$> decr_balance qty) + @@ fun () -> + let alias = "multisig_dest_entrypoint" in + let entrypoint = "a" in + let* _address = + let path = + Michelson_script.(find ["mini_scenarios"; alias] protocol |> path) + in + Client.originate_contract + ~alias + ~amount:Tez.zero + ~burn_cap:(Tez.of_int 10) + ~prg:path + ~src:bootstrap1 + client + in + let* data = + Client.prepare_multisig_transaction + ~multisig + ~qty + ~dst:alias + ~bytes_only:true + ~entrypoint + client + in + let* sig0 = Client.sign_bytes ~signer:key0 ~data client + and* sig2 = + Client.sign_multisig_transaction_transfer + ~multisig + ~qty + ~dst:alias + ~key:key2 + ~entrypoint + client + in + Client.from_multisig_transfer + ~multisig + ~qty + ~src:bootstrap1 + ~dst:alias + ~signatures:[sig0; sig2] + ~entrypoint + client + in + (* The generic multisig contract can call other contracts with + arbitrary parameters but the legacy one can only send Unit. *) + Log.info ~color "Transfer with arg" ; + (* Test that the multisig transfer preparation command type checks the + parameter. *) + let alias = "multisig_dest_entrypoint_arg" in + let* _address = + let path = + Michelson_script.(find ["mini_scenarios"; alias] protocol |> path) + in + Client.originate_contract + ~alias + ~amount:Tez.zero + ~burn_cap:(Tez.of_int 10) + ~prg:path + ~src:bootstrap1 + client + in + let entrypoint = "a" in + let arg = "42" in + let prepare_transfer () = + Client.spawn_prepare_multisig_transaction + ~multisig + ~qty + ~dst:alias + ~bytes_only:true + ~entrypoint + ~arg + client + in + let* () = + match msig.version with + | Legacy -> + prepare_transfer () + |> Process.check_error + ~msg: + (rex + "This multisig contract can only transfer tokens to \ + contracts of type unit; calling a contract with \ + argument 42 is not supported.") + | Generic -> + check_state_change + ~__LOC__ + Msig_state.(incr_counter <$> decr_balance qty) + @@ fun () -> + let* data = prepare_transfer () |> Process.check_and_read_stdout in + let data = String.trim data in + let* sig0 = Client.sign_bytes ~signer:key0 ~data client + and* sig2 = + Client.sign_multisig_transaction_transfer + ~multisig + ~qty + ~dst:alias + ~key:key2 + ~entrypoint + ~arg + client + in + Client.from_multisig_transfer + ~multisig + ~qty + ~src:bootstrap1 + ~dst:alias + ~signatures:[sig0; sig2] + ~entrypoint + ~arg + client + in + (* Test that the multisig transfer preparation command type + checks the parameter. *) + Log.info ~color "Transfer ill-typed" ; + let* () = + let msg = + rex + @@ + match msig.version with + | Generic -> + "The entrypoint b of contract .* called from a multisig contract \ + is of type string; the provided parameter 42 is ill-typed." + | Legacy -> + "This multisig contract can only transfer tokens to contracts of \ + type unit; calling a contract with argument 42 is not \ + supported." + in + Client.spawn_prepare_multisig_transaction + ~multisig + ~qty + ~dst:alias + ~bytes_only:true + ~entrypoint:"b" + ~arg:"42" + client + |> Process.check_error ~msg + in + (* Test that the multisig transfer preparation command checks the + balance. *) + Log.info ~color "Transfer too high" ; + let* output = + Client.spawn_prepare_multisig_transaction + ~multisig + ~qty:(Tez.of_int 1000) + ~dst:bootstrap1 + ~bytes_only:true + client + |> Process.check_and_read_stderr + in + Check.( + (output + =~ rex "Transferred amount is bigger than current multisig balance") + ~error_msg:"Expected output %L to match expression %R.") ; + (* The generic multisig client can run lambdas, this can be used to + atomically run several operations. *) + Log.info ~color "Multiple operations" ; + let bootstrap1_address = Constant.bootstrap1.public_key_hash in + let bootstrap2_address = Constant.bootstrap2.public_key_hash in + let bootstrap3_address = Constant.bootstrap3.public_key_hash in + let lambda = + sf + {|{ DROP; NIL operation; PUSH key_hash "%s"; IMPLICIT_ACCOUNT; PUSH mutez 1000000; UNIT; TRANSFER_TOKENS; CONS; PUSH key_hash "%s"; IMPLICIT_ACCOUNT; PUSH mutez 2000000; UNIT; TRANSFER_TOKENS; CONS; PUSH key_hash "%s"; SOME; SET_DELEGATE; CONS }|} + bootstrap1_address + bootstrap2_address + bootstrap3_address + in + let* lambda = + Client.normalize_data + ~data:("text:" ^ lambda) + (* Force 'text' to avoid the client attempting read the lambda as a path *) + ~typ:"lambda unit (list operation)" + ~mode:Client.Optimized + client + in + let prepare_lambda () = + Client.spawn_prepare_multisig_transaction_run_lambda + ~multisig + ~lambda + ~bytes_only:true + client + in + let* () = + match msig.version with + | Legacy -> + prepare_lambda () + |> Process.check_error + ~msg:(rex "This multisig contract has a fixed set of actions") + | Generic -> + check_state_change + ~__LOC__ + Msig_state.( + incr_counter + <$> decr_balance (Tez.of_int 3) + <$> set_delegate bootstrap3_address) + @@ fun () -> + let* data = prepare_lambda () |> Process.check_and_read_stdout in + let data = String.trim data in + let* sig0 = Client.sign_bytes ~signer:key0 ~data client + and* sig2 = + Client.sign_multisig_transaction_run_lambda + ~multisig + ~lambda + ~key:key2 + client + in + Client.from_multisig_run_lambda + ~multisig + ~lambda + ~src:bootstrap1 + ~signatures:[sig0; sig2] + client + in + (* Test for the error message for ill-typed lambdas. *) + Log.info ~color "Multiple operations failure" ; + let* () = + let msg = + rex + @@ + match msig.version with + | Generic -> + "The provided lambda .* for multisig contract is ill-typed; .* \ + is expected." + | Legacy -> "This multisig contract has a fixed set of actions." + in + Client.spawn_prepare_multisig_transaction_run_lambda + ~multisig + ~lambda:"{ DROP }" + ~bytes_only:true + client + |> Process.check_error ~msg + in + (* Test the multisig command for changing delegate. *) + Log.info ~color "Delegate change" ; + let* () = + check_state_change + ~__LOC__ + Msig_state.( + incr_counter <$> set_delegate Constant.bootstrap5.public_key_hash) + @@ fun () -> + let* data = + Client.prepare_multisig_transaction_set_delegate + ~multisig + ~dlgt:bootstrap5 + ~bytes_only:true + client + in + let* sig0 = + Client.sign_multisig_transaction_set_delegate + ~multisig + ~dlgt:bootstrap5 + ~key:key0 + client + and* sig2 = Client.sign_bytes ~signer:key2 ~data client in + Client.set_delegate_of_multisig + ~multisig + ~dlgt:bootstrap5 + ~src:bootstrap1 + ~signatures:[sig0; sig2] + client + in + (* Test the multisig command for removing delegation. *) + Log.info ~color "Delegate withdraw" ; + let* () = + check_state_change + ~__LOC__ + Msig_state.(incr_counter <$> withdraw_delegate) + @@ fun () -> + let* data = + Client.prepare_multisig_transaction_withdraw_delegate + ~multisig + ~bytes_only:true + client + in + let* sig0 = + Client.sign_multisig_transaction_withdraw_delegate + ~multisig + ~key:key0 + client + and* sig1 = Client.sign_bytes ~signer:key1 ~data client in + Client.withdraw_delegate_of_multisig + ~multisig + ~src:bootstrap1 + ~signatures:[sig0; sig1] + client + in + (* Test changing the keys and threshold with the `run transaction` + command. *) + Log.info ~color "Run transaction change keys and threshold" ; + let* key0_key = Client.show_address ~alias:key0 client + and* key2_key = Client.show_address ~alias:key2 client in + let threshold = 2 in + let* () = + check_state_change + ~__LOC__ + Msig_state.( + incr_counter + <$> set_threshold_and_public_keys + threshold + [key0_key.public_key; key2_key.public_key]) + @@ fun () -> + let public_keys = [key0; key2] in + let* data = + Client.prepare_multisig_transaction_set_threshold_and_public_keys + ~multisig + ~threshold + ~public_keys + ~bytes_only:true + client + in + let* sig0 = + Client.sign_multisig_transaction_set_threshold_and_public_keys + ~multisig + ~signing_key:key0 + ~threshold + ~public_keys + client + and* sig2 = Client.sign_bytes ~signer:key2 ~data client in + Client.run_transaction_on_multisig + ~multisig + ~bytes:data + ~src:bootstrap1 + ~signatures:[sig0; sig2] + client + in + (* Test changing the keys and threshold with `set threshold of + multisig` command. *) + Log.info ~color "Change keys and threshold" ; + let threshold = 2 in + let* () = + check_state_change + ~__LOC__ + Msig_state.( + incr_counter + <$> set_threshold_and_public_keys + threshold + [key0_key.public_key; key2_key.public_key]) + @@ fun () -> + let public_keys = [key0; key2] in + let* data = + Client.prepare_multisig_transaction_set_threshold_and_public_keys + ~multisig + ~threshold + ~public_keys + ~bytes_only:true + client + in + let* sig0 = + Client.sign_multisig_transaction_set_threshold_and_public_keys + ~multisig + ~signing_key:key0 + ~threshold + ~public_keys + client + and* sig2 = Client.sign_bytes ~signer:key2 ~data client in + Client.set_threshold_of_multisig + ~multisig + ~threshold + ~public_keys + ~src:bootstrap1 + ~signatures:[sig0; sig2] + client + in + + unit) + protocols + +(* Verify that non-multisig contracts are rejected *) +let test_unsupported_multisig = + Protocol.register_test + ~__FILE__ + ~title:"Unsupported multisig" + ~tags:["unsupported"; "multisig"] + @@ fun protocol -> + Log.info "Deploy nonmultisig" ; + let* client = Client.init_mockup ~protocol () in + let contract = contract_path protocol "attic" "id.tz" in + let* _address = + Client.originate_contract + ~amount:Tez.zero + ~alias:"id" + ~src:"bootstrap1" + ~prg:contract + ~burn_cap:(Tez.of_int 10) + ~init:{|""|} + client + in + Client.spawn_from_multisig_transfer + ~multisig:"id" + ~qty:(Tez.of_int 10) + ~src:"bootstrap1" + ~dst:"bootstrap2" + ~signatures:[] + client + |> Process.check_error + ~msg: + (rex + "The hash of this script is \ + exprv8K6ceBpFH5SFjQm4BRYSLJCHQBFeQU6BFTdvQSRPaPkzdLyAL, it was \ + not found among in the list of known multisig script hashes.") + +let register ~protocols = + test_unsupported_multisig protocols ; + test_multisig + ~supports:Protocol.Any_protocol + ~sig_algs:(None, Some "secp256k1", Some "ed25519") + protocols ; + test_multisig + ~supports:Protocol.Any_protocol + ~sig_algs:(Some "p256", Some "ed25519", None) + protocols ; + test_multisig + ~supports:(Protocol.From_protocol 16) + ~sig_algs:(None, Some "bls", None) + protocols -- GitLab