Skip to content

Commit 047c3c9

Browse files
authored
Merge pull request #18022 from MinaProtocol/cjjdespres/rescue-vesting-parameter-update
Move the vesting parameter update code out of the slot reduction test module
2 parents 623b678 + e8545ab commit 047c3c9

File tree

3 files changed

+246
-194
lines changed

3 files changed

+246
-194
lines changed

src/lib/mina_base/account.ml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,16 @@ let liquid_balance_at_slot ~global_slot (account : t) =
911911
~vesting_period ~vesting_increment ~initial_minimum_balance ) )
912912
|> Option.value_exn
913913

914+
(** Apply the vesting parameter update to an account's timing information. See
915+
[Timing.Slot_reduction_update] for usage notes. *)
916+
let slot_reduction_update ~hardfork_slot (account : t) =
917+
let timing =
918+
account.timing |> Timing.to_record
919+
|> Timing.slot_reduction_update ~hardfork_slot
920+
|> Timing.of_record
921+
in
922+
{ account with timing }
923+
914924
let gen : t Quickcheck.Generator.t =
915925
let open Quickcheck.Let_syntax in
916926
let%bind public_key = Public_key.Compressed.gen in

src/lib/mina_base/account_timing.ml

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,3 +307,216 @@ let if_ b ~(then_ : var) ~(else_ : var) =
307307
let deriver obj =
308308
let open Fields_derivers_zkapps in
309309
iso_record ~to_record ~of_record As_record.deriver obj
310+
311+
(** A module defining the vesting parameter update procedure from the slot
312+
reduction MIP. See
313+
https://github.com/MinaProtocol/MIPs/blob/main/MIPS/mip-0006-slot-reduction-90s.md.
314+
To summarize, this vesting parameter update is intended to be applied as an
315+
edit to the ledgers during a hard fork that reduces the slot time by half.
316+
The update only applies to accounts that are actively vesting at the time of
317+
the hard fork; these are accounts that either have not reached their cliff
318+
time yet, or have reached their cliff time but still have a positive minimum
319+
balance. The update procedure does the best it can to adjust the account
320+
timing information so that actively vesting accounts will still unlock funds
321+
at the same system times as they would have had the hard fork not occurred.
322+
It will do this perfectly for any account with a "reasonable" vesting
323+
schedule, i.e., one that will finish before half the remaining life of the
324+
chain (at the time of the hard fork) has elapsed.
325+
326+
An important note on the parameters: the slot at which the hard fork occurs
327+
is called [hardfork_slot] below. This slot is the global slot since genesis
328+
that is set in the genesis constants of the new (post hard fork) chain. It
329+
is not the global slot of the hard fork block, nor the [slot_tx_end], nor
330+
the [slot_chain_end]. The update procedure also assumes that the genesis
331+
timestamp of the new chain is set to be equal to the timestamp of the
332+
[hardfork_slot] {i relative to the old (pre hard fork) chain}. This
333+
simplifies the equations.
334+
*)
335+
module Slot_reduction_update = struct
336+
open Unsigned
337+
338+
(** A form of [As_record.t] where all the fields are lifted to
339+
the same [UInt64.t] type, so the arithmetic in the vesting update
340+
equations becomes simpler. *)
341+
type t =
342+
( unit
343+
, Unsigned_extended.UInt64.t
344+
, Unsigned_extended.UInt64.t
345+
, Unsigned_extended.UInt64.t
346+
, Unsigned_extended.UInt64.t )
347+
As_record.t
348+
[@@deriving equal, sexp_of]
349+
350+
let clamp_uint64_to_uint32 x =
351+
UInt64.(
352+
if compare x (of_uint32 UInt32.max_int) <= 0 then to_uint32 x
353+
else UInt32.max_int)
354+
355+
let of_record (t : as_record) : t =
356+
{ is_timed = ()
357+
; initial_minimum_balance = t.initial_minimum_balance |> Balance.to_uint64
358+
; cliff_time =
359+
t.cliff_time |> Global_slot_since_genesis.to_uint32 |> UInt64.of_uint32
360+
; cliff_amount = t.cliff_amount |> Amount.to_uint64
361+
; vesting_period =
362+
t.vesting_period |> Global_slot_span.to_uint32 |> UInt64.of_uint32
363+
; vesting_increment = t.vesting_increment |> Amount.to_uint64
364+
}
365+
366+
(** Convert to a regular [as_record] by clamping all the values
367+
that might be out of range, as specified in the MIP *)
368+
let to_record (t : t) : as_record =
369+
{ is_timed = true
370+
; initial_minimum_balance = t.initial_minimum_balance |> Balance.of_uint64
371+
; cliff_time =
372+
t.cliff_time |> clamp_uint64_to_uint32
373+
|> Global_slot_since_genesis.of_uint32
374+
; cliff_amount = t.cliff_amount |> Amount.of_uint64
375+
; vesting_period =
376+
t.vesting_period |> clamp_uint64_to_uint32 |> Global_slot_span.of_uint32
377+
; vesting_increment = t.vesting_increment |> Amount.of_uint64
378+
}
379+
380+
(** Calculate the total number of iterations needed for this account to vest
381+
completely, beyond the initial cliff unlock. This is [None] (undefined) if
382+
the vesting increment is zero and the cliff amount is smaller than the
383+
initial minimum balance. *)
384+
let vesting_iterations (t : t) : UInt64.t option =
385+
UInt64.(
386+
if compare t.initial_minimum_balance t.cliff_amount <= 0 then
387+
(* Account will complete vesting instantly at the cliff *)
388+
Some zero
389+
else if equal t.vesting_increment zero then
390+
(* Number of iterations is undefined - account is permanently stuck with
391+
a minimum balance *)
392+
None
393+
else
394+
let balance_to_unlock =
395+
Infix.(t.initial_minimum_balance - t.cliff_amount)
396+
in
397+
let full_increment_iterations =
398+
Infix.(balance_to_unlock / t.vesting_increment)
399+
in
400+
if equal Infix.(balance_to_unlock mod t.vesting_increment) zero then
401+
(* The account unlocks an equal amount of funds during each iteration *)
402+
Some full_increment_iterations
403+
else
404+
(* The account needs one more iteration to unlock the last little bit
405+
of funds. Note: if this happens, then full_increment_iterations
406+
will necessarily be well below UInt64.max_int, because division by
407+
t.vesting_increment will have decreased balance_to_unlock by a
408+
factor of at least two. *)
409+
Some Infix.(full_increment_iterations + one))
410+
411+
(** True if an account has started vesting but the slot at which it completes
412+
vesting is still in the future *)
413+
let is_partially_vested ~global_slot (t : t) =
414+
let global_slot =
415+
global_slot |> Global_slot_since_genesis.to_uint32 |> UInt64.of_uint32
416+
in
417+
match vesting_iterations t with
418+
| None ->
419+
false
420+
| Some iterations ->
421+
UInt64.(
422+
compare global_slot t.cliff_time >= 0
423+
&& compare iterations
424+
Infix.((global_slot - t.cliff_time) / t.vesting_period)
425+
> 0)
426+
427+
(** True if an account has not started vesting *)
428+
let not_yet_vesting ~global_slot (t : t) =
429+
let global_slot =
430+
global_slot |> Global_slot_since_genesis.to_uint32 |> UInt64.of_uint32
431+
in
432+
UInt64.compare global_slot t.cliff_time < 0
433+
434+
(** True if an account is actively vesting, as defined by the slot reduction
435+
MIP. Note that this is (almost) equivalent to the minimum balance of the
436+
account being positive at [global_slot].
437+
438+
One subtlety: this is not equivalent to the statement "the account
439+
unlocked funds at [global_slot]". Once an account has a minimum balance of
440+
zero, it is no longer considered to be participating in the vesting
441+
system. In particular, at the [final_vesting_slot] of the timing the
442+
account will have unlocked funds, and yet it will not be actively vesting
443+
at that slot. (Unlocking funds happens between slots, so to speak).
444+
445+
Second subtlety: this isn't exactly equivalent to the minimum balance of
446+
an account being positive. If an account has zero [vesting_increment] and
447+
didn't vest completely at [cliff_time], then it will be stuck with a
448+
permanent positive minimum balance. Such accounts are not actively
449+
vesting. See [vesting_iterations]. *)
450+
let is_actively_vesting ~global_slot (t : t) =
451+
not_yet_vesting ~global_slot t || is_partially_vested ~global_slot t
452+
453+
(** Hardfork adjustment assuming that t is actively vesting *)
454+
let actively_vesting_hardfork_adjustment ~hardfork_slot (t : t) =
455+
let hardfork_slot =
456+
hardfork_slot |> Global_slot_since_genesis.to_uint32 |> UInt64.of_uint32
457+
in
458+
UInt64.(
459+
if compare hardfork_slot t.cliff_time < 0 then
460+
(* t has not started vesting *)
461+
{ t with
462+
cliff_time =
463+
(* global_slot and cliff_time are in the uint32 range, so this will
464+
not wrap *)
465+
Infix.(hardfork_slot + ((t.cliff_time - hardfork_slot) * of_int 2))
466+
; vesting_period =
467+
(* vesting period is in the uint32 range, so this will not wrap *)
468+
Infix.(of_int 2 * t.vesting_period)
469+
}
470+
else
471+
(* t is partially but not fully vested *)
472+
{ t with
473+
initial_minimum_balance =
474+
(let balance_after_cliff =
475+
assert (compare t.initial_minimum_balance t.cliff_amount > 0) ;
476+
Infix.(t.initial_minimum_balance - t.cliff_amount)
477+
in
478+
let elapsed_vesting_periods =
479+
assert (compare t.vesting_period zero > 0) ;
480+
Infix.((hardfork_slot - t.cliff_time) / t.vesting_period)
481+
in
482+
let incremental_unlocked_balance =
483+
assert (
484+
equal elapsed_vesting_periods zero
485+
|| compare t.vesting_increment
486+
(div max_int elapsed_vesting_periods)
487+
<= 0 ) ;
488+
Infix.(t.vesting_increment * elapsed_vesting_periods)
489+
in
490+
assert (
491+
compare balance_after_cliff incremental_unlocked_balance >= 0 ) ;
492+
Infix.(balance_after_cliff - incremental_unlocked_balance) )
493+
; cliff_time =
494+
(* All the times and spans are in the uint32 range, so none of this
495+
will overflow *)
496+
Infix.(
497+
hardfork_slot
498+
+ of_int 2
499+
* ( t.vesting_period
500+
- ((hardfork_slot - t.cliff_time) mod t.vesting_period) ))
501+
; cliff_amount = t.vesting_increment
502+
; vesting_period =
503+
(* vesting_period is in the uint32 range, so this will not wrap *)
504+
Infix.(of_int 2 * t.vesting_period)
505+
})
506+
507+
(** Apply the hardfork adjustment to the given timing, doing nothing if it is
508+
not actively vesting *)
509+
let hardfork_adjustment ~hardfork_slot (t : t) =
510+
if is_actively_vesting ~global_slot:hardfork_slot t then
511+
actively_vesting_hardfork_adjustment ~hardfork_slot t
512+
else t
513+
end
514+
515+
(** Apply the slot reduction update to the [as_record] timing. This does nothing
516+
if the account is not actively vesting at [hardfork_slot]. See the
517+
[Slot_reduction_update] module documentation for general usage notes. *)
518+
let slot_reduction_update ~hardfork_slot (t : as_record) =
519+
if t.is_timed then
520+
Slot_reduction_update.(
521+
t |> of_record |> hardfork_adjustment ~hardfork_slot |> to_record)
522+
else t

0 commit comments

Comments
 (0)