Skip to content

Commit 5f63155

Browse files
committed
Put slot reduction update code in account_timing
The code implementing the vesting parameter update for the slot reduction MIP needs to be accessible in more places than just the tests for that code. In particular, it will need to be used in the hard fork account migration method.
1 parent fe8d307 commit 5f63155

File tree

2 files changed

+234
-194
lines changed

2 files changed

+234
-194
lines changed

src/lib/mina_base/account_timing.ml

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,3 +307,214 @@ 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+
Slot_reduction_update.(
520+
t |> of_record |> hardfork_adjustment ~hardfork_slot |> to_record)

0 commit comments

Comments
 (0)