diff --git a/src/proto_alpha/lib_protocol/alpha_context.ml b/src/proto_alpha/lib_protocol/alpha_context.ml index 3a70b76db070f6834e18b026639cf36ca3ad52c2..e842dce9c15d33752bade873df55d6e0b58e82b5 100644 --- a/src/proto_alpha/lib_protocol/alpha_context.ml +++ b/src/proto_alpha/lib_protocol/alpha_context.ml @@ -126,6 +126,11 @@ module Gas = struct type error += Gas_limit_too_high = Raw_context.Gas_limit_too_high + type error += Block_quota_exceeded = Raw_context.Block_quota_exceeded + + type error += + | Operation_quota_exceeded = Raw_context.Operation_quota_exceeded + let check_limit = Raw_context.check_gas_limit let set_limit = Raw_context.set_gas_limit diff --git a/src/proto_alpha/lib_protocol/gas_limit_repr.ml b/src/proto_alpha/lib_protocol/gas_limit_repr.ml index d1fd63281d4fe780291814b2a7f5666574f06fa6..54eb619aebfa55805aa4d590a9e1e0dce8397a1b 100644 --- a/src/proto_alpha/lib_protocol/gas_limit_repr.ml +++ b/src/proto_alpha/lib_protocol/gas_limit_repr.ml @@ -61,10 +61,6 @@ let cost_encoding = Data_encoding.z let pp_cost fmt z = Z.pp_print fmt z -type error += Block_quota_exceeded (* `Temporary *) - -type error += Operation_quota_exceeded (* `Temporary *) - let allocation_weight = Z.of_int (scaling_factor * 2) let step_weight = Z.of_int scaling_factor @@ -79,23 +75,10 @@ let byte_written_weight = Z.of_int (scaling_factor * 15) let cost_to_milligas (cost : cost) : Arith.fp = Arith.unsafe_fp cost -let raw_consume block_gas operation_gas cost = - match operation_gas with - | Unaccounted -> - ok (block_gas, Unaccounted) - | Limited {remaining} -> - let gas = cost_to_milligas cost in - if Arith.(gas > zero) then - let remaining = Arith.sub remaining gas in - let block_remaining = Arith.sub block_gas gas in - if Arith.(remaining < zero) then error Operation_quota_exceeded - else if Arith.(block_remaining < zero) then error Block_quota_exceeded - else ok (block_remaining, Limited {remaining}) - else ok (block_gas, operation_gas) - -let raw_check_enough block_gas operation_gas cost = - raw_consume block_gas operation_gas cost - >|? fun (_block_remaining, _remaining) -> () +let raw_consume gas_counter cost = + let gas = cost_to_milligas cost in + let remaining = Arith.sub gas_counter gas in + if Arith.(remaining < zero) then None else Some remaining let alloc_cost n = Z.mul allocation_weight (Z.succ n) @@ -116,26 +99,3 @@ let ( +@ ) x y = Z.add x y let ( *@ ) x y = Z.mul x y let alloc_mbytes_cost n = alloc_cost (Z.of_int 12) +@ alloc_bytes_cost n - -let () = - let open Data_encoding in - register_error_kind - `Temporary - ~id:"gas_exhausted.operation" - ~title:"Gas quota exceeded for the operation" - ~description: - "A script or one of its callee took more time than the operation said \ - it would" - empty - (function Operation_quota_exceeded -> Some () | _ -> None) - (fun () -> Operation_quota_exceeded) ; - register_error_kind - `Temporary - ~id:"gas_exhausted.block" - ~title:"Gas quota exceeded for the block" - ~description: - "The sum of gas consumed by all the operations in the block exceeds the \ - hard gas limit per block" - empty - (function Block_quota_exceeded -> Some () | _ -> None) - (fun () -> Block_quota_exceeded) diff --git a/src/proto_alpha/lib_protocol/gas_limit_repr.mli b/src/proto_alpha/lib_protocol/gas_limit_repr.mli index d4668cae0ee891afb6d33bd381821cd398537228..a990ac58726585064217ff796ea8a24fa57448a1 100644 --- a/src/proto_alpha/lib_protocol/gas_limit_repr.mli +++ b/src/proto_alpha/lib_protocol/gas_limit_repr.mli @@ -37,13 +37,7 @@ val cost_encoding : cost Data_encoding.encoding val pp_cost : Format.formatter -> cost -> unit -type error += Block_quota_exceeded (* `Temporary *) - -type error += Operation_quota_exceeded (* `Temporary *) - -val raw_consume : Arith.fp -> t -> cost -> (Arith.fp * t) tzresult - -val raw_check_enough : Arith.fp -> t -> cost -> unit tzresult +val raw_consume : Arith.fp -> cost -> Arith.fp option val free : cost diff --git a/src/proto_alpha/lib_protocol/raw_context.ml b/src/proto_alpha/lib_protocol/raw_context.ml index a829c1f7648e6175a3355880d76d23cd471f5d4a..d598b4b35dd2bb0ba61b461d2f4afa6583e77539 100644 --- a/src/proto_alpha/lib_protocol/raw_context.ml +++ b/src/proto_alpha/lib_protocol/raw_context.ml @@ -25,6 +25,42 @@ module Int_set = Set.Make (Compare.Int) +(* + + Gas levels maintainance + ======================= + + The context maintains two levels of gas, one corresponds to the gas + available for the current operation while the other is the gas + available for the current block. + + When gas is consumed, we must morally decrement these two levels to + check if one of them hits zero. However, since these decrements are + the same on both levels, it is not strictly necessary to update the + two levels: we can simply maintain the minimum of both levels in a + [gas_counter]. The meaning of [gas_counter] is denoted by + [gas_counter_status]: *) + +type gas_counter_status = + (* When the operation gas is unaccounted: *) + | Unlimited_operation_gas + (* When the operation gas level is the minimum: *) + | Count_operation_gas of {block_gas_delta : Gas_limit_repr.Arith.fp} + (* When the block gas level is the minimum. *) + | Count_block_gas of {operation_gas_delta : Gas_limit_repr.Arith.fp} + +(* + In each case, we keep enough information in [gas_counter_status] to + reconstruct the level that is not represented by [gas_counter]. In + the gas [Unlimited_operation_gas], the block gas level is stored + in [gas_counter]. + + [Raw_context] interface provides two accessors for the operation + gas level and the block gas level. These accessors compute these values + on-the-fly based on the current value of [gas_counter] and + [gas_counter_status]. + +*) type t = { context : Context.t; constants : Constants_repr.parametric; @@ -39,16 +75,19 @@ type t = { (Signature.Public_key.t * int list * bool) Signature.Public_key_hash.Map.t; fees : Tez_repr.t; rewards : Tez_repr.t; - block_gas : Gas_limit_repr.Arith.fp; - operation_gas : Gas_limit_repr.t; storage_space_to_pay : Z.t option; allocated_contracts : int option; origination_nonce : Contract_repr.origination_nonce option; temporary_lazy_storage_ids : Lazy_storage_kind.Temp_ids.t; internal_nonce : int; internal_nonces_used : Int_set.t; + gas_counter : Gas_limit_repr.Arith.fp; + gas_counter_status : gas_counter_status; } +let update_gas_counter_status ctxt gas_counter_status = + {ctxt with gas_counter_status} + type context = t type root_context = t @@ -99,6 +138,10 @@ let included_endorsements ctxt = ctxt.included_endorsements type error += Too_many_internal_operations (* `Permanent *) +type error += Block_quota_exceeded (* `Temporary *) + +type error += Operation_quota_exceeded (* `Temporary *) + let () = let open Data_encoding in register_error_kind @@ -109,7 +152,27 @@ let () = "A transaction exceeded the hard limit of internal operations it can emit" empty (function Too_many_internal_operations -> Some () | _ -> None) - (fun () -> Too_many_internal_operations) + (fun () -> Too_many_internal_operations) ; + register_error_kind + `Temporary + ~id:"gas_exhausted.operation" + ~title:"Gas quota exceeded for the operation" + ~description: + "A script or one of its callee took more time than the operation said \ + it would" + empty + (function Operation_quota_exceeded -> Some () | _ -> None) + (fun () -> Operation_quota_exceeded) ; + register_error_kind + `Temporary + ~id:"gas_exhausted.block" + ~title:"Gas quota exceeded for the block" + ~description: + "The sum of gas consumed by all the operations in the block exceeds the \ + hard gas limit per block" + empty + (function Block_quota_exceeded -> Some () | _ -> None) + (fun () -> Block_quota_exceeded) let fresh_internal_nonce ctxt = if Compare.Int.(ctxt.internal_nonce >= 65_535) then @@ -209,6 +272,24 @@ let () = (function Gas_limit_too_high -> Some () | _ -> None) (fun () -> Gas_limit_too_high) +let gas_level ctxt = + let open Gas_limit_repr in + match ctxt.gas_counter_status with + | Unlimited_operation_gas -> + Unaccounted + | Count_block_gas {operation_gas_delta} -> + Limited {remaining = Arith.(add ctxt.gas_counter operation_gas_delta)} + | Count_operation_gas _ -> + Limited {remaining = ctxt.gas_counter} + +let block_gas_level ctxt = + let open Gas_limit_repr in + match ctxt.gas_counter_status with + | Unlimited_operation_gas | Count_block_gas _ -> + ctxt.gas_counter + | Count_operation_gas {block_gas_delta} -> + Arith.(add ctxt.gas_counter block_gas_delta) + let check_gas_limit ctxt (remaining : 'a Gas_limit_repr.Arith.t) = if Gas_limit_repr.Arith.( @@ -218,21 +299,46 @@ let check_gas_limit ctxt (remaining : 'a Gas_limit_repr.Arith.t) = else ok_unit let set_gas_limit ctxt (remaining : 'a Gas_limit_repr.Arith.t) = - let remaining = Gas_limit_repr.Arith.fp remaining in - {ctxt with operation_gas = Limited {remaining}} + let open Gas_limit_repr in + let remaining = Arith.fp remaining in + let block_gas = block_gas_level ctxt in + let (gas_counter_status, gas_counter) = + if Arith.(remaining < block_gas) then + let block_gas_delta = Arith.sub block_gas remaining in + (Count_operation_gas {block_gas_delta}, remaining) + else + let operation_gas_delta = Arith.sub remaining block_gas in + (Count_block_gas {operation_gas_delta}, block_gas) + in + let ctxt = update_gas_counter_status ctxt gas_counter_status in + {ctxt with gas_counter} -let set_gas_unlimited ctxt = {ctxt with operation_gas = Unaccounted} +let set_gas_unlimited ctxt = + let block_gas = block_gas_level ctxt in + let ctxt = {ctxt with gas_counter = block_gas} in + update_gas_counter_status ctxt Unlimited_operation_gas -let consume_gas ctxt cost = - Gas_limit_repr.raw_consume ctxt.block_gas ctxt.operation_gas cost - >>? fun (block_gas, operation_gas) -> ok {ctxt with block_gas; operation_gas} +let is_gas_unlimited ctxt = + match ctxt.gas_counter_status with + | Unlimited_operation_gas -> + true + | _ -> + false -let check_enough_gas ctxt cost = - Gas_limit_repr.raw_check_enough ctxt.block_gas ctxt.operation_gas cost +let is_counting_block_gas ctxt = + match ctxt.gas_counter_status with Count_block_gas _ -> true | _ -> false -let gas_level ctxt = ctxt.operation_gas +let consume_gas ctxt cost = + if is_gas_unlimited ctxt then ok ctxt + else + match Gas_limit_repr.raw_consume ctxt.gas_counter cost with + | Some gas_counter -> + Ok {ctxt with gas_counter} + | None -> + if is_counting_block_gas ctxt then error Block_quota_exceeded + else error Operation_quota_exceeded -let block_gas_level ctxt = ctxt.block_gas +let check_enough_gas ctxt cost = consume_gas ctxt cost >>? fun _ -> ok_unit let gas_consumed ~since ~until = match (gas_level since, gas_level until) with @@ -530,15 +636,15 @@ let prepare ~level ~predecessor_timestamp ~timestamp ~fitness ctxt = fees = Tez_repr.zero; rewards = Tez_repr.zero; deposits = Signature.Public_key_hash.Map.empty; - operation_gas = Unaccounted; storage_space_to_pay = None; allocated_contracts = None; - block_gas = + gas_counter = Gas_limit_repr.Arith.fp constants.Constants_repr.hard_gas_limit_per_block; origination_nonce = None; temporary_lazy_storage_ids = Lazy_storage_kind.Temp_ids.init; internal_nonce = 0; internal_nonces_used = Int_set.empty; + gas_counter_status = Unlimited_operation_gas; } type previous_protocol = Genesis of Parameters_repr.t | Edo_008 @@ -635,6 +741,10 @@ module type T = sig val absolute_key : context -> key -> key + type error += Block_quota_exceeded + + type error += Operation_quota_exceeded + val consume_gas : context -> Gas_limit_repr.cost -> context tzresult val check_enough_gas : context -> Gas_limit_repr.cost -> unit tzresult diff --git a/src/proto_alpha/lib_protocol/raw_context.mli b/src/proto_alpha/lib_protocol/raw_context.mli index 27c22851ddadb88b74bcdb37f43bd6c5f62fd445..de829aba5d4baf1184bcfb8d0d278281a37ac31a 100644 --- a/src/proto_alpha/lib_protocol/raw_context.mli +++ b/src/proto_alpha/lib_protocol/raw_context.mli @@ -236,8 +236,16 @@ module type T = sig from partial key relative a view. *) val absolute_key : context -> key -> key + (** Raised if block gas quota is exhausted during gas + consumption. *) + type error += Block_quota_exceeded + + (** Raised if operation gas quota is exhausted during gas + consumption. *) + type error += Operation_quota_exceeded + (** Internally used in {!Storage_functors} to consume gas from - within a view. *) + within a view. May raise {!Block_quota_exceeded} or {!Operation_quota_exceeded}. *) val consume_gas : context -> Gas_limit_repr.cost -> context tzresult (** Check if consume_gas will fail *) diff --git a/src/proto_alpha/lib_protocol/storage_functors.ml b/src/proto_alpha/lib_protocol/storage_functors.ml index 77f8d6cd16e2b2de57e159ff24757ed741cbb874..132d0d4bde19fc84fa485de526c7d8a108ebf145 100644 --- a/src/proto_alpha/lib_protocol/storage_functors.ml +++ b/src/proto_alpha/lib_protocol/storage_functors.ml @@ -115,6 +115,10 @@ module Make_subcontext (R : REGISTER) (C : Raw_context.T) (N : NAME) : let absolute_key c k = C.absolute_key c (to_key k) + type error += Block_quota_exceeded = C.Block_quota_exceeded + + type error += Operation_quota_exceeded = C.Operation_quota_exceeded + let consume_gas = C.consume_gas let check_enough_gas = C.check_enough_gas @@ -771,6 +775,10 @@ module Make_indexed_subcontext (C : Raw_context.T) (I : INDEX) : let (t, i) = unpack c in C.absolute_key t (to_key i k) + type error += Block_quota_exceeded = C.Block_quota_exceeded + + type error += Operation_quota_exceeded = C.Operation_quota_exceeded + let consume_gas c g = let (t, i) = unpack c in C.consume_gas t g >>? fun t -> ok (pack t i) diff --git a/src/proto_alpha/lib_protocol/test/gas_levels.ml b/src/proto_alpha/lib_protocol/test/gas_levels.ml new file mode 100644 index 0000000000000000000000000000000000000000..7cfef33cb085ff4ddb4d67e21742803951aeb174 --- /dev/null +++ b/src/proto_alpha/lib_protocol/test/gas_levels.ml @@ -0,0 +1,134 @@ +(*****************************************************************************) +(* *) +(* 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. *) +(* *) +(*****************************************************************************) + +open Test +open Protocol +open Raw_context + +exception Gas_levels_test_error of string + +let err x = Exn (Gas_levels_test_error x) + +let succeed x = match x with Ok _ -> true | _ -> false + +let failed x = not (succeed x) + +let dummy_context () = + Context.init 1 + >>=? fun (block, _) -> + Raw_context.prepare + ~level:Int32.zero + ~predecessor_timestamp:Time.Protocol.epoch + ~timestamp:Time.Protocol.epoch + ~fitness:[] + (block.context : Environment_context.Context.t) + >|= Environment.wrap_error + +let detect_gas_exhaustion_in_fresh_context () = + dummy_context () + >>=? fun context -> + fail_unless + (consume_gas context (Z.of_int max_int) |> succeed) + (err "In a fresh context, gas consumption is unlimited.") + +let make_context initial_operation_gas = + dummy_context () + >>=? fun context -> + return + ( Gas_limit_repr.Arith.integral_of_int initial_operation_gas + |> set_gas_limit context ) + +let detect_gas_exhaustion_when_operation_gas_hits_zero () = + make_context 10 + >>=? fun context -> + fail_unless + (consume_gas context (Z.of_int max_int) |> failed) + (err "Fail when consuming more than the remaining operation gas.") + +let detect_gas_exhaustion_when_block_gas_hits_zero () = + make_context max_int + >>=? fun context -> + fail_unless + (consume_gas context (Z.of_int max_int) |> failed) + (err "Fail when consuming more than the remaining block gas.") + +let monitor initial_operation_level gas_level expectation () = + let open Gas_limit_repr.Arith in + make_context initial_operation_level + >>=? fun context -> + fail_unless + ( match consume_gas context (Z.of_int 10000) (* in milligas. *) with + | Ok context -> + let remaining = gas_level context in + remaining = integral_of_int expectation + | _ -> + false ) + (err "Monitor operation gas at each gas consumption") + +let operation_gas_level context = + match gas_level context with + | Gas_limit_repr.Limited {remaining} -> + remaining + | _ -> + (* because this function is called after [set_gas_limit]. *) + assert false + +(* + + Monitoring runs differently depending on the minimum between the + operation gas level and the block gas level. Hence, we check that + in both situations, the gas levels are correctly reported. + +*) +let monitor_operation_gas_level = monitor 100 operation_gas_level 90 + +let monitor_operation_gas_level' = + monitor max_int operation_gas_level (max_int - 10) + +let monitor_block_gas_level = monitor 100 block_gas_level 10399990 + +let monitor_block_gas_level' = monitor max_int block_gas_level 10399990 + +let quick (what, how) = tztest what `Quick how + +let tests = + List.map + quick + [ ( "Detect gas exhaustion in fresh context", + detect_gas_exhaustion_in_fresh_context ); + ( "Detect gas exhaustion when operation gas as hits zero", + detect_gas_exhaustion_when_operation_gas_hits_zero ); + ( "Detect gas exhaustion when block gas as hits zero", + detect_gas_exhaustion_when_block_gas_hits_zero ); + ( "Each gas consumption impacts operation gas level (operation < block)", + monitor_operation_gas_level ); + ( "Each gas consumption impacts operation gas level (block < operation)", + monitor_operation_gas_level' ); + ( "Each gas consumption has an impact on block gas level (operation < \ + block)", + monitor_block_gas_level ); + ( "Each gas consumption has an impact on block gas level (block < \ + operation)", + monitor_block_gas_level' ) ] diff --git a/src/proto_alpha/lib_protocol/test/main.ml b/src/proto_alpha/lib_protocol/test/main.ml index f1f32be10d8872654d8a6dd09d4e6bb8f8958e4d..fcfdc965fb33bd5e18b7318a4013fdca0e447078 100644 --- a/src/proto_alpha/lib_protocol/test/main.ml +++ b/src/proto_alpha/lib_protocol/test/main.ml @@ -44,6 +44,7 @@ let () = ("typechecking", Typechecking.tests); ("gas properties", Gas_properties.tests); ("fixed point computation", Fixed_point.tests); + ("gas levels", Gas_levels.tests); ("gas cost functions", Gas_costs.tests); ("lazy storage diff", Lazy_storage_diff.tests); ("sapling", Test_sapling.tests);