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: improve performance BTC reward distribution #306

Open
wants to merge 110 commits into
base: main
Choose a base branch
from

Conversation

RafilxTenfen
Copy link
Contributor

@RafilxTenfen RafilxTenfen commented Nov 28, 2024

  • Add new structures to track BTC delegation rewards and lazily distribute
  • Add new event BTCDelegationStatus_EXPIRED for BTC delegations that indicates a BTC delegation is no longer contributing to security
  • Important files diff for review
    • proto/babylon/incentive/rewards.proto
    • x/finality/keeper/power_dist_change.go
    • x/incentive/keeper/btc_staking_gauge.go
    • x/incentive/keeper/grpc_query.go
    • x/incentive/keeper/msg_server.go
    • x/incentive/keeper/reward_tracker.go
    • x/incentive/keeper/reward_tracker_store.go
    • x/btcstaking/keeper/btc_delegations.go
    • x/btcstaking/keeper/msg_server.go
    • x/btcstaking/types/btc_delegation.go

TODOs

  • e2e test for rewards

Benchmarks

Command to generate pprof go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile\?seconds\=300

  • Main branch that iterates over delegations to distribute rewards with 163k BTC delegations, took 15.52s (13.1%) of the time during the 300seconds interval used to generate the pprof

image

  • Current branch rafilx/improve-performance-btc-dist with 100k BTC delegations, took 6.73s (1.8%) of the time during the 300seconds interval

image

Note that the percentage of the time used that is ~7x lower should be considered instead of the time, the benchmark from main was running in the benchmark server 48 processors and 256GB of RAM while the one in the current branch I run locally with 24 processors and 96GB of RAM.... So the percentage of the time usage instead of seconds should be the one in comparison

Thanks @Lazar955 for help with benchmark
The function used to show the difference is the TallyBlocks because the RewardBTCStaking does not appear in the pprof of the current branch demonstrating that the time spent there is negligible to the overall period

@RafilxTenfen RafilxTenfen self-assigned this Nov 29, 2024
…otal amount of satoshi staked as sdkmath.int
… (needed to create a new set of keys to store map of del => fp address)
@RafilxTenfen
Copy link
Contributor Author

It would be great to have some e2e test with pre-defined values i.e setup some scenario with lets ay 2 fps, each fp having 2 delegations with pre-defined values, known inflation, and split between fps and validators and calculate whether caluclated rewards for stakers makes sense.

I have added an integration tests with rewards, and there is also a fuzz test that verifies the proportion distributed FuzzRewardBTCStaking

but I will write an e2e test tomorrow with 2 active fps and 2 BTC delegations, it will be a bit difficult to estimate the rewards, but I think the goal would be to see the difference between the delegations amounts

app/app.go Outdated Show resolved Hide resolved
proto/babylon/incentive/rewards.proto Outdated Show resolved Hide resolved
testutil/coins/margin.go Show resolved Hide resolved
testutil/incentives-helper/keeper.go Show resolved Hide resolved
@@ -247,7 +261,11 @@ func (k Keeper) ProcessAllPowerDistUpdateEvents(

// if this finality provider is slashed, continue to avoid
// assigning delegation to it
if _, ok := slashedFPs[fpBTCPKHex]; ok {
Copy link
Member

Choose a reason for hiding this comment

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

No BtcDels field is no longer necessary. It is used for tracking the share of every BTC delegation under a given FP. Now this shall be replaced by storeBTCDelegationRewardsTracker store

Copy link
Member

@SebastianElvis SebastianElvis left a comment

Choose a reason for hiding this comment

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

That's a comprehensive PR 👍 Still parsing this. Some initial comments

Meanwhile, is it possible to consolidate some of the abstractions, e.g., in reward_tracker.go and reward_tracker_store.go? The current implementation is a bit hard to follow and some abstractions might not be necessary. See comments

Copy link
Member

Choose a reason for hiding this comment

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

Do we consider using the new collections package for handling all these DB operations (like here)? It seems that some functions defined below are captured in the collections

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did not consider to use collections package, thanks for pointing it out
I will update

proto/babylon/incentive/rewards.proto Show resolved Hide resolved
Comment on lines +81 to +88
// sendAllBtcRewardsToGauge iterates over all the finality providers associated
// with the delegator and withdraw the rewards available to the gauge.
// This creates new periods for each delegation and finality provider.
func (k Keeper) sendAllBtcRewardsToGauge(ctx context.Context, del sdk.AccAddress) error {
return k.iterBtcDelegationsByDelegator(ctx, del, func(del, fp sdk.AccAddress) error {
return k.btcDelegationModified(ctx, fp, del)
})
}
Copy link
Member

Choose a reason for hiding this comment

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

Tbh this function is a bit hard to follow. In order to distribute rewards to each BTC delegator, it uses btcDelegationModified which is actually a "hook" function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It iterates over all the delegations from one address and acts as if the btc delegation was modified (which was, since BTC rewards are being withdraw) it just doesn't modifies the amount staked

// CalculateBTCDelegationRewards calculates the rewards entitled for this delegation
// from the starting period cumulative reward and the ending period received as parameter
// It returns the amount of rewards without decimals (it removes the DecimalAccumulatedRewards).
func (k Keeper) CalculateBTCDelegationRewards(ctx context.Context, fp, del sdk.AccAddress, endPeriod uint64) (sdk.Coins, error) {
Copy link
Member

Choose a reason for hiding this comment

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

This function is only used in CalculateBTCDelegationRewardsAndSendToGauge. Could we merge it into it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could, but it is very helpful to have a separate function that only calculates the rewards instead of calculating and sending them to the gauge

Copy link
Contributor Author

Choose a reason for hiding this comment

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

mostly for testing

Comment on lines +54 to +59
// FpSlashed a slashed finality provider should withdraw all the rewards
// available to it, by iterating over all the delegations for this FP
// and sending to the gauge. After the rewards are removed, it should
// delete every rewards tracker value in the store related to this slashed
// finality provider.
func (k Keeper) FpSlashed(ctx context.Context, fp sdk.AccAddress) error {
Copy link
Member

Choose a reason for hiding this comment

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

Do we need to do anything upon a slashed FP?

  • After a FP is slashed BTC delegators will unbond and withdraw anyway, and this will trigger all those lazy executions. So enforce withdrawal does not seem necessary.
  • Enforcing withdrawal might cause some edge case, e.g., a FP is slashed which triggers all withdrawal. Then if a BTC delegator wants to withdraw will the code panick since some keys in KV store are removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is not strictly necessary, but the FP will not receive rewards anymore under that BTC pk, so we will basically stay with all of those structures inside the database, my idea here was to use everything at once and then prune it

Enforcing withdrawal might cause some edge case, e.g., a FP is slashed which triggers all withdrawal. Then if a BTC delegator wants to withdraw will the code panick since some keys in KV store are removed?

If the BTC delegator wants to withdraw after the FP is slashed, it will just not exists that FP,DEL record anymore and will not appear in the iterator, it should not panic

Comment on lines +30 to +32
if err := k.sendAllBtcDelegationTypeToRewardsGauge(ctx, sType, address); err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
Copy link
Member

Choose a reason for hiding this comment

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

RPC should not trigger state update at all. Otherwise it's possibel that someone queries this API and the queried node's state root is updated, then in the next block this node won't have a matching state root with any other node.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

in the query the database does not commit the modified changes to the state
The same is done by the cosmos-sdk DelegationRewards query

Comment on lines 284 to 286
// add all BTC delegations that are not unbonded to the new finality provider
for j := range dc.FinalityProviders[i].BtcDels {
btcDel := *dc.FinalityProviders[i].BtcDels[j]
Copy link
Member

Choose a reason for hiding this comment

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

Would recommend we remove any usage of BtcDels under FinalityProviderDistInfo. The original purpose of BtcDels is to track the share of every BTC delegator under the FP. Now this is replaced by KV stores under x/incentive and no need to do this anymore

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can this be done in another PR after this one

Copy link
Member

Choose a reason for hiding this comment

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

Could we do it in this PR or merge this PR to a feature branch? imo replacing BtcDels with the "virtual periods" is the main point of this feature so it's better to have it before merging this feature to main

Copy link
Collaborator

@KonradStaniec KonradStaniec Dec 18, 2024

Choose a reason for hiding this comment

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

hmm I also think we could remove those here as this is main value proposition of this pr. In my mind it should not be that hard i.e:

  • remove BtcDels []*BTCDelDistInfo from FinalityProviderDistInfo
  • Update FinalityProviderDistInfo.TotalBondedSat based on new activated and unbonded delegations. So with every new unbonding FinalityProviderDistInfo.TotalBondedSat = FinalityProviderDistInfo.TotalBondedSat - unbonding.value and with avery activated staking FinalityProviderDistInfo.TotalBondedSat = FinalityProviderDistInfo.TotalBondedSat + activated.value

With this change, it seems, our voting power event handling wil become optimal (we care only about the diff in voting power in each block)

@@ -326,6 +350,20 @@ func (k Keeper) ProcessAllPowerDistUpdateEvents(
}
}

k.processBtcDelegations(ctx, cacheFpByBtcPkHex, newActiveBtcDels, func(fp, del sdk.AccAddress, sats uint64) {
Copy link
Member

Choose a reason for hiding this comment

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

Can we do this inside the for _, event := range events loop? This loop was supposed to be the "event bus" that dispatches notifications to relevant modules, including x/incentive

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we could if the events were sorted and never received more than once

ref #306 (comment)

One BTC delegation can generate 2 unbonding events

Copy link
Member

Choose a reason for hiding this comment

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

I see. Probably a better fix is then distinguishing between two types of unbonded events, but this is certainly out of this PR's scope.

Also, why is newActiveBtcDels needed given that a BTC del cannot be active twice?

Copy link
Member

@gitferry gitferry left a comment

Choose a reason for hiding this comment

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

Great work! My first round of comments:

proto/babylon/incentive/rewards.proto Outdated Show resolved Hide resolved
x/btcstaking/types/btc_delegation.go Outdated Show resolved Hide resolved
})

k.processBtcDelegations(ctx, cacheFpByBtcPkHex, newUnbondedBtcDels, func(fp, del sdk.AccAddress, sats uint64) {
err := k.IncentiveKeeper.BtcDelegationUnbonded(ctx, fp, del, sats)
Copy link
Member

Choose a reason for hiding this comment

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

I think this pr works because the unbonded delegation will not appear in the cache so that unbonding events will processed twice by the incentive module

x/finality/keeper/power_dist_change.go Show resolved Hide resolved
proto/babylon/incentive/rewards.proto Show resolved Hide resolved
@@ -355,7 +355,7 @@ func (n *internalNode) initNodeConfigs(persistentPeers []string) error {
valConfig.P2P.ExternalAddress = fmt.Sprintf("%s:%d", n.moniker, 26656)
valConfig.RPC.ListenAddress = "tcp://0.0.0.0:26657"
valConfig.StateSync.Enable = false
valConfig.LogLevel = "debug"
valConfig.LogLevel = "info"
Copy link
Member

Choose a reason for hiding this comment

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

probably we can keep it at debug level for debugging purposes

Comment on lines 284 to 286
// add all BTC delegations that are not unbonded to the new finality provider
for j := range dc.FinalityProviders[i].BtcDels {
btcDel := *dc.FinalityProviders[i].BtcDels[j]
Copy link
Member

Choose a reason for hiding this comment

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

Could we do it in this PR or merge this PR to a feature branch? imo replacing BtcDels with the "virtual periods" is the main point of this feature so it's better to have it before merging this feature to main

@@ -326,6 +350,20 @@ func (k Keeper) ProcessAllPowerDistUpdateEvents(
}
}

k.processBtcDelegations(ctx, cacheFpByBtcPkHex, newActiveBtcDels, func(fp, del sdk.AccAddress, sats uint64) {
Copy link
Member

Choose a reason for hiding this comment

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

I see. Probably a better fix is then distinguishing between two types of unbonded events, but this is certainly out of this PR's scope.

Also, why is newActiveBtcDels needed given that a BTC del cannot be active twice?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants