-
Notifications
You must be signed in to change notification settings - Fork 22
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
base: main
Are you sure you want to change the base?
Conversation
…improve-performance-btc-dist
…otal amount of satoshi staked as sdkmath.int
… need the total to create new delegation
…improve-performance-btc-dist
…distribution testing
…improve-performance-btc-dist
…improve-performance-btc-dist
… (needed to create a new set of keys to store map of del => fp address)
…improve-performance-btc-dist
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 |
@@ -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 { |
There was a problem hiding this comment.
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
There was a problem hiding this 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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
// 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) | ||
}) | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mostly for testing
// 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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
if err := k.sendAllBtcDelegationTypeToRewardsGauge(ctx, sType, address); err != nil { | ||
return nil, status.Error(codes.Internal, err.Error()) | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
// 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] |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
fromFinalityProviderDistInfo
- Update
FinalityProviderDistInfo.TotalBondedSat
based on new activated and unbonded delegations. So with every new unbondingFinalityProviderDistInfo.TotalBondedSat = FinalityProviderDistInfo.TotalBondedSat - unbonding.value
and with avery activated stakingFinalityProviderDistInfo.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) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this 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:
}) | ||
|
||
k.processBtcDelegations(ctx, cacheFpByBtcPkHex, newUnbondedBtcDels, func(fp, del sdk.AccAddress, sats uint64) { | ||
err := k.IncentiveKeeper.BtcDelegationUnbonded(ctx, fp, del, sats) |
There was a problem hiding this comment.
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
@@ -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" |
There was a problem hiding this comment.
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
// 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] |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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?
…improve-performance-btc-dist
… if btcstaking is active
BTCDelegationStatus_EXPIRED
for BTC delegations that indicates a BTC delegation is no longer contributing to securityproto/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
Benchmarks
Command to generate pprof
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile\?seconds\=300
rafilx/improve-performance-btc-dist
with 100k BTC delegations, took 6.73s (1.8%) of the time during the 300seconds intervalNote 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