Revision cbda8dd964df0dfd3415209db7ff9d1d9e91c1cf authored by Alain Mebsout on 08 July 2022, 08:12:22 UTC, committed by Alain Mebsout on 06 September 2022, 14:48:23 UTC
1 parent dcc833b
Raw File
sapling.ml
(*****************************************************************************)
(*                                                                           *)
(* Open Source License                                                       *)
(* Copyright (c) 2022 Nomadic Labs <contact@nomadic-labs.com>                *)
(*                                                                           *)
(* Permission is hereby granted, free of charge, to any person obtaining a   *)
(* copy of this software and associated documentation files (the "Software"),*)
(* to deal in the Software without restriction, including without limitation *)
(* the rights to use, copy, modify, merge, publish, distribute, sublicense,  *)
(* and/or sell copies of the Software, and to permit persons to whom the     *)
(* Software is furnished to do so, subject to the following conditions:      *)
(*                                                                           *)
(* The above copyright notice and this permission notice shall be included   *)
(* in all copies or substantial portions of the Software.                    *)
(*                                                                           *)
(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*)
(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,  *)
(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL   *)
(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*)
(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING   *)
(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER       *)
(* DEALINGS IN THE SOFTWARE.                                                 *)
(*                                                                           *)
(*****************************************************************************)

(* Testing
   -------
   Component:    Sapling
   Invocation:   dune exec tezt/tests/main.exe -- --file sapling.ml
   Subject:      Tests for the Sapling client and protocol
*)

open Tezt_tezos

module Helpers = struct
  module Re = struct
    let extract_sapling_address client_output =
      client_output =~* rex ".*?(zet1\\w{65}.*)" |> Option.get

    let extract_sapling_balance client_output =
      client_output
      =~* rex "Total Sapling funds ?(\\d*)ꜩ"
      |> Option.get |> int_of_string

    let mutez_of_string s =
      match String.split_on_char '.' s with
      | [int] -> int_of_string int * 1_000_000
      | [int; dec] ->
          let zeroes_to_add = 6 - String.length dec in
          if zeroes_to_add < 0 then
            failwith (Format.sprintf "Decimal part too long in %S" s)
          else int_of_string (int ^ dec ^ String.make zeroes_to_add '0')
      | _ -> failwith (Format.sprintf "Malformed mutez string %S" s)

    (* These regexs only work from protocol J, before there is no field
       `storage fees`. *)
    let balance_diff ~dst client_output =
      let fees =
        let re = ".*fees.* \\.* \\+ꜩ?(\\d*[.\\d*]*)" in
        let res = matches client_output (rex re) in
        List.map mutez_of_string res |> List.fold_left ( + ) 0
      in
      let amount_pos =
        let re = dst ^ ".*\\+ꜩ?(\\d*[.\\d*]*)" in
        match matches client_output (rex re) with
        | [s] -> mutez_of_string s
        | _ -> 0
      in
      let amount_neg =
        let re = dst ^ ".*\\-ꜩ?(\\d*[.\\d*]*)" in
        match matches client_output (rex re) with
        | [s] -> mutez_of_string s
        | _ -> 0
      in
      (amount_pos - amount_neg, fees)
  end

  let init protocol contract =
    let* node = Node.init [Node.Synchronisation_threshold 0; Connections 0] in
    let* client = Client.init ~endpoint:(Node node) () in
    let* () = Client.activate_protocol ~protocol client in
    Log.info "Activated protocol." ;
    let* contract_address =
      let prg =
        sf
          "./src/%s/lib_protocol/test/integration/michelson/contracts/%s"
          (Protocol.directory protocol)
          contract
      in
      Client.originate_contract
        ~wait:"none"
        ~init:"{}"
        ~alias:contract
        ~amount:Tez.zero
        ~burn_cap:(Tez.of_int 10)
        ~src:Constant.bootstrap1.alias
        ~prg
        client
    in
    let* () = Client.bake_for_and_wait client in
    Log.info "  - Originated %s." contract ;
    return (client, contract_address)

  let gen_user (client, contract) name =
    let* () =
      Client.spawn_command
        client
        ["sapling"; "gen"; "key"; name; "--unencrypted"]
      |> Process.check
    in
    let* () =
      Client.spawn_command
        client
        [
          "sapling";
          "use";
          "key";
          name;
          "for";
          "contract";
          contract;
          "--memo-size";
          "8";
        ]
      |> Process.check
    in
    let* client_output =
      Client.spawn_command client ["sapling"; "gen"; "address"; name]
      |> Process.check_and_read_stdout
    in
    return @@ Re.extract_sapling_address client_output

  let balance (client, contract) name =
    let* client_output =
      Client.spawn_command
        client
        ["sapling"; "get"; "balance"; "for"; name; "in"; "contract"; contract]
      |> Process.check_and_read_stdout
    in
    return @@ Re.extract_sapling_balance client_output

  let assert_balance client name amount =
    let* balance = balance client name in
    assert (balance = amount) ;
    unit

  let shield ?(expect_failure = false) (client, contract) src dst amount_tez =
    let* client_output =
      Client.spawn_command
        client
        (["--wait"; "none"]
        @ [
            "sapling";
            "shield";
            string_of_int amount_tez;
            "from";
            Account.(src.alias);
            "to";
            dst;
            "using";
            contract;
            "--burn-cap";
            "2";
          ])
      |> Process.check_and_read_stdout ~expect_failure
    in
    if expect_failure then return (0, 0)
    else
      let* () = Client.bake_for_and_wait client in
      Log.info "shield" ;
      return @@ Re.balance_diff ~dst:contract client_output

  let transfer ?(expect_failure = false) (client, contract) src dst amount_tez =
    let temp_sapling_transaction_file = Temp.file "sapling_transaction" in
    let* () =
      Client.spawn_command
        client
        [
          "sapling";
          "forge";
          "transaction";
          string_of_int amount_tez;
          "from";
          src;
          "to";
          dst;
          "using";
          contract;
          "--file";
          temp_sapling_transaction_file;
        ]
      |> Process.check ~expect_failure
    in
    if expect_failure then unit
    else
      let* () =
        Client.spawn_command
          client
          (["--wait"; "none"]
          @ [
              "sapling";
              "submit";
              temp_sapling_transaction_file;
              "from";
              Constant.bootstrap1.alias;
              "using";
              contract;
              "--burn-cap";
              "1";
            ])
        |> Process.check
      in
      let* () = Client.bake_for_and_wait client in
      Log.info "transfer" ;
      unit

  let unshield ?(expect_failure = false) (client, contract) src dst amount_tez =
    let* client_output =
      Client.spawn_command
        client
        (["--wait"; "none"]
        @ [
            "sapling";
            "unshield";
            string_of_int amount_tez;
            "from";
            src;
            "to";
            Account.(dst.alias);
            "using";
            contract;
            "--burn-cap";
            "1";
          ])
      |> Process.check_and_read_stdout ~expect_failure
    in
    if expect_failure then return (0, 0)
    else
      let* () = Client.bake_for_and_wait client in
      Log.info "unshield" ;
      return @@ Re.balance_diff ~dst:contract client_output

  let balance_tz1 (client, _contract) pkh =
    let*! bal = RPC.Contracts.get_balance ~contract_id:pkh client in
    return (JSON.as_int bal)
end

let contract = "sapling_contract.tz"

module Insufficient_funds = struct
  let shield =
    Protocol.register_test
      ~__FILE__
      ~title:"insufficient_funds.shield"
      ~tags:["sapling"]
    @@ fun protocol ->
    let open Helpers in
    let* c = init protocol contract in
    let alice_tz1 = Constant.bootstrap2 in
    let* alice_address = gen_user c "alice" in
    let* _ =
      shield ~expect_failure:true c alice_tz1 alice_address 1_000_000_000
    in
    unit

  let transfer =
    Protocol.register_test
      ~__FILE__
      ~title:"insufficient_funds.transfer"
      ~tags:["sapling"]
    @@ fun protocol ->
    let open Helpers in
    let* c = init protocol contract in
    let alice_tz1 = Constant.bootstrap2 in
    let* alice_address = gen_user c "alice" in
    let* bob_address = gen_user c "bob" in
    let* _ = shield c alice_tz1 alice_address 10 in
    let* () = transfer ~expect_failure:true c "alice" bob_address 11 in
    unit

  let unshield =
    Protocol.register_test
      ~__FILE__
      ~title:"insufficient_funds.unshield"
      ~tags:["sapling"]
    @@ fun protocol ->
    let open Helpers in
    let* c = init protocol contract in
    let alice_tz1 = Constant.bootstrap2 in
    let* alice_address = gen_user c "alice" in
    let* bob_address = gen_user c "bob" in
    let* _ = shield c alice_tz1 alice_address 10 in
    let* () = transfer c "alice" bob_address 10 in
    let* _ = unshield ~expect_failure:true c "bob" alice_tz1 11 in
    unit
end

(* Roundtrip of shield, transfer and unshield. At each step we check the
   balances of the Tezos account that submits the operations, the two Sapling
   accounts and the smart contract. *)
let successful_roundtrip =
  Protocol.register_test
    ~__FILE__
    ~title:"successful_roundtrip"
    ~tags:["sapling"]
  @@ fun protocol ->
  let open Helpers in
  let* c = init protocol contract in
  let alice_tz1 = Constant.bootstrap2 in
  let* alice_address = gen_user c "alice" in
  let* bob_address = gen_user c "bob" in
  let* () = assert_balance c "alice" 0 in
  let* () = assert_balance c "bob" 0 in
  let* balance_alice_tz1_before = balance_tz1 c alice_tz1.public_key_hash in
  let* amount, fees = shield c alice_tz1 alice_address 10 in
  let* balance_alice_tz1_after = balance_tz1 c alice_tz1.public_key_hash in
  assert (amount = 10_000_000) ;
  assert (balance_alice_tz1_after = balance_alice_tz1_before - 10_000_000 - fees) ;
  let* balance_contract = balance_tz1 c (snd c) in
  assert (balance_contract = 10_000_000) ;
  let* () = assert_balance c "alice" 10 in
  let* () = assert_balance c "bob" 0 in
  let* () = transfer c "alice" bob_address 10 in
  let* balance_contract = balance_tz1 c (snd c) in
  assert (balance_contract = 10_000_000) ;
  let* () = assert_balance c "alice" 0 in
  let* () = assert_balance c "bob" 10 in
  let* balance_alice_tz1_before = balance_tz1 c alice_tz1.public_key_hash in
  let* amount, fees = unshield c "bob" alice_tz1 10 in
  let* balance_alice_tz1_after = balance_tz1 c alice_tz1.public_key_hash in
  assert (amount = -10_000_000) ;
  assert (balance_alice_tz1_after = balance_alice_tz1_before + 10_000_000 - fees) ;
  let* balance_contract = balance_tz1 c (snd c) in
  assert (balance_contract = 0) ;
  let* () = assert_balance c "alice" 0 in
  let* () = assert_balance c "bob" 0 in
  unit

let register ~protocols =
  Insufficient_funds.shield protocols ;
  Insufficient_funds.transfer protocols ;
  Insufficient_funds.unshield protocols ;
  successful_roundtrip protocols
back to top