Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: refactor vesting funds into a head and a tail #1636

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

Stebalien
Copy link
Member

This lets us efficiently take fees from vesting funds without loading/storing this object each time. It also lets us check if there are funds to vest without having to load any additional state, because the "next" batch of vesting funds are always stored in the root state object.

If FIP-0100 passes, we'll likely want this change as we'll otherwise read/write the vesting funds queue on every deadline for every miner.

fixes #1594

1. In the test harness, check if we should vest every epoch. The queue
should already be quantized.
2. Correctly handle the fact that we vest at the _end_ of an epoch in
the cron vesting test.
@Stebalien
Copy link
Member Author

Built on #1635.

@@ -4464,25 +4460,6 @@ fn handle_proving_deadline(
let state: State = rt.transaction(|state: &mut State, rt| {
let policy = rt.policy();

// Vesting rewards for a miner are quantized to every 12 hours and we can determine what those "vesting epochs" are.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to the end, see the comment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the benefit of moving this below ?

@Stebalien Stebalien force-pushed the steb/vesting-head branch 2 times, most recently from d4e6f8f to 8c53bc7 Compare February 21, 2025 05:14
@Stebalien
Copy link
Member Author

This is significantly more than just "fixing #1594" so it probably requires a FIP update to FIP-0100. But, IMO, we should strongly consider this because without it, we'll be re-writing the vesting queue every deadline. I guess that isn't terrible in the grand scheme of things but.... it's not great.

This lets us efficiently take fees from vesting funds without
loading/storing this object each time. It also lets us check if there
are funds to vest without having to load any additional state, because
the "next" batch of vesting funds are always stored in the root state object.

If FIP-0100 passes, we'll likely want this change as we'll otherwise
read/write the vesting funds queue on every deadline for every miner.

fixes #1594
pub fn unlock_unvested_funds(
/// Unlock all vested (first return value) then unlock unvested funds up to at most the
/// specified target.
pub fn unlock_vested_and_unvested_funds(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please note that changing the interface at the same time as the implementation like this is not the most friendly for review. I get that you are in cleanup mode and this is how it is. Decoupled changes at commit level would probably halve my "wtf are we doing again" moments.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's not much I can do about that here, I'm changing the behavior from:

  1. Unlock unvested only, leaving unvested untouched.
  2. Unlock vested and unvested.

This didn't matter much before because the VestingFunds had all the state locally (so we could just unlock vested, then unlock unvested). But it matters now because I need to read the "tail" which may be behind a CID, so we get better performance by doing it in one go.

I could have done this in two commits, but then I would have had to have implemented it the other way first.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it matters now because I need to read the "tail" which may be behind a CID, so we get better performance by doing it in one go.

Yeah I'm questioning whether that performance is worth it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but then I would have had to have implemented it the other way first.

That's a good point

Ok(unlocked)
}

// Adds locked funds and unlocks everything that has already vested.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noting this second part looks like a behavior change. I can't see any problems with the new additional behavior as long as all the callers are doing the necessary book keeping with LockedFunds field

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok looks like this is just pushing down this coupled behavior to this level. Since only caller of this appears to be doing the same thing its not a real behavior change at all.

@@ -4464,25 +4460,6 @@ fn handle_proving_deadline(
let state: State = rt.transaction(|state: &mut State, rt| {
let policy = rt.policy();

// Vesting rewards for a miner are quantized to every 12 hours and we can determine what those "vesting epochs" are.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the benefit of moving this below ?


Ok((from_vesting, from_balance))
Ok((to_burn, total_unlocked))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Despite not liking touching this code the change does read as correct.

Comment on lines +4523 to +4527
// Vest locked funds. Locked funds will already have been vested automatically if we paid
// any fees/penalties, but we try to vest one more time just in case.
//
// If there's nothing to vest, this is an inexpensive operation as it'll just look at the
// "head" of the vesting queue, which is inlined into the root state object.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, but in the case where we haven't either had to pay fees or penalties we won't have done this, so going back to the 1/2 day quantisation might be a good idea here because now we're doing it on every deadline regardless

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We quantize when we put funds into the vesting queue itself, so we'll vest on that schedule regardless. We only had that buggy check as an optimization to avoid loading the vesting queue on every deadline.

Now that we don't need to load the queue, we can just check every deadline (and make it much harder to have vesting funds). Basically, the queue is now the source of truth.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, this makes sense, so confirm my reading of this: when giving block rewards we spread them out so that you get a chunk every 24 hours for a period of 180 days; but we quantize at 12 hours so we could have vesting at a maximum of once every 12 hours. As long as we're not nibbling too much into the next vesting epoch with our fees, we'll always retain that in the head until it actually vests at one of the 12 hour increments and then we get a new head and a rewritten VestingFundsInner block.

@Stebalien Stebalien force-pushed the steb/vesting-head branch 3 times, most recently from 1b29979 to 242e68e Compare February 24, 2025 21:29
This will save space when we have no vesting funds and, tbh, simplifies
the code a bit.

Also note:

- I changed the return value of `load` to return a vector. I can make it
work with iterators, but it's very... "rusty" (unreadable).
- I made `can_vest` private.
@Stebalien
Copy link
Member Author

I think I've addressed all the feedback so far.


let from_balance = cmp::min(&unlocked_balance, &self.fee_debt).clone();
self.fee_debt -= &from_balance;
let unlocked_balance = self.get_unlocked_balance(curr_balance)?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let unlocked_balance = self.get_unlocked_balance(curr_balance)?;
// locked unvested funds should now have been moved to unlocked balance if
// there was enough to cover the fee debt
let unlocked_balance = self.get_unlocked_balance(curr_balance)?;

Comment on lines +30 to +35
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(transparent)]
pub struct VestingFunds(Option<VestingFundsInner>);

#[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)]
struct VestingFundsInner {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so this means that in the miner actor state we have a potential null field, but we don't make it Optional there so we have a concrete VestingFunds on which to attach methods that can vary behaviour depending on whether it's null or not?

current_epoch: ChainEpoch,
vesting_sum: &TokenAmount,
proving_period_start: ChainEpoch,
spec: &VestSpec,
) {
) -> Result<TokenAmount, ActorError> {
// Quantization is aligned with when regular cron will be invoked, in the last epoch of deadlines.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you sanity check this comment @Stebalien, it doesn't seem true to me; we offset at proving_period_start

// The "next" batch of vesting funds.
head: VestingFund,
// The rest of the vesting funds, if any.
tail: Cid, // Vec<VestingFund>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still could be Optional<Cid> so we could avoid storing the empty array CID when you get down to having nothing beyond the head left.

@rvagg
Copy link
Member

rvagg commented Feb 25, 2025

Documenting this and how a migration would work in here: filecoin-project/FIPs#1130

I've said that a migration shouldn't make any attempt to perform vesting in the case that it happens to put a head that's already past its epoch, it'll be resolved properly the next time its touched (and therefore solve #1594 for everyone that's got an active cron).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: 📌 Triage
Status: ⌨️ In Progress
Development

Successfully merging this pull request may close these issues.

No vesting during cron
3 participants