diff --git a/src/proto_alpha/lib_protocol/sc_rollup_errors.ml b/src/proto_alpha/lib_protocol/sc_rollup_errors.ml index 2dde9a810fb32bf3419581fe96e0a4f1eca19cb9..47d9df611560b8aaf6a11a1dcd4626b48d57c094 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_errors.ml +++ b/src/proto_alpha/lib_protocol/sc_rollup_errors.ml @@ -71,6 +71,7 @@ type error += | (* `Temporary *) Sc_rollup_bad_commitment_serialization | (* `Permanent *) Sc_rollup_address_generation | (* `Permanent *) Sc_rollup_zero_tick_commitment + | (* `Permanent *) Sc_rollup_commitment_past_curfew let () = register_error_kind @@ -448,6 +449,18 @@ let () = Data_encoding.empty (function Sc_rollup_bad_commitment_serialization -> Some () | _ -> None) (fun () -> Sc_rollup_bad_commitment_serialization) ; + let description = "Commitment is past the curfew for this level." in + register_error_kind + `Permanent + ~id:"Sc_rollup_commitment_past_curfew" + ~title:"Commitment past curfew." + ~description: + "A commitment exists for this inbox level for longer than the curfew \ + period." + ~pp:(fun ppf () -> Format.fprintf ppf "%s" description) + Data_encoding.empty + (function Sc_rollup_commitment_past_curfew -> Some () | _ -> None) + (fun () -> Sc_rollup_commitment_past_curfew) ; let description = "Error while generating rollup address" in register_error_kind `Permanent diff --git a/src/proto_alpha/lib_protocol/sc_rollup_stake_storage.ml b/src/proto_alpha/lib_protocol/sc_rollup_stake_storage.ml index 8d28d386a77ac1234c26d715babf4d0deec284ed..8784830939faf674661305c9b6e73c5dfbd9efe9 100644 --- a/src/proto_alpha/lib_protocol/sc_rollup_stake_storage.ml +++ b/src/proto_alpha/lib_protocol/sc_rollup_stake_storage.ml @@ -169,8 +169,40 @@ let assert_commitment_period ctxt rollup commitment = in return ctxt -(** Check invariants on [inbox_level], enforcing overallocation of storage and - regularity of block production. +(** [assert_commitment_is_not_past_curfew ctxt rollup inbox_level] will look in the + storage [Commitment_first_publication_level] for the level of the oldest commit for + [inbox_level] and if it is more than [sc_rollup_challenge_window_in_blocks] + ago it fails with [Sc_rollup_commitment_past_curfew]. Otherwise it adds the + respective storage (if it is not set) and returns the context. *) +let assert_commitment_is_not_past_curfew ctxt rollup inbox_level = + let open Lwt_result_syntax in + let refutation_deadline_blocks = + Int32.of_int @@ Constants_storage.sc_rollup_challenge_window_in_blocks ctxt + in + let current_level = (Raw_context.current_level ctxt).level in + let* ctxt, oldest_commit = + Store.Commitment_first_publication_level.find (ctxt, rollup) inbox_level + in + match oldest_commit with + | Some oldest_commit -> + if + Compare.Int32.( + Raw_level_repr.diff current_level oldest_commit + > refutation_deadline_blocks) + then tzfail Sc_rollup_commitment_past_curfew + else return ctxt + | None -> + (* The storage cost is covered by the stake. *) + let* ctxt, _diff, _existed = + Store.Commitment_first_publication_level.add + (ctxt, rollup) + inbox_level + current_level + in + return ctxt + +(** Check invariants on [inbox_level], enforcing overallocation of storage, + regularity of block production and curfew. The constants used by [assert_refine_conditions_met] must be chosen such that the maximum cost of storage allocated by each staker is at most the size @@ -180,7 +212,10 @@ let assert_refine_conditions_met ctxt rollup lcc commitment = let open Lwt_result_syntax in let* ctxt = assert_commitment_not_too_far_ahead ctxt rollup lcc commitment in let* ctxt = assert_commitment_period ctxt rollup commitment in - return ctxt + assert_commitment_is_not_past_curfew + ctxt + rollup + Commitment.(commitment.inbox_level) let get_commitment_stake_count ctxt rollup node = let open Lwt_result_syntax in @@ -398,6 +433,11 @@ let cement_commitment ctxt rollup new_lcc = [max_number_of_cemented_commitments], if such exist. *) let* ctxt = deallocate_commitment_metadata ctxt rollup old_lcc in + let* ctxt, _freed_size = + Store.Commitment_first_publication_level.remove_existing + (ctxt, rollup) + new_lcc_commitment.inbox_level + in (* Decrease max_number_of_stored_cemented_commitments by one because we start counting commitments from old_lcc, rather than from new_lcc. *) let num_commitments_to_keep = diff --git a/src/proto_alpha/lib_protocol/storage.ml b/src/proto_alpha/lib_protocol/storage.ml index fed23a171b4b1bbae20c271e7f3daf9aec09f62b..ab1eff9872cde7ea4c17bdbd89b371a650ac6cba 100644 --- a/src/proto_alpha/lib_protocol/storage.ml +++ b/src/proto_alpha/lib_protocol/storage.ml @@ -1795,6 +1795,15 @@ module Sc_rollup = struct let encoding = Data_encoding.int32 end) + module Commitment_first_publication_level = + Make_indexed_carbonated_data_storage + (Make_subcontext (Registered) (Indexed_context.Raw_context) + (struct + let name = ["commitment_first_publication_level"] + end)) + (Make_index (Raw_level_repr.Index)) + (Raw_level_repr) + module Commitment_added = Make_indexed_carbonated_data_storage (Make_subcontext (Registered) (Indexed_context.Raw_context) diff --git a/src/proto_alpha/lib_protocol/storage.mli b/src/proto_alpha/lib_protocol/storage.mli index 7e6bc25c3acdda05612cd28d4ed7335b6f96ca10..19930309cd5420be3651cbb4616d1556349f12df 100644 --- a/src/proto_alpha/lib_protocol/storage.mli +++ b/src/proto_alpha/lib_protocol/storage.mli @@ -824,6 +824,23 @@ module Sc_rollup : sig and type value = int32 and type t = Raw_context.t * Sc_rollup_repr.t + (** This storage contains for each rollup and inbox level not yet cemented the + level of publication of the first commitment. This is used to compute the + curfew for a given rollup and inbox level. + + The storage size is bounded for each rollup by + + [max_lookahead / commitment_period] + + Since the storage is cleaned when commitments are cemented, this storage + space is only temporarily bought by stakers with their deposits. + *) + module Commitment_first_publication_level : + Non_iterable_indexed_carbonated_data_storage + with type key = Raw_level_repr.t + and type value = Raw_level_repr.t + and type t = Raw_context.t * Sc_rollup_repr.t + module Commitment_added : Non_iterable_indexed_carbonated_data_storage with type key = Sc_rollup_commitment_repr.Hash.t diff --git a/src/proto_alpha/lib_protocol/test/integration/operations/test_sc_rollup.ml b/src/proto_alpha/lib_protocol/test/integration/operations/test_sc_rollup.ml index df975ae634ab4da92a9a3c3446b7793ae72edb08..229263ab0153e3d5f31e3e6243c9d0fa2427e0f7 100644 --- a/src/proto_alpha/lib_protocol/test/integration/operations/test_sc_rollup.ml +++ b/src/proto_alpha/lib_protocol/test/integration/operations/test_sc_rollup.ml @@ -223,9 +223,15 @@ let number_of_ticks_exn n = | Some x -> x | None -> Stdlib.failwith "Bad Number_of_ticks" -let dummy_commitment ?compressed_state ?(number_of_ticks = 3000L) ctxt rollup = +let dummy_commitment ?predecessor ?compressed_state ?(number_of_ticks = 3000L) + ctxt rollup = let* genesis_info = Context.Sc_rollup.genesis_info ctxt rollup in - let predecessor = genesis_info.commitment_hash in + let predecessor, pred_level = + match predecessor with + | Some pred -> + (Sc_rollup.Commitment.hash_uncarbonated pred, pred.inbox_level) + | None -> (genesis_info.commitment_hash, genesis_info.level) + in let* compressed_state = match compressed_state with | None -> @@ -235,7 +241,6 @@ let dummy_commitment ?compressed_state ?(number_of_ticks = 3000L) ctxt rollup = return compressed_state | Some compressed_state -> return compressed_state in - let root_level = genesis_info.level in let* inbox_level = let+ constants = Context.get_constants ctxt in let Constants.Parametric.{commitment_period_in_blocks = commitment_freq; _} @@ -243,7 +248,7 @@ let dummy_commitment ?compressed_state ?(number_of_ticks = 3000L) ctxt rollup = constants.parametric.sc_rollup in Raw_level.of_int32_exn - (Int32.add (Raw_level.to_int32 root_level) (Int32.of_int commitment_freq)) + (Int32.add (Raw_level.to_int32 pred_level) (Int32.of_int commitment_freq)) in return Sc_rollup.Commitment. @@ -254,6 +259,21 @@ let dummy_commitment ?compressed_state ?(number_of_ticks = 3000L) ctxt rollup = compressed_state; } +let publish_op_and_dummy_commitment ~src ?compressed_state ?predecessor rollup + block = + let compressed_state = + Option.map + (fun s -> + Sc_rollup.State_hash.context_hash_to_state_hash + (Tezos_crypto.Context_hash.hash_string [s])) + compressed_state + in + let* commitment = + dummy_commitment ?compressed_state ?predecessor (B block) rollup + in + let* publish = Op.sc_rollup_publish (B block) src rollup commitment in + return (publish, commitment) + (* Verify that parameters and unparsed parameters match. *) let verify_params ctxt ~parameters_ty ~parameters ~unparsed_parameters = let show exp = Expr.to_string @@ exp in @@ -2178,6 +2198,191 @@ let test_zero_tick_commitment_fails () = in return_unit +(** [test_curfew] creates a rollup, publishes two conflicting + commitments. Branches are expected to continue (commitment are able to be + published). Tries to publish another commitment at the same initial + `inbox_level` after [challenge_window_in_blocks - 1] and after + [challenge_window_in_blocks] blocks. Only the first attempt is expected to + succeed. *) +let test_curfew () = + let open Lwt_result_syntax in + let* block, (account1, account2, account3), rollup = + init_and_originate Context.T3 "unit" + in + let* constants = Context.get_constants (B block) in + let challenge_window = + constants.parametric.sc_rollup.challenge_window_in_blocks + in + let commitment_period = + constants.parametric.sc_rollup.commitment_period_in_blocks + in + let* block = Block.bake_n commitment_period block in + let* publish1, commitment1 = + publish_op_and_dummy_commitment + ~src:account1 + ~compressed_state:"first" + rollup + block + in + let* publish2, commitment2 = + publish_op_and_dummy_commitment + ~src:account2 + ~compressed_state:"second" + rollup + block + in + let* block = Block.bake ~operations:[publish1; publish2] block in + let* block = Block.bake_n (challenge_window - 1) block in + let* publish11, commitment11 = + publish_op_and_dummy_commitment + ~src:account1 + ~predecessor:commitment1 + rollup + block + in + let* publish21, commitment21 = + publish_op_and_dummy_commitment + ~src:account2 + ~predecessor:commitment2 + rollup + block + in + let* publish3, _commitment3 = + publish_op_and_dummy_commitment + ~src:account3 + ~compressed_state:"third" + rollup + block + in + let* block = Block.bake ~operations:[publish11; publish21; publish3] block in + let* publish111, _commitment111 = + publish_op_and_dummy_commitment + ~src:account1 + ~predecessor:commitment11 + rollup + block + in + let* publish211, _commitment211 = + publish_op_and_dummy_commitment + ~src:account2 + ~predecessor:commitment21 + rollup + block + in + let* publish4, _commitment4 = + publish_op_and_dummy_commitment + ~src:account3 + ~compressed_state:"fourth" + rollup + block + in + let* incr = Incremental.begin_construction block in + let* incr = Incremental.add_operation incr publish111 in + let* incr = Incremental.add_operation incr publish211 in + let expect_apply_failure = function + | Environment.Ecoproto_error + (Sc_rollup_errors.Sc_rollup_commitment_past_curfew as e) + :: _ -> + Assert.test_error_encodings e ; + return_unit + | _ -> + failwith "It should have failed with [Sc_rollup_commitment_past_curfew]" + in + let* _incr = Incremental.add_operation ~expect_apply_failure incr publish4 in + return_unit + +(** [test_curfew_is_clean] makes sure that the curfew-related storage is cleaned + when a commitment is cemented. *) +let test_curfew_is_clean () = + let open Lwt_result_syntax in + let* block, account1, rollup = init_and_originate Context.T1 "unit" in + let find_first_publication_level block inbox_level = + let* alpha_ctxt = Block.to_alpha_ctxt block in + let raw_ctxt = Alpha_context.Internal_for_tests.to_raw alpha_ctxt in + let rollup = + Data_encoding.Binary.( + to_bytes_exn Alpha_context.Sc_rollup.Address.encoding rollup + |> of_bytes_exn Protocol.Sc_rollup_repr.Address.encoding) + in + let inbox_level = + Data_encoding.Binary.( + to_bytes_exn Raw_level.encoding inbox_level + |> of_bytes_exn Raw_level_repr.encoding) + in + let* _raw_ctxt, first_publication_level = + Storage.Sc_rollup.Commitment_first_publication_level.find + (raw_ctxt, rollup) + inbox_level + >|= Environment.wrap_tzresult + in + return first_publication_level + in + let* constants = Context.get_constants (B block) in + let challenge_window = + constants.parametric.sc_rollup.challenge_window_in_blocks + in + let commitment_period = + constants.parametric.sc_rollup.commitment_period_in_blocks + in + let* block = Block.bake_n commitment_period block in + let* commitment = dummy_commitment (B block) rollup in + let* operation = Op.sc_rollup_publish (B block) account1 rollup commitment in + let* first_publication_level = + find_first_publication_level block commitment.inbox_level + in + let* () = + match first_publication_level with + | Some _x -> + failwith "The storage should be empty before the first publication." + | None -> return_unit + in + let* block = Block.bake ~operation block in + let* () = + let* first_publication_level = + find_first_publication_level block commitment.inbox_level + in + match first_publication_level with + | Some x -> + assert ( + Int32.equal block.header.shell.level @@ Raw_level_repr.to_int32 x) ; + return_unit + | None -> failwith "The level of publication is expected to exist" + in + let* block = Block.bake_n challenge_window block in + let* cement_op = + let hash = Sc_rollup.Commitment.hash_uncarbonated commitment in + Op.sc_rollup_cement (B block) account1 rollup hash + in + let* block = Block.bake block ~operation:cement_op in + let* first_publication_level = + find_first_publication_level block commitment.inbox_level + in + match first_publication_level with + | Some _x -> + failwith + "The storage should have been cleaned when commitment is cemented" + | None -> return_unit + +(** [test_curfew_period_is_started_only_after_first_publication checks that + publishing the first commitment of a given [inbox_level] after + [inbox_level + challenge_window] is still possible. *) +let test_curfew_period_is_started_only_after_first_publication () = + let open Lwt_result_syntax in + let* block, account1, rollup = init_and_originate Context.T1 "unit" in + let* constants = Context.get_constants (B block) in + let challenge_window = + constants.parametric.sc_rollup.challenge_window_in_blocks + in + let commitment_period = + constants.parametric.sc_rollup.commitment_period_in_blocks + in + let* block = Block.bake_n commitment_period block in + let* block = Block.bake_n challenge_window block in + let* commitment = dummy_commitment (B block) rollup in + let* operation = Op.sc_rollup_publish (B block) account1 rollup commitment in + let* _block = Block.bake ~operation block in + return_unit + let tests = [ Tztest.tztest @@ -2295,4 +2500,14 @@ let tests = "0-tick commitments are forbidden" `Quick test_zero_tick_commitment_fails; + Tztest.tztest "check the curfew functionality" `Quick test_curfew; + Tztest.tztest + "check the curfew storage is cleaned after cement" + `Quick + test_curfew_is_clean; + Tztest.tztest + "check that a commitment can be published after the inbox_level + \ + challenge window is passed." + `Quick + test_curfew_period_is_started_only_after_first_publication; ] diff --git a/tezt/tests/expected/sc_rollup.ml/Alpha- arith - participant of a refutation game are slashed-rewarded.out b/tezt/tests/expected/sc_rollup.ml/Alpha- arith - participant of a refutation game are slashed-rewarded.out index 9d01f02ecc10a9135cee19c1b816373a8d7cb16f..d2a801bd2daf9836eff7a60d378f659d44ff4b1b 100644 --- a/tezt/tests/expected/sc_rollup.ml/Alpha- arith - participant of a refutation game are slashed-rewarded.out +++ b/tezt/tests/expected/sc_rollup.ml/Alpha- arith - participant of a refutation game are slashed-rewarded.out @@ -35,7 +35,7 @@ This sequence of operations was run: ./octez-client --wait none publish commitment from '[PUBLIC_KEY_HASH]' for sc rollup '[SC_ROLLUP_HASH]' with compressed state '[SC_ROLLUP_PVM_STATE_HASH]' at inbox level 4 and predecessor '[SC_ROLLUP_COMMITMENT_HASH]' and number of ticks 1 Node is bootstrapped. -Estimated gas: 5781.719 units (will add 100 for safety) +Estimated gas: 6241.735 units (will add 100 for safety) Estimated storage: no bytes added Operation successfully injected in the node. Operation hash is '[OPERATION_HASH]' @@ -46,13 +46,13 @@ and/or an external block explorer to make sure that it has been included. This sequence of operations was run: Manager signed operations: From: [PUBLIC_KEY_HASH] - Fee to the baker: ꜩ0.00093 + Fee to the baker: ꜩ0.000976 Expected counter: 1 - Gas limit: 5882 + Gas limit: 6342 Storage limit: 0 bytes Balance updates: - [PUBLIC_KEY_HASH] ... -ꜩ0.00093 - payload fees(the block proposer) ....... +ꜩ0.00093 + [PUBLIC_KEY_HASH] ... -ꜩ0.000976 + payload fees(the block proposer) ....... +ꜩ0.000976 Smart contract rollup commitment publishing: Address: [SC_ROLLUP_HASH] Commitment: @@ -61,7 +61,7 @@ This sequence of operations was run: predecessor: [SC_ROLLUP_COMMITMENT_HASH] number_of_ticks: 1 This smart contract rollup commitment publishing was successfully applied - Consumed gas: 5781.719 + Consumed gas: 6241.735 Hash of commit: [SC_ROLLUP_COMMITMENT_HASH] Commitment published at level: 5 Balance updates: @@ -71,7 +71,7 @@ This sequence of operations was run: ./octez-client --wait none publish commitment from '[PUBLIC_KEY_HASH]' for sc rollup '[SC_ROLLUP_HASH]' with compressed state '[SC_ROLLUP_PVM_STATE_HASH]' at inbox level 4 and predecessor '[SC_ROLLUP_COMMITMENT_HASH]' and number of ticks 2 Node is bootstrapped. -Estimated gas: 5781.719 units (will add 100 for safety) +Estimated gas: 6241.727 units (will add 100 for safety) Estimated storage: no bytes added Operation successfully injected in the node. Operation hash is '[OPERATION_HASH]' @@ -82,13 +82,13 @@ and/or an external block explorer to make sure that it has been included. This sequence of operations was run: Manager signed operations: From: [PUBLIC_KEY_HASH] - Fee to the baker: ꜩ0.00093 + Fee to the baker: ꜩ0.000976 Expected counter: 1 - Gas limit: 5882 + Gas limit: 6342 Storage limit: 0 bytes Balance updates: - [PUBLIC_KEY_HASH] ... -ꜩ0.00093 - payload fees(the block proposer) ....... +ꜩ0.00093 + [PUBLIC_KEY_HASH] ... -ꜩ0.000976 + payload fees(the block proposer) ....... +ꜩ0.000976 Smart contract rollup commitment publishing: Address: [SC_ROLLUP_HASH] Commitment: @@ -97,7 +97,7 @@ This sequence of operations was run: predecessor: [SC_ROLLUP_COMMITMENT_HASH] number_of_ticks: 2 This smart contract rollup commitment publishing was successfully applied - Consumed gas: 5781.719 + Consumed gas: 6241.727 Hash of commit: [SC_ROLLUP_COMMITMENT_HASH] Commitment published at level: 6 Balance updates: