package octez-protocol-019-PtParisB-libs
Octez protocol 019-PtParisB libraries
Install
Dune Dependency
Authors
Maintainers
Sources
tezos-octez-v20.1.tag.bz2
sha256=ddfb5076eeb0b32ac21c1eed44e8fc86a6743ef18ab23fff02d36e365bb73d61
sha512=d22a827df5146e0aa274df48bc2150b098177ff7e5eab52c6109e867eb0a1f0ec63e6bfbb0e3645a6c2112de3877c91a17df32ccbff301891ce4ba630c997a65
doc/src/octez-protocol-019-PtParisB-libs.plugin/mempool.ml.html
Source file mempool.ml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793
(*****************************************************************************) (* *) (* Open Source License *) (* Copyright (c) 2018 Nomadic Development. <contact@tezcore.com> *) (* Copyright (c) 2021-2022 Nomadic Labs, <contact@nomadic-labs.com> *) (* Copyright (c) 2022 TriliTech <contact@trili.tech> *) (* *) (* 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 Protocol open Alpha_context type nanotez = Q.t let nanotez_enc : nanotez Data_encoding.t = let open Data_encoding in def "nanotez" ~title:"A thousandth of a mutez" ~description:"One thousand nanotez make a mutez (1 tez = 1e9 nanotez)" (conv (fun q -> (q.Q.num, q.Q.den)) (fun (num, den) -> {Q.num; den}) (tup2 z z)) let manager_op_replacement_factor_enc : Q.t Data_encoding.t = let open Data_encoding in def "manager operation replacement factor" ~title:"A manager operation's replacement factor" ~description:"The fee and fee/gas ratio of an operation to replace another" (conv (fun q -> (q.Q.num, q.Q.den)) (fun (num, den) -> {Q.num; den}) (tup2 z z)) type config = { minimal_fees : Tez.t; minimal_nanotez_per_gas_unit : nanotez; minimal_nanotez_per_byte : nanotez; clock_drift : Period.t option; replace_by_fee_factor : Q.t; (** Factor by which the fee and fee/gas ratio of an old operation in the mempool are both multiplied to determine the values that a new operation must exceed in order to replace the old operation. See the [better_fees_and_ratio] function further below. *) } let default_minimal_fees = match Tez.of_mutez 100L with None -> assert false | Some t -> t let default_minimal_nanotez_per_gas_unit = Q.of_int 100 let default_minimal_nanotez_per_byte = Q.of_int 1000 let managers_quota = Stdlib.List.nth Main.validation_passes Operation_repr.manager_pass (* If the drift is not specified, it will be the duration of round zero. It allows only to spam with one future round. /!\ Warning /!\ : current plugin implementation implies that this drift cumulates with the accepted drift regarding the current head's timestamp. *) let default_config = { minimal_fees = default_minimal_fees; minimal_nanotez_per_gas_unit = default_minimal_nanotez_per_gas_unit; minimal_nanotez_per_byte = default_minimal_nanotez_per_byte; clock_drift = None; replace_by_fee_factor = Q.make (Z.of_int 105) (Z.of_int 100) (* Default value of [replace_by_fee_factor] is set to 5% *); } let config_encoding : config Data_encoding.t = let open Data_encoding in conv (fun { minimal_fees; minimal_nanotez_per_gas_unit; minimal_nanotez_per_byte; clock_drift; replace_by_fee_factor; } -> ( minimal_fees, minimal_nanotez_per_gas_unit, minimal_nanotez_per_byte, clock_drift, replace_by_fee_factor )) (fun ( minimal_fees, minimal_nanotez_per_gas_unit, minimal_nanotez_per_byte, clock_drift, replace_by_fee_factor ) -> { minimal_fees; minimal_nanotez_per_gas_unit; minimal_nanotez_per_byte; clock_drift; replace_by_fee_factor; }) (obj5 (dft "minimal_fees" Tez.encoding default_config.minimal_fees) (dft "minimal_nanotez_per_gas_unit" nanotez_enc default_config.minimal_nanotez_per_gas_unit) (dft "minimal_nanotez_per_byte" nanotez_enc default_config.minimal_nanotez_per_byte) (opt "clock_drift" Period.encoding) (dft "replace_by_fee_factor" manager_op_replacement_factor_enc default_config.replace_by_fee_factor)) (** Static information to store in the filter state. *) type info = { head : Block_header.shell_header; round_durations : Round.round_durations; hard_gas_limit_per_block : Gas.Arith.integral; head_round : Round.t; round_zero_duration : Period.t; grandparent_level_start : Timestamp.t; } let init_state_prototzresult ~head round_durations hard_gas_limit_per_block = let open Lwt_result_syntax in let*? head_round = Alpha_context.Fitness.round_from_raw head.Tezos_base.Block_header.fitness in let round_zero_duration = Round.round_duration round_durations Round.zero in let*? grandparent_round = Alpha_context.Fitness.predecessor_round_from_raw head.fitness in let*? proposal_level_offset = Round.level_offset_of_round round_durations ~round:Round.(succ grandparent_round) in let*? proposal_round_offset = Round.level_offset_of_round round_durations ~round:head_round in let*? proposal_offset = Period.(add proposal_level_offset proposal_round_offset) in let grandparent_level_start = Timestamp.(head.timestamp - proposal_offset) in return { head; round_durations; hard_gas_limit_per_block; head_round; round_zero_duration; grandparent_level_start; } let init_state ~head round_durations hard_gas_limit_per_block = Lwt.map Environment.wrap_tzresult (init_state_prototzresult ~head round_durations hard_gas_limit_per_block) let init context ~(head : Tezos_base.Block_header.shell_header) = let open Lwt_result_syntax in let* ( ctxt, (_ : Receipt.balance_updates), (_ : Migration.origination_result list) ) = prepare context ~level:(Int32.succ head.level) ~predecessor_timestamp:head.timestamp ~timestamp:head.timestamp |> Lwt.map Environment.wrap_tzresult in let round_durations = Constants.round_durations ctxt in let hard_gas_limit_per_block = Constants.hard_gas_limit_per_block ctxt in init_state ~head round_durations hard_gas_limit_per_block let flush old_info ~head = (* To avoid the need to prepare a context as in [init], we retrieve the [round_durations] from the previous state. Indeed, they are only determined by the [minimal_block_delay] and [delay_increment_per_round] constants (see {!Raw_context.prepare}), and all the constants remain unchanged during the lifetime of a protocol. As to [hard_gas_limit_per_block], it is directly a protocol constant. *) init_state ~head old_info.round_durations old_info.hard_gas_limit_per_block let manager_prio p = `Low p let consensus_prio = `High let other_prio = `Medium let compute_manager_contents_fee_and_gas_limit contents = let open Operation in let l = to_list (Contents_list contents) in List.fold_left (fun acc -> function | Contents (Manager_operation {fee; gas_limit; _}) -> ( match acc with | Error _ as e -> e | Ok (total_fee, total_gas) -> ( match Tez.(total_fee +? fee) with | Ok total_fee -> Ok (total_fee, Gas.Arith.add total_gas gas_limit) | Error _ as e -> e)) | _ -> acc) (Ok (Tez.zero, Gas.Arith.zero)) l type Environment.Error_monad.error += Fees_too_low let () = Environment.Error_monad.register_error_kind `Permanent ~id:"prefilter.fees_too_low" ~title:"Operation fees are too low" ~description:"Operation fees are too low" ~pp:(fun ppf () -> Format.fprintf ppf "Operation fees are too low") Data_encoding.unit (function Fees_too_low -> Some () | _ -> None) (fun () -> Fees_too_low) let size_of_operation op = (WithExceptions.Option.get ~loc:__LOC__ @@ Data_encoding.Binary.fixed_length Tezos_base.Operation.shell_header_encoding) + Data_encoding.Binary.length Operation.protocol_data_encoding op (** Returns the weight and resources consumption of an operation. The weight corresponds to the one implemented by the baker, to decide which operations to put in a block first (the code is largely duplicated). See {!Tezos_baking_alpha.Operation_selection.weight_manager} *) let weight_and_resources_manager_operation ~hard_gas_limit_per_block ?size ~fee ~gas op = let max_size = managers_quota.max_size in let size = match size with None -> size_of_operation op | Some s -> s in let size_f = Q.of_int size in let gas_f = Q.of_bigint (Gas.Arith.integral_to_z gas) in let fee_f = Q.of_int64 (Tez.to_mutez fee) in let size_ratio = Q.(size_f / Q.of_int max_size) in let gas_ratio = Q.(gas_f / Q.of_bigint (Gas.Arith.integral_to_z hard_gas_limit_per_block)) in let resources = Q.max size_ratio gas_ratio in (Q.(fee_f / resources), resources) let pre_filter_manager : type t. info -> config -> Operation.packed_protocol_data -> t Kind.manager contents_list -> [ `Passed_prefilter of Q.t list | `Branch_refused of tztrace | `Branch_delayed of tztrace | `Refused of tztrace | `Outdated of tztrace ] = fun info config packed_op op -> let size = size_of_operation packed_op in let check_gas_and_fee fee gas_limit = let fees_in_nanotez = Q.mul (Q.of_int64 (Tez.to_mutez fee)) (Q.of_int 1000) in let minimal_fees_in_nanotez = Q.mul (Q.of_int64 (Tez.to_mutez config.minimal_fees)) (Q.of_int 1000) in let minimal_fees_for_gas_in_nanotez = Q.mul config.minimal_nanotez_per_gas_unit (Q.of_bigint @@ Gas.Arith.integral_to_z gas_limit) in let minimal_fees_for_size_in_nanotez = Q.mul config.minimal_nanotez_per_byte (Q.of_int size) in if Q.compare fees_in_nanotez (Q.add minimal_fees_in_nanotez (Q.add minimal_fees_for_gas_in_nanotez minimal_fees_for_size_in_nanotez)) >= 0 then `Fees_ok else `Refused [Environment.wrap_tzerror Fees_too_low] in match compute_manager_contents_fee_and_gas_limit op with | Error err -> `Refused (Environment.wrap_tztrace err) | Ok (fee, gas_limit) -> ( match check_gas_and_fee fee gas_limit with | `Refused _ as err -> err | `Fees_ok -> let weight, _op_resources = weight_and_resources_manager_operation ~hard_gas_limit_per_block:info.hard_gas_limit_per_block ~fee ~gas:gas_limit packed_op in `Passed_prefilter [weight]) type Environment.Error_monad.error += Wrong_operation let () = Environment.Error_monad.register_error_kind `Temporary ~id:"prefilter.wrong_operation" ~title:"Wrong operation" ~description:"Failing_noop operations are not accepted in the mempool." ~pp:(fun ppf () -> Format.fprintf ppf "Failing_noop operations are not accepted in the mempool") Data_encoding.unit (function Wrong_operation -> Some () | _ -> None) (fun () -> Wrong_operation) type Environment.Error_monad.error += Consensus_operation_in_far_future let () = Environment.Error_monad.register_error_kind `Branch ~id:"prefilter.Consensus_operation_in_far_future" ~title:"Consensus operation in far future" ~description:"Consensus operation too far in the future are not accepted." ~pp:(fun ppf () -> Format.fprintf ppf "Consensus operation too far in the future are not accepted.") Data_encoding.unit (function Consensus_operation_in_far_future -> Some () | _ -> None) (fun () -> Consensus_operation_in_far_future) (** {2 consensus operation filtering} In Tenderbake, we increased a lot the number of consensus operations, therefore it seems necessary to be able to filter consensus operations that could be produced by a Byzantine baker mis-using its right to produce operations in future rounds or levels. We consider the situation where the head is at level [h_l], round [h_r], and with timestamp [h_ts], with the predecessor of the head being at round [hp_r]. We receive at a time [now] a consensus operation for level [op_l] and round [op_r]. A consensus operation is considered too far in the future, and therefore filtered, if the earliest possible starting time of its round is greater than the current time plus a safety margin of [config.clock_drift]. To consider potential level 2 reorgs, we first compute the expected timestamp of round zero at previous level [hp0_ts], All ops at level p_l and round r' such that time(r') is greater than (now + drift) are deemed too far in the future: h_r op_ts now+drift (h_l,r') hp0_ts h_0 h_l | | | +----+-----+---------+-------------------+--+-----+--------------+----------- | | | | | | | | h_ts h_r end time | now | earliest expected | | | | time of round r' |<----op_r rounds duration -------->| | | |<--------------- operations kept ---->|<-rejected----------... | |<-----------operations considered by the filter -----------... For an operation on a proposal at the next level, we consider the minimum starting time of the operation's round, obtained by assuming that the proposal at the next level was built on top of a proposal at round 0 for the current level, itself based on a proposal at round 0 of previous level. Operations on proposal with higher levels are treated similarly. All ops at the next level and round r' such that timestamp(r') > now+drift are deemed too far in the future. r=0 r=1 h_r now now+drift (h_l+1,r') hp0_ts h_0 h_l h_l | | | +----+---- |-------+----+---------+----------+----------+---------- | | | | | | t0 | h_ts earliest expected | | | | time of round r' |<--- | earliest| | | next level| | | |<---------------------------------->| round_offset(r') *) (** At a given level a consensus operation is acceptable if its earliest expected timestamp, [op_earliest_ts] is below the current clock with an accepted drift for the clock given by a configuration. *) let acceptable ~drift ~op_earliest_ts ~now_timestamp = let open Result_syntax in Timestamp.( let+ now_drifted = now_timestamp +? drift in op_earliest_ts <= now_drifted) (** Check that an operation with the given [op_round], at level [op_level] is likely to be correct, meaning it could have been produced before now (+ the safety margin from configuration). Given an operation at level greater or equal than/to the current level, we compute the expected timestamp of the operation's round. If the operation is at a greater level, we assume that it is based on the proposal at round zero of the current level. All operations whose (level, round) is lower than or equal to the current head are deemed valid. Note that in case where their is a high drift in the computer clock, they might not have been considered valid by comparing their expected timestamp to the clock. This is a stricter than necessary filter as it will reject operations that could be valid in the current timeframe if the proposal they attest is built over a predecessor of the current proposal that would be of lower round than the current one. What can we do that would be smarter: get current head's predecessor round and timestamp to compute the timestamp t0 of a predecessor that would have been proposed at round 0. Timestamp of round at current level for an alternative head that would be based on such proposal would be computed based on t0. For level higher than current head, compute the round's earliest timestamp if all proposal passed at round 0 starting from t0. *) let acceptable_op ~config ~round_durations ~round_zero_duration ~proposal_level ~proposal_round ~proposal_timestamp ~(proposal_predecessor_level_start : Timestamp.t) ~op_level ~op_round ~now_timestamp = let open Result_syntax in if Raw_level.(succ op_level < proposal_level) || (op_level = proposal_level && op_round <= proposal_round) then (* Past and current round operations are not in the future *) (* This case could be handled directly in `pre_filter_far_future_consensus_ops` for a (slightly) better performance. *) Ok true else (* If, by some tolerance on local clock drift, the timestamp of the current head is itself in the future, we use this time instead of now_timestamp *) let now_timestamp = Timestamp.(max now_timestamp proposal_timestamp) in (* Computing when the current level started. *) let drift = Option.value ~default:round_zero_duration config.clock_drift in (* We compute the earliest timestamp possible [op_earliest_ts] for the operation's (level,round), as if all proposals were accepted at round 0 since the previous level. *) (* Invariant: [op_level + 1 >= proposal_level] *) let level_offset = Raw_level.(diff (succ op_level) proposal_level) in let* time_shift = Period.mult level_offset round_zero_duration in let* earliest_op_level_start = Timestamp.(proposal_predecessor_level_start +? time_shift) in (* computing the operations's round start from it's earliest possible level start *) let* op_earliest_ts = Round.timestamp_of_another_round_same_level round_durations ~current_round:Round.zero ~current_timestamp:earliest_op_level_start ~considered_round:op_round in (* We finally check that the expected time of the operation is acceptable *) acceptable ~drift ~op_earliest_ts ~now_timestamp type level_and_round = {level : Raw_level.t; round : Round.t} let pre_filter_far_future_consensus_ops info config ({level = op_level; round = op_round} : level_and_round) : bool Lwt.t = let open Result_syntax in let res = let now_timestamp = Time.System.now () |> Time.System.to_protocol in let* proposal_level = Raw_level.of_int32 info.head.level in acceptable_op ~config ~round_durations:info.round_durations ~round_zero_duration:info.round_zero_duration ~proposal_level ~proposal_round:info.head_round ~proposal_timestamp:info.head.timestamp ~proposal_predecessor_level_start:info.grandparent_level_start ~op_level ~op_round ~now_timestamp in match res with Ok b -> Lwt.return b | Error _ -> Lwt.return_false let prefilter_consensus_operation info config level_and_round = let open Lwt_syntax in let* keep = pre_filter_far_future_consensus_ops info config level_and_round in if keep then return (`Passed_prefilter consensus_prio) else return (`Branch_refused [Environment.wrap_tzerror Consensus_operation_in_far_future]) (** A quasi infinite amount of "valid" (pre)attestations could be sent by a committee member, one for each possible round number. This filter rejects (pre)attestations that refer to a round that could not have been reached within the time span between the last head's timestamp and the current local clock. We add [config.clock_drift] time as a safety margin. *) let pre_filter info config ({shell = _; protocol_data = Operation_data {contents; _} as op} : Main.operation) = let open Lwt_syntax in let prefilter_manager_op manager_op = return @@ match pre_filter_manager info config op manager_op with | `Passed_prefilter prio -> `Passed_prefilter (manager_prio prio) | (`Branch_refused _ | `Branch_delayed _ | `Refused _ | `Outdated _) as err -> err in match contents with | Single (Failing_noop _) -> return (`Refused [Environment.wrap_tzerror Wrong_operation]) | Single (Preattestation consensus_content) | Single (Attestation {consensus_content; dal_content = _}) -> let level_and_round : level_and_round = {level = consensus_content.level; round = consensus_content.round} in prefilter_consensus_operation info config level_and_round | Single (Seed_nonce_revelation _) | Single (Double_preattestation_evidence _) | Single (Double_attestation_evidence _) | Single (Double_baking_evidence _) | Single (Activate_account _) | Single (Proposals _) | Single (Vdf_revelation _) | Single (Drain_delegate _) | Single (Ballot _) -> return (`Passed_prefilter other_prio) | Single (Manager_operation _) as op -> prefilter_manager_op op | Cons (Manager_operation _, _) as op -> prefilter_manager_op op let syntactic_check _ = Lwt.return `Well_formed let is_manager_operation op = match Operation.acceptable_pass op with | Some pass -> Compare.Int.equal pass Operation_repr.manager_pass | None -> false (* Should not fail on a valid manager operation. *) let compute_fee_and_gas_limit {protocol_data = Operation_data data; _} = compute_manager_contents_fee_and_gas_limit data.contents let gas_as_q gas = Gas.Arith.integral_to_z gas |> Q.of_bigint let fee_and_ratio_as_q fee gas = let fee = Tez.to_mutez fee |> Z.of_int64 |> Q.of_bigint in let gas = gas_as_q gas in let ratio = Q.div fee gas in (fee, ratio) let bumped_fee_and_ratio_as_q config fee gas = let bump = Q.mul config.replace_by_fee_factor in let fee, ratio = fee_and_ratio_as_q fee gas in (bump fee, bump ratio) (** Determine whether the new manager operation is sufficiently better than the old manager operation to replace it. Sufficiently better means that the new operation's fee and fee/gas ratio are both greater than or equal to the old operation's same metrics bumped by the factor [config.replace_by_fee_factor]. *) let better_fees_and_ratio config old_gas old_fee new_gas new_fee = let bumped_old_fee, bumped_old_ratio = bumped_fee_and_ratio_as_q config old_fee old_gas in let new_fee, new_ratio = fee_and_ratio_as_q new_fee new_gas in Q.compare new_fee bumped_old_fee >= 0 && Q.compare new_ratio bumped_old_ratio >= 0 (** [conflict_handler config] returns a conflict handler for {!Mempool.add_operation} (see {!Mempool.conflict_handler}). - For non-manager operations, we select the greater operation according to {!Operation.compare}. - A manager operation is replaced only when the new operation's fee and fee/gas ratio both exceed the old operation's by at least a factor of [config.replace_by_fee_factor] (see {!better_fees_and_ratio}). Precondition: both operations must be individually valid (because of the call to {!Operation.compare}). *) let conflict_handler config : Mempool.conflict_handler = let open Result_syntax in fun ~existing_operation ~new_operation -> let (_ : Operation_hash.t), old_op = existing_operation in let (_ : Operation_hash.t), new_op = new_operation in if is_manager_operation old_op && is_manager_operation new_op then let new_op_is_better = let* old_fee, old_gas_limit = compute_fee_and_gas_limit old_op in let* new_fee, new_gas_limit = compute_fee_and_gas_limit new_op in return (better_fees_and_ratio config old_gas_limit old_fee new_gas_limit new_fee) in match new_op_is_better with | Ok b when b -> `Replace | Ok _ | Error _ -> `Keep else if Operation.compare existing_operation new_operation < 0 then `Replace else `Keep let int64_ceil_of_q q = let n = Q.to_int64 q in if Q.(equal q (of_int64 n)) then n else Int64.succ n (* Compute the minimal fee (expressed in mutez) that [candidate_op] would need to have in order for the {!conflict_handler} to let it replace [op_to_replace], when both operations are manager operations. As specified in {!conflict_handler}, this means that [candidate_op] with the returned fee needs to have both its fee and its fee/gas ratio exceed (or match) [op_to_replace]'s same metrics bumped by the {!config}'s [replace_by_fee_factor]. Return [None] when at least one operation is not a manager operation. Also return [None] if both operations are manager operations but there was an error while computing the needed fee. However, note that this cannot happen when both manager operations have been successfully validated by the protocol. *) let fee_needed_to_replace_by_fee config ~op_to_replace ~candidate_op = let open Result_syntax in if is_manager_operation candidate_op && is_manager_operation op_to_replace then (let* _fee, candidate_gas = compute_fee_and_gas_limit candidate_op in let* old_fee, old_gas = compute_fee_and_gas_limit op_to_replace in if Gas.Arith.(old_gas = zero || candidate_gas = zero) then (* This should not happen when both operations are valid. *) return_none else let candidate_gas = gas_as_q candidate_gas in let bumped_old_fee, bumped_old_ratio = bumped_fee_and_ratio_as_q config old_fee old_gas in (* The new operation needs to exceed both the bumped fee and the bumped ratio to make {!better_fees_and_ratio} return [true]. (Having fee or ratio equal to its bumped counterpart is ok too, hence the [ceil] in [int64_ceil_of_q].) *) let fee_needed_for_fee = int64_ceil_of_q bumped_old_fee in let fee_needed_for_ratio = int64_ceil_of_q Q.(bumped_old_ratio * candidate_gas) in return_some (max fee_needed_for_fee fee_needed_for_ratio)) |> Option.of_result |> Option.join else None let find_manager {shell = _; protocol_data = Operation_data {contents; _}} = match contents with | Single (Manager_operation {source; _}) -> Some source | Cons (Manager_operation {source; _}, _) -> Some source | Single ( Preattestation _ | Attestation _ | Proposals _ | Ballot _ | Seed_nonce_revelation _ | Vdf_revelation _ | Double_baking_evidence _ | Double_preattestation_evidence _ | Double_attestation_evidence _ | Activate_account _ | Drain_delegate _ | Failing_noop _ ) -> None (* The purpose of this module is to offer a version of [fee_needed_to_replace_by_fee] where the caller doesn't need to provide the [op_to_replace]. Instead, it needs to call [Conflict_map.update] every time a new operation is added to the mempool. This setup prevents the mempool plugin from exposing the notion of manager operations and their source. *) module Conflict_map = struct (* The state consists in a map of all validated manager operations, indexed by their source. Note that the protocol already enforces that there is at most one operation per source. The state only tracks manager operations because other kinds of operations don't have fees that we might adjust to change the outcome of the {!conflict_handler}, so [fee_needed_to_replace_by_fee] will always return [None] when they are involved anyway. *) type t = packed_operation Signature.Public_key_hash.Map.t let empty = Signature.Public_key_hash.Map.empty (* Remove all the [replacements] from the state, then add [new_operation]. Non-manager operations are ignored. It is important to remove before adding: otherwise, we would remove the newly added operation when it has the same manager as one of the replacements. *) let update conflict_map ~new_operation ~replacements = let conflict_map = List.fold_left (fun conflict_map op -> match find_manager op with | Some manager -> Signature.Public_key_hash.Map.remove manager conflict_map | None -> (* Non-manager operation: ignore it. *) conflict_map) conflict_map replacements in match find_manager new_operation with | Some manager -> Signature.Public_key_hash.Map.add manager new_operation conflict_map | None -> (* Non-manager operation: ignore it. *) conflict_map let fee_needed_to_replace_by_fee config ~candidate_op ~conflict_map = match find_manager candidate_op with | None -> (* Non-manager operation. *) None | Some manager -> ( match Signature.Public_key_hash.Map.find manager conflict_map with | None -> (* This can only happen when the pre-existing conflicting operation is a [Drain_delegate], which cannot be replaced by a manager operation regardless of the latter's fee. *) None | Some op_to_replace -> fee_needed_to_replace_by_fee config ~candidate_op ~op_to_replace) end let fee_needed_to_overtake ~op_to_overtake ~candidate_op = let open Result_syntax in if is_manager_operation candidate_op && is_manager_operation op_to_overtake then (let* _fee, candidate_gas = compute_fee_and_gas_limit candidate_op in let* target_fee, target_gas = compute_fee_and_gas_limit op_to_overtake in if Gas.Arith.(target_gas = zero || candidate_gas = zero) then (* This should not happen when both operations are valid. *) return_none else (* Compute the target ratio as in {!Operation_repr.weight_manager}. We purposefully don't use {!fee_and_ratio_as_q} because the code here needs to stay in sync with {!Operation_repr.weight_manager} rather than {!better_fees_and_ratio}. *) let target_fee = Q.of_int64 (Tez.to_mutez target_fee) in let target_gas = Q.of_bigint (Gas.Arith.integral_to_z target_gas) in let target_ratio = Q.(target_fee / target_gas) in (* Compute the minimal fee needed to have a strictly greater ratio. *) let candidate_gas = Q.of_bigint (Gas.Arith.integral_to_z candidate_gas) in return_some (Int64.succ Q.(to_int64 (target_ratio * candidate_gas)))) |> Option.of_result |> Option.join else None module Internal_for_tests = struct let default_config_with_clock_drift clock_drift = {default_config with clock_drift} let default_config_with_replace_factor replace_by_fee_factor = {default_config with replace_by_fee_factor} let get_clock_drift {clock_drift; _} = clock_drift let acceptable_op = acceptable_op let fee_needed_to_replace_by_fee = fee_needed_to_replace_by_fee end
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>