Raw File
tx_rollup_l2_apply.ml
(*****************************************************************************)
(*                                                                           *)
(* Open Source License                                                       *)
(* Copyright (c) 2022 Marigold <contact@marigold.dev>                        *)
(* Copyright (c) 2022 Nomadic Labs <contact@nomadic-labs.com>                *)
(* Copyright (c) 2022 Oxhead Alpha <info@oxheadalpha.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.                                                 *)
(*                                                                           *)
(*****************************************************************************)

open Alpha_context
open Tx_rollup_l2_context_sig
open Tx_rollup_l2_batch

type error +=
  | Counter_mismatch of {
      account : Tx_rollup_l2_address.t;
      expected : int64;
      provided : int64;
    }
  | Incorrect_aggregated_signature
  | Unallocated_metadata of int32
  | Multiple_operations_for_signer of Bls.Public_key.t
  | Invalid_transaction_encoding
  | Invalid_batch_encoding
  | Unexpectedly_indexed_ticket
  | Missing_ticket of Ticket_hash.t
  | Unknown_address of Tx_rollup_l2_address.t
  | Invalid_self_transfer
  | Invalid_zero_transfer
  | Maximum_withdraws_per_message_exceeded of {current : int; maximum : int}

let () =
  let open Data_encoding in
  (* Counter mismatch *)
  register_error_kind
    `Branch
    ~id:"tx_rollup_operation_counter_mismatch"
    ~title:"Operation counter mismatch"
    ~description:
      "A transaction rollup operation has been submitted with an incorrect \
       counter"
    (obj3
       (req "account" Tx_rollup_l2_address.encoding)
       (req "expected" int64)
       (req "provided" int64))
    (function
      | Counter_mismatch {account; expected; provided} ->
          Some (account, expected, provided)
      | _ -> None)
    (fun (account, expected, provided) ->
      Counter_mismatch {account; expected; provided}) ;
  (* Incorrect aggregated signature *)
  register_error_kind
    `Permanent
    ~id:"tx_rollup_incorrect_aggregated_signature"
    ~title:"Incorrect aggregated signature"
    ~description:"The aggregated signature is incorrect"
    empty
    (function Incorrect_aggregated_signature -> Some () | _ -> None)
    (function () -> Incorrect_aggregated_signature) ;
  (* Unallocated metadata *)
  register_error_kind
    `Branch
    ~id:"tx_rollup_unknown_metadata"
    ~title:"Unknown metadata"
    ~description:
      "A public key index was provided but the account information for this \
       index is not present in the context."
    (obj1 (req "idx" int32))
    (function Unallocated_metadata i -> Some i | _ -> None)
    (function i -> Unallocated_metadata i) ;
  (* Invalid transaction *)
  register_error_kind
    `Permanent
    ~id:"tx_rollup_invalid_transaction"
    ~title:"Invalid transaction"
    ~description:
      "The signer signed multiple operations in the same transaction. He must \
       gather all the contents in a single operation"
    (obj1 (req "pk" Bls.Public_key.encoding))
    (function Multiple_operations_for_signer idx -> Some idx | _ -> None)
    (function idx -> Multiple_operations_for_signer idx) ;
  (* Invalid transaction encoding *)
  register_error_kind
    `Permanent
    ~id:"tx_rollup_invalid_transaction_encoding"
    ~title:"Invalid transaction encoding"
    ~description:"The transaction could not be decoded from bytes"
    empty
    (function Invalid_transaction_encoding -> Some () | _ -> None)
    (function () -> Invalid_transaction_encoding) ;
  (* Invalid batch encoding *)
  register_error_kind
    `Permanent
    ~id:"tx_rollup_invalid_batch_encoding"
    ~title:"Invalid batch encoding"
    ~description:"The batch could not be decoded from bytes"
    empty
    (function Invalid_batch_encoding -> Some () | _ -> None)
    (function () -> Invalid_batch_encoding) ;
  (* Unexpectedly indexed ticket *)
  register_error_kind
    `Permanent
    ~id:"tx_rollup_unexpectedly_indexed_ticket"
    ~title:"Unexpected indexed ticket in deposit or transfer"
    ~description:
      "Tickets in layer2-to-layer1 transfers must be referenced by value."
    empty
    (function Unexpectedly_indexed_ticket -> Some () | _ -> None)
    (function () -> Unexpectedly_indexed_ticket) ;
  (* Missing ticket *)
  register_error_kind
    `Temporary
    ~id:"tx_rollup_missing_ticket"
    ~title:"Attempted to withdraw from a ticket missing in the rollup"
    ~description:
      "A withdrawal must reference a ticket that already exists in the rollup."
    (obj1 (req "ticket_hash" Ticket_hash.encoding))
    (function Missing_ticket ticket_hash -> Some ticket_hash | _ -> None)
    (function ticket_hash -> Missing_ticket ticket_hash) ;
  (* Unknown address *)
  register_error_kind
    `Temporary
    ~id:"tx_rollup_unknown_address"
    ~title:"Attempted to sign a transfer with an unknown address"
    ~description:
      "The address must exist in the context when signing a transfer with it."
    (obj1 (req "address" Tx_rollup_l2_address.encoding))
    (function Unknown_address addr -> Some addr | _ -> None)
    (function addr -> Unknown_address addr) ;
  (* Invalid self transfer *)
  register_error_kind
    `Temporary
    ~id:"tx_rollup_invalid_self_transfer"
    ~title:"Attempted to transfer ticket to self"
    ~description:"The index for the destination is the same as the sender"
    empty
    (function Invalid_self_transfer -> Some () | _ -> None)
    (function () -> Invalid_self_transfer) ;
  (* Invalid zero transfer *)
  register_error_kind
    `Permanent
    ~id:"tx_rollup_invalid_zero_transfer"
    ~title:"Attempted to transfer zero ticket"
    ~description:"A transfer's amount must be greater than zero."
    empty
    (function Invalid_zero_transfer -> Some () | _ -> None)
    (function () -> Invalid_zero_transfer) ;
  (* Maximum_withdraws_per_message_exceeded *)
  register_error_kind
    `Permanent
    ~id:"tx_rollup_maximum_withdraws_per_message_exceeded"
    ~title:"Maximum tx-rollup withdraws per message exceeded"
    ~description:
      "The maximum number of withdraws allowed per tx-rollup message exceeded"
    (obj2 (req "current" int31) (req "limit" int31))
    (function
      | Maximum_withdraws_per_message_exceeded {current; maximum} ->
          Some (current, maximum)
      | _ -> None)
    (fun (current, maximum) ->
      Maximum_withdraws_per_message_exceeded {current; maximum})

type indexes = {
  address_indexes :
    (Tx_rollup_l2_address.t * Tx_rollup_l2_address.Indexable.index) list;
  ticket_indexes : (Ticket_hash.t * Ticket_indexable.index) list;
}

let encoding_indexes : indexes Data_encoding.t =
  let open Data_encoding in
  conv
    (fun {address_indexes; ticket_indexes} -> (address_indexes, ticket_indexes))
    (fun (address_indexes, ticket_indexes) -> {address_indexes; ticket_indexes})
  @@ obj2
       (req
          "address_indexes"
          (list
             (tup2
                Tx_rollup_l2_address.encoding
                Tx_rollup_l2_address.Indexable.index_encoding)))
       (req
          "ticket_indexes"
          (list (tup2 Ticket_hash.encoding Ticket_indexable.index_encoding)))

module Message_result = struct
  type transaction_result =
    | Transaction_success
    | Transaction_failure of {index : int; reason : error}

  type deposit_result = Deposit_success of indexes | Deposit_failure of error

  let encoding_transaction_result =
    let open Data_encoding in
    union
      [
        (let kind = "transaction_success" in
         case
           ~title:kind
           (Tag 0)
           (constant kind)
           (function Transaction_success -> Some () | _ -> None)
           (fun () -> Transaction_success));
        (let kind = "transaction_failure" in
         case
           ~title:kind
           (Tag 1)
           (obj1
              (req
                 kind
                 (obj2
                    (req "transaction_index" Data_encoding.int31)
                    (req "reason" Error_monad.error_encoding))))
           (function
             | Transaction_failure {index; reason} -> Some (index, reason)
             | _ -> None)
           (fun (index, reason) -> Transaction_failure {index; reason}));
      ]

  let encoding_deposit_result =
    let open Data_encoding in
    union
      [
        (let kind = "deposit_success" in
         case
           ~title:kind
           (Tag 0)
           (obj1 (req kind encoding_indexes))
           (function Deposit_success indexes -> Some indexes | _ -> None)
           (fun indexes -> Deposit_success indexes));
        (let kind = "deposit_failure" in
         case
           ~title:kind
           (Tag 1)
           (obj1 (req kind (obj1 (req "reason" Error_monad.error_encoding))))
           (function Deposit_failure reason -> Some reason | _ -> None)
           (fun reason -> Deposit_failure reason));
      ]

  module Batch_V1 = struct
    type t =
      | Batch_result of {
          results :
            ((Indexable.index_only, Indexable.unknown) V1.transaction
            * transaction_result)
            list;
          indexes : indexes;
        }

    let encoding =
      let open Data_encoding in
      conv
        (fun (Batch_result {results; indexes}) -> (results, indexes))
        (fun (results, indexes) -> Batch_result {results; indexes})
        (obj2
           (req "results"
           @@ list
                (Data_encoding.tup2
                   (Compact.make
                      ~tag_size:`Uint8
                      V1.compact_transaction_signer_index)
                   encoding_transaction_result))
           (req "allocated_indexes" encoding_indexes))
  end

  type message_result =
    | Deposit_result of deposit_result
    | Batch_V1_result of Batch_V1.t

  let message_result_encoding =
    let open Data_encoding in
    union
      [
        (let kind = "deposit_result" in
         case
           ~title:kind
           (Tag 0)
           (obj1 (req kind encoding_deposit_result))
           (function Deposit_result result -> Some result | _ -> None)
           (fun result -> Deposit_result result));
        (let kind = "batch_v1_result" in
         case
           ~title:kind
           (Tag 1)
           (obj1 (req kind Batch_V1.encoding))
           (function Batch_V1_result result -> Some result | _ -> None)
           (fun result -> Batch_V1_result result));
      ]

  type t = message_result * Tx_rollup_withdraw.t list

  let encoding =
    Data_encoding.(
      tup2 message_result_encoding (list Tx_rollup_withdraw.encoding))
end

type parameters = {
  (* Maximum number of allowed L2-to-L1 withdraws per batch *)
  tx_rollup_max_withdrawals_per_batch : int;
}

module Make (Context : CONTEXT) = struct
  open Context
  open Syntax
  open Message_result

  type ctxt = Context.t

  (** {3. Indexes. } *)

  (** The application of a message can (and is supposed to) use and
      create several indexes during the application of a {Tx_rollup_message.t}.
  *)

  let index get_or_associate_index add_index ctxt indexes indexable =
    let open Indexable in
    match destruct indexable with
    | Right v -> (
        let+ ctxt, created, idx = get_or_associate_index ctxt v in
        match created with
        | `Existed -> (ctxt, indexes, idx)
        | `Created -> (ctxt, add_index indexes (v, idx), idx))
    | Left i -> return (ctxt, indexes, i)

  let address_index ctxt indexes indexable =
    let get_or_associate_index = Address_index.get_or_associate_index in
    let add_index indexes x =
      {indexes with address_indexes = x :: indexes.address_indexes}
    in
    index get_or_associate_index add_index ctxt indexes indexable

  let ticket_index ctxt indexes indexable =
    let get_or_associate_index = Ticket_index.get_or_associate_index in
    let add_index indexes x =
      {indexes with ticket_indexes = x :: indexes.ticket_indexes}
    in
    index get_or_associate_index add_index ctxt indexes indexable

  let address_of_signer_index :
      Signer_indexable.index -> Tx_rollup_l2_address.Indexable.index =
   fun idx -> Indexable.(index_exn (to_int32 idx))

  let signer_of_address_index :
      Tx_rollup_l2_address.Indexable.index -> Signer_indexable.index =
   fun idx -> Indexable.(index_exn (to_int32 idx))

  let empty_indexes = {address_indexes = []; ticket_indexes = []}

  let assert_non_zero_quantity qty =
    fail_when Tx_rollup_l2_qty.(qty = zero) Invalid_zero_transfer

  (** {2. Counter } *)

  (** [get_metadata ctxt idx] returns the metadata associated to [idx] in
      [ctxt]. It must have an associated metadata in the context, otherwise,
      something went wrong in {!check_signature}. *)
  let get_metadata : ctxt -> address_index -> metadata m =
   fun ctxt idx ->
    let open Address_metadata in
    let* metadata = get ctxt idx in
    match metadata with
    | None -> fail (Unallocated_metadata (Indexable.to_int32 idx))
    | Some metadata -> return metadata

  (** [get_metadata_signer] gets the metadata for a signer using {!get_metadata}.
      It transforms a signer index to an address one. *)
  let get_metadata_signer : ctxt -> Signer_indexable.index -> metadata m =
   fun ctxt signer_idx -> get_metadata ctxt (address_of_signer_index signer_idx)

  (** [transfers ctxt source_idx destination_idx tidx amount] transfers [amount]
      from [source_idx] to [destination_idx] of [tidx]. *)
  let transfer ctxt source_idx destination_idx tidx amount =
    let* () =
      fail_unless
        Compare.Int.(Indexable.compare_indexes source_idx destination_idx <> 0)
        Invalid_self_transfer
    in
    let* () = assert_non_zero_quantity amount in
    let* ctxt = Ticket_ledger.spend ctxt tidx source_idx amount in
    Ticket_ledger.credit ctxt tidx destination_idx amount

  (** [deposit ctxt aidx tidx amount] credits [amount] of [tidx] to [aidx].
      They are deposited from the layer1 and created in the layer2 context, but,
      we only handle the creation part (i.e. in the layer2) in this module. *)
  let deposit ctxt aidx tidx amount = Ticket_ledger.credit ctxt tidx aidx amount

  module Batch_V1 = struct
    open Tx_rollup_l2_batch.V1

    (** [operation_with_signer_index ctxt indexes op] takes an operation
        and performs multiple get/sets on the context to return an operation
        where the signer is replaced by its index.

        It performs on the [ctxt]:
        {ul {li If the signer is an index, we read the public key from the
                [ctxt].}
            {li If the signer is a public key, we associate a new index to
                it in the [ctxt]. The public key is also added to the metadata
                if not already present.}}

        {b Note:} If the context already contains all the required information,
        we only read from it. *)
    let operation_with_signer_index :
        ctxt ->
        indexes ->
        ('signer, 'content) operation ->
        (ctxt
        * indexes
        * (Indexable.index_only, 'content) operation
        * Bls.Public_key.t)
        m =
     fun ctxt indexes op ->
      let* ctxt, indexes, pk, idx =
        match Indexable.destruct op.signer with
        | Left signer_index ->
            (* Get the public key from the index. *)
            let address_index = address_of_signer_index signer_index in
            let* metadata = get_metadata ctxt address_index in
            let pk = metadata.public_key in
            return (ctxt, indexes, pk, address_index)
        | Right (Bls_pk signer_pk) -> (
            (* Initialize the ctxt with public_key if it's necessary. *)
            let addr = Bls.Public_key.hash signer_pk in
            let* ctxt, created, idx =
              Address_index.get_or_associate_index ctxt addr
            in

            (* If the address is created, we add it to [indexes]. *)
            match created with
            | `Existed ->
                (* If the public key existed in the context, it should not
                   be added in [indexes]. However, the metadata might not
                   have been initialized for the public key. Especially during
                   a deposit, the deposit destination is a layer2 address and
                   it contains no information about the public key.
                *)
                let* ctxt =
                  let* metadata = Address_metadata.get ctxt idx in
                  match metadata with
                  | Some _ ->
                      (* If the metadata exists, then the public key necessarily
                         exists, we do not need to change the context. *)
                      return ctxt
                  | None ->
                      Address_metadata.init_with_public_key ctxt idx signer_pk
                in
                return (ctxt, indexes, signer_pk, idx)
            | `Created ->
                (* If the index is created, we need to add to indexes and
                   initialize the metadata. *)
                let indexes =
                  {
                    indexes with
                    address_indexes = (addr, idx) :: indexes.address_indexes;
                  }
                in
                let* ctxt =
                  Address_metadata.init_with_public_key ctxt idx signer_pk
                in
                return (ctxt, indexes, signer_pk, idx))
        | Right (L2_addr signer_addr) -> (
            (* In order to get the public key associated to [signer_addr], there
               needs to be both an index associated to it, and a metadata for this
               index. *)
            let* idx = Address_index.get ctxt signer_addr in
            match idx with
            | None -> fail (Unknown_address signer_addr)
            | Some idx ->
                let* metadata = get_metadata ctxt idx in
                return (ctxt, indexes, metadata.public_key, idx))
      in
      let op : (Indexable.index_only, 'content) operation =
        {op with signer = signer_of_address_index idx}
      in
      return (ctxt, indexes, op, pk)

    (** [check_transaction ctxt indexes transmitted transaction] performs an
        *active* check of an operation.
        We consider this as an *active* check because the function is likely to
        write in the [ctxt], since it replaces the signer's public key
        (if provided) by its index in {!operation_with_signer_index}.

        Outside of the active preprocessing, we check that a signer signs
        at most one operation in the [transaction].

        It also associates the signer to the bytes representation of a
        transaction in [transmitted], which is used to check the aggregated
        signature.
    *)
    let check_transaction ctxt indexes transmitted transaction =
      let* buf =
        match
          Data_encoding.Binary.to_bytes_opt
            (Data_encoding.Compact.make ~tag_size:`Uint8 compact_transaction)
            transaction
        with
        | Some buf -> return buf
        | None -> fail Invalid_transaction_encoding
      in
      let* ctxt, indexes, transmitted, _, rev_ops =
        list_fold_left_m
          (fun (ctxt, indexes, transmitted, signers, ops) op ->
            let* ctxt, indexes, op, pk =
              operation_with_signer_index ctxt indexes op
            in
            if List.mem ~equal:Bls.Public_key.equal pk signers then
              fail (Multiple_operations_for_signer pk)
            else
              return
                ( ctxt,
                  indexes,
                  (pk, buf) :: transmitted,
                  pk :: signers,
                  op :: ops ))
          (ctxt, indexes, transmitted, [], [])
          transaction
      in
      return (ctxt, indexes, transmitted, List.rev rev_ops)

    let check_signature :
        ctxt ->
        ('signer, 'content) t ->
        (ctxt * indexes * (Indexable.index_only, 'content) t) m =
     fun ctxt ({contents = transactions; aggregated_signature} as batch) ->
      let* ctxt, indexes, transmitted, rev_new_transactions =
        list_fold_left_m
          (fun (ctxt, indexes, transmitted, new_transactions) transaction ->
            (* To check the signature, we need the list of [buf] each signer
               signed. That is, the [buf] is the binary encoding of the
               [transaction]. *)
            let* ctxt, indexes, transmitted, transaction =
              check_transaction ctxt indexes transmitted transaction
            in
            return (ctxt, indexes, transmitted, transaction :: new_transactions))
          (ctxt, empty_indexes, [], [])
          transactions
      in
      (* Once we collected the public keys for each signer and the buffers
         they signed, we can check the signature. *)
      let* b = bls_verify transmitted aggregated_signature in
      let* () = fail_unless b Incorrect_aggregated_signature in
      let batch = {batch with contents = List.rev rev_new_transactions} in
      return (ctxt, indexes, batch)

    (** {2. Apply } *)

    (** [apply_operation_content ctxt source content] performs the transfer
        on the [ctxt]. The validity of the transfer is checked in
        the context itself, e.g. for an invalid balance.

        It returns the potential created indexes:

        {ul {li The destination address index.}
            {li The ticket exchanged index.}}
    *)
    let apply_operation_content :
        ctxt ->
        indexes ->
        Signer_indexable.index ->
        'content operation_content ->
        (ctxt * indexes * Tx_rollup_withdraw.t option) m =
     fun ctxt indexes source_idx op_content ->
      match op_content with
      | Withdraw {destination = claimer; ticket_hash; qty = amount} ->
          (* To withdraw, the ticket must already exist in the
             rollup and be indexed (the ticket must have already been
             assigned an index in the content: otherwise the ticket has
             not been seen before and we can't withdraw from it). *)
          let* tidx_opt = Ticket_index.get ctxt ticket_hash in
          let*? tidx =
            Option.value_e ~error:(Missing_ticket ticket_hash) tidx_opt
          in
          let source_idx = address_of_signer_index source_idx in

          (* spend the ticket -- this is responsible for checking that
             the source has the required balance *)
          let* () = assert_non_zero_quantity amount in
          let* ctxt = Ticket_ledger.spend ctxt tidx source_idx amount in
          let withdrawal = Tx_rollup_withdraw.{claimer; ticket_hash; amount} in
          return (ctxt, indexes, Some withdrawal)
      | Transfer {destination; ticket_hash; qty} ->
          let* ctxt, indexes, dest_idx =
            address_index ctxt indexes destination
          in
          let* ctxt, indexes, tidx = ticket_index ctxt indexes ticket_hash in
          let source_idx = address_of_signer_index source_idx in
          let* ctxt = transfer ctxt source_idx dest_idx tidx qty in
          return (ctxt, indexes, None)

    (** [check_counter ctxt signer counter] asserts that the provided [counter] is the
        successor of the one associated to the [signer] in the [ctxt]. *)
    let check_counter :
        ctxt -> Indexable.index_only Signer_indexable.t -> int64 -> unit m =
     fun ctxt signer counter ->
      let* metadata = get_metadata_signer ctxt signer in
      fail_unless
        Compare.Int64.(counter = Int64.succ metadata.counter)
        (Counter_mismatch
           {
             account = Bls.Public_key.hash metadata.public_key;
             expected = Int64.succ metadata.counter;
             provided = counter;
           })

    (** [apply_operation ctxt indexes op] checks the counter validity for the [op.signer] with
        {!check_counter}, and then calls {!apply_operation_content} for each content in [op]. *)
    let apply_operation :
        ctxt ->
        indexes ->
        (Indexable.index_only, Indexable.unknown) operation ->
        (ctxt * indexes * Tx_rollup_withdraw.t list) m =
     fun ctxt indexes {signer; counter; contents} ->
      (* Before applying any operation, we check the counter *)
      let* () = check_counter ctxt signer counter in
      let* ctxt, indexes, rev_withdrawals =
        list_fold_left_m
          (fun (ctxt, indexes, withdrawals) content ->
            let* ctxt, indexes, withdrawal_opt =
              apply_operation_content ctxt indexes signer content
            in
            return (ctxt, indexes, Option.to_list withdrawal_opt @ withdrawals))
          (ctxt, indexes, [])
          contents
      in
      return (ctxt, indexes, rev_withdrawals |> List.rev)

    (** [apply_transaction ctxt indexes transaction] applies each operation in
        the [transaction]. It returns a {!transaction_result}, i.e. either
        every operation in the [transaction] succedeed and the [ctxt] is
        modified, or the [transaction] is a failure and the context
        is left untouched.
    *)
    let apply_transaction :
        ctxt ->
        indexes ->
        (Indexable.index_only, Indexable.unknown) transaction ->
        (ctxt * indexes * transaction_result * Tx_rollup_withdraw.t list) m =
     fun initial_ctxt initial_indexes transaction ->
      let rec fold (ctxt, prev_indexes, withdrawals) index ops =
        match ops with
        | [] -> return (ctxt, prev_indexes, Transaction_success, withdrawals)
        | op :: rst ->
            let* ctxt, indexes, status, withdrawals =
              catch
                (apply_operation ctxt prev_indexes op)
                (fun (ctxt, indexes, op_withdrawals) ->
                  fold
                    (ctxt, indexes, withdrawals @ op_withdrawals)
                    (index + 1)
                    rst)
                (fun reason ->
                  return
                    ( initial_ctxt,
                      initial_indexes,
                      Transaction_failure {index; reason},
                      [] ))
            in
            return (ctxt, indexes, status, withdrawals)
      in
      fold (initial_ctxt, initial_indexes, []) 0 transaction

    (** [update_counters ctxt status transaction] updates the counters for
        the signers of operations in [transaction]. If the [transaction]
        failed because of a [Counter_mismatch] the counters are left
        untouched.
    *)
    let update_counters ctxt status transaction =
      match status with
      | Transaction_failure {reason = Counter_mismatch _; _} -> return ctxt
      | Transaction_failure _ | Transaction_success ->
          list_fold_left_m
            (fun ctxt (op : (Indexable.index_only, _) operation) ->
              Address_metadata.incr_counter ctxt
              @@ address_of_signer_index op.signer)
            ctxt
            transaction

    let apply_batch :
        ctxt ->
        parameters ->
        (Indexable.unknown, Indexable.unknown) t ->
        (ctxt * Message_result.Batch_V1.t * Tx_rollup_withdraw.t list) m =
     fun ctxt parameters batch ->
      let* ctxt, indexes, batch = check_signature ctxt batch in
      let {contents; _} = batch in
      let* ctxt, indexes, rev_results, withdrawals =
        list_fold_left_m
          (fun (prev_ctxt, prev_indexes, results, withdrawals) transaction ->
            let* new_ctxt, new_indexes, status, transaction_withdrawals =
              apply_transaction prev_ctxt prev_indexes transaction
            in
            let* new_ctxt = update_counters new_ctxt status transaction in
            return
              ( new_ctxt,
                new_indexes,
                (transaction, status) :: results,
                withdrawals @ transaction_withdrawals ))
          (ctxt, indexes, [], [])
          contents
      in
      let limit = parameters.tx_rollup_max_withdrawals_per_batch in
      if Compare.List_length_with.(withdrawals > limit) then
        fail
          (Maximum_withdraws_per_message_exceeded
             {current = List.length withdrawals; maximum = limit})
      else
        let results = List.rev rev_results in
        return
          ( ctxt,
            Message_result.Batch_V1.Batch_result {results; indexes},
            withdrawals )
  end

  let apply_deposit :
      ctxt ->
      Tx_rollup_message.deposit ->
      (ctxt * deposit_result * Tx_rollup_withdraw.t option) m =
   fun initial_ctxt Tx_rollup_message.{sender; destination; ticket_hash; amount} ->
    let apply_deposit () =
      let* ctxt, indexes, aidx =
        address_index initial_ctxt empty_indexes destination
      in
      let* ctxt, indexes, tidx =
        ticket_index ctxt indexes Indexable.(value ticket_hash)
      in
      let* ctxt = deposit ctxt aidx tidx amount in
      return (ctxt, indexes)
    in
    catch
      (apply_deposit ())
      (fun (ctxt, indexes) -> return (ctxt, Deposit_success indexes, None))
      (fun reason ->
        (* Should there be an error during the deposit, then return
           the full [amount] to [sender] in the form of a
           withdrawal. *)
        let withdrawal =
          Tx_rollup_withdraw.{claimer = sender; ticket_hash; amount}
        in
        return (initial_ctxt, Deposit_failure reason, Some withdrawal))

  let apply_message :
      ctxt -> parameters -> Tx_rollup_message.t -> (ctxt * Message_result.t) m =
   fun ctxt parameters msg ->
    let open Tx_rollup_message in
    match msg with
    | Deposit deposit ->
        let* ctxt, result, withdrawl_opt = apply_deposit ctxt deposit in
        return (ctxt, (Deposit_result result, Option.to_list withdrawl_opt))
    | Batch str -> (
        let batch =
          Data_encoding.Binary.of_string_opt Tx_rollup_l2_batch.encoding str
        in
        match batch with
        | Some (V1 batch) ->
            let* ctxt, result, withdrawals =
              Batch_V1.apply_batch ctxt parameters batch
            in
            return (ctxt, (Batch_V1_result result, withdrawals))
        | None -> fail Invalid_batch_encoding)
end
back to top