/
staking.go
317 lines (279 loc) · 13.7 KB
/
staking.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
package keeper
import (
"fmt"
"strconv"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/lavanet/lava/utils"
epochstoragetypes "github.com/lavanet/lava/x/epochstorage/types"
"github.com/lavanet/lava/x/pairing/types"
planstypes "github.com/lavanet/lava/x/plans/types"
spectypes "github.com/lavanet/lava/x/spec/types"
)
const (
MAX_CHANGE_RATE = 1
CHANGE_WINDOW = time.Hour * 24
)
func (k Keeper) StakeNewEntry(ctx sdk.Context, validator, creator, chainID string, amount sdk.Coin, endpoints []epochstoragetypes.Endpoint, geolocation int32, moniker string, delegationLimit sdk.Coin, delegationCommission uint64) error {
logger := k.Logger(ctx)
specChainID := chainID
spec, err := k.specKeeper.GetExpandedSpec(ctx, specChainID)
if err != nil || !spec.Enabled {
return utils.LavaFormatWarning("spec not found or not active", err,
utils.Attribute{Key: "spec", Value: specChainID},
)
}
// if we get here, the spec is active and supported
if amount.IsLT(k.dualstakingKeeper.MinSelfDelegation(ctx)) { // we count on this to also check the denom
return utils.LavaFormatWarning("insufficient stake amount", fmt.Errorf("stake amount smaller than MinSelfDelegation"),
utils.Attribute{Key: "spec", Value: specChainID},
utils.Attribute{Key: "provider", Value: creator},
utils.Attribute{Key: "stake", Value: amount},
utils.Attribute{Key: "minSelfDelegation", Value: k.dualstakingKeeper.MinSelfDelegation(ctx).String()},
)
}
senderAddr, err := sdk.AccAddressFromBech32(creator)
if err != nil {
return utils.LavaFormatWarning("invalid address", err,
utils.Attribute{Key: "provider", Value: creator},
)
}
if !planstypes.IsValidProviderGeoEnum(geolocation) {
return utils.LavaFormatWarning(`geolocations are treated as a bitmap. To configure multiple geolocations,
use the uint representation of the valid geolocations`, fmt.Errorf("missing or invalid geolocation"),
utils.Attribute{Key: "geolocation", Value: geolocation},
utils.Attribute{Key: "valid_geolocations", Value: planstypes.PrintGeolocations()},
)
}
endpointsVerified, err := k.validateGeoLocationAndApiInterfaces(endpoints, geolocation, spec)
if err != nil {
return utils.LavaFormatWarning("invalid endpoints implementation for the given spec", err,
utils.Attribute{Key: "provider", Value: creator},
utils.Attribute{Key: "endpoints", Value: endpoints},
utils.Attribute{Key: "chain", Value: chainID},
utils.Attribute{Key: "geolocation", Value: geolocation},
)
}
// validate there are no more than 5 endpoints per geolocation
if len(endpoints) > len(planstypes.GetGeolocationsFromUint(geolocation))*types.MAX_ENDPOINTS_AMOUNT_PER_GEO {
return utils.LavaFormatWarning("stake provider failed", fmt.Errorf("number of endpoint for geolocation exceeded limit"),
utils.LogAttr("creator", creator),
utils.LogAttr("chain_id", chainID),
utils.LogAttr("moniker", moniker),
utils.LogAttr("geolocation", geolocation),
utils.LogAttr("max_endpoints_allowed", types.MAX_ENDPOINTS_AMOUNT_PER_GEO),
)
}
// new staking takes effect from the next block
stakeAppliedBlock := uint64(ctx.BlockHeight()) + 1
if len(moniker) > 50 {
moniker = moniker[:50]
}
existingEntry, entryExists := k.epochStorageKeeper.GetStakeEntryByAddressCurrent(ctx, chainID, creator)
if entryExists {
// modify the entry
if existingEntry.Address != creator {
return utils.LavaFormatWarning("returned stake entry by address doesn't match sender address", fmt.Errorf("sender and stake entry address mismatch"),
utils.Attribute{Key: "spec", Value: specChainID},
utils.Attribute{Key: "provider", Value: senderAddr.String()},
)
}
details := []utils.Attribute{
{Key: "spec", Value: specChainID},
{Key: "provider", Value: senderAddr.String()},
{Key: "stakeAppliedBlock", Value: stakeAppliedBlock},
{Key: "stake", Value: amount},
}
details = append(details, utils.Attribute{Key: "moniker", Value: moniker})
// if the provider has no delegations then we dont limit the changes
if !existingEntry.DelegateTotal.IsZero() {
// if there was a change in the last 24h than we dont allow changes
if ctx.BlockTime().UTC().Unix()-int64(existingEntry.LastChange) < int64(CHANGE_WINDOW.Seconds()) {
if delegationCommission != existingEntry.DelegateCommission || existingEntry.DelegateLimit != delegationLimit {
return utils.LavaFormatWarning(fmt.Sprintf("stake entry commmision or delegate limit can only be changes once in %s", CHANGE_WINDOW), nil,
utils.LogAttr("last_change_time", existingEntry.LastChange))
}
}
// check that the change is not mode than MAX_CHANGE_RATE
if int64(delegationCommission)-int64(existingEntry.DelegateCommission) > MAX_CHANGE_RATE {
return utils.LavaFormatWarning("stake entry commission increase too high", fmt.Errorf("commission change cannot increase by more than %d at a time", MAX_CHANGE_RATE),
utils.LogAttr("original_commission", existingEntry.DelegateCommission),
utils.LogAttr("wanted_commission", delegationCommission),
)
}
// check that the change in delegation limit is decreasing and that new_limit*100/old_limit < (100-MAX_CHANGE_RATE)
if delegationLimit.IsLT(existingEntry.DelegateLimit) && delegationLimit.Amount.MulRaw(100).Quo(existingEntry.DelegateLimit.Amount).LT(sdk.NewInt(100-MAX_CHANGE_RATE)) {
return utils.LavaFormatWarning("stake entry DelegateLimit decrease too high", fmt.Errorf("DelegateLimit change cannot decrease by more than %d at a time", MAX_CHANGE_RATE),
utils.LogAttr("change_percentage", delegationLimit.Amount.MulRaw(100).Quo(existingEntry.DelegateLimit.Amount)),
utils.LogAttr("original_limit", existingEntry.DelegateLimit),
utils.LogAttr("wanted_limit", delegationLimit),
)
}
}
// we dont change stakeAppliedBlocks and chain once they are set, if they need to change, unstake first
existingEntry.Geolocation = geolocation
existingEntry.Endpoints = endpointsVerified
existingEntry.Moniker = moniker
existingEntry.DelegateCommission = delegationCommission
existingEntry.DelegateLimit = delegationLimit
existingEntry.LastChange = uint64(ctx.BlockTime().UTC().Unix())
k.epochStorageKeeper.ModifyStakeEntryCurrent(ctx, chainID, existingEntry)
if amount.Amount.GT(existingEntry.Stake.Amount) {
// delegate the difference
diffAmount := amount.Sub(existingEntry.Stake)
err = k.dualstakingKeeper.DelegateFull(ctx, senderAddr.String(), validator, senderAddr.String(), chainID, diffAmount)
if err != nil {
details = append(details, utils.Attribute{Key: "neededStake", Value: amount.Sub(existingEntry.Stake).String()})
return utils.LavaFormatWarning("insufficient funds to pay for difference in stake", err,
details...,
)
}
} else if amount.Amount.LT(existingEntry.Stake.Amount) {
// unbond the difference
diffAmount := existingEntry.Stake.Sub(amount)
err = k.dualstakingKeeper.UnbondFull(ctx, senderAddr.String(), validator, senderAddr.String(), chainID, diffAmount, false)
if err != nil {
details = append(details, utils.Attribute{Key: "neededStake", Value: amount.Sub(existingEntry.Stake).String()})
return utils.LavaFormatWarning("insufficient funds to pay for difference in stake", err,
details...,
)
}
}
// TODO: create a new entry entirely because then we can keep the copies of this list as pointers only
// then we need to change the Copy of StoreCurrentEpochStakeStorage to copy of the pointers only
// must also change the unstaking to create a new entry entirely
detailsMap := map[string]string{}
for _, val := range details {
detailsMap[val.Key] = fmt.Sprint(val.Value)
}
utils.LogLavaEvent(ctx, logger, types.ProviderStakeUpdateEventName, detailsMap, "Changing Stake")
return nil
}
// entry isn't staked so add him
details := []utils.Attribute{
{Key: "spec", Value: specChainID},
{Key: "provider", Value: senderAddr.String()},
{Key: "stakeAppliedBlock", Value: stakeAppliedBlock},
{Key: "stake", Value: amount.String()},
{Key: "geolocation", Value: geolocation},
}
// if there are registered delegations to the provider, count them in the delegateTotal
delegateTotal := sdk.ZeroInt()
nextEpoch, err := k.epochStorageKeeper.GetNextEpoch(ctx, uint64(ctx.BlockHeight()))
if err != nil {
return utils.LavaFormatWarning("cannot get next epoch to count past delegations", err,
utils.LogAttr("provider", senderAddr.String()),
utils.LogAttr("block", nextEpoch),
)
}
delegations, err := k.dualstakingKeeper.GetProviderDelegators(ctx, senderAddr.String(), nextEpoch)
if err != nil {
utils.LavaFormatWarning("cannot get provider's delegators", err,
utils.LogAttr("provider", senderAddr.String()),
utils.LogAttr("block", nextEpoch),
)
}
for _, d := range delegations {
if d.Delegator == senderAddr.String() {
// ignore provider self delegation
continue
}
delegateTotal = delegateTotal.Add(d.Amount.Amount)
}
stakeEntry := epochstoragetypes.StakeEntry{
Stake: sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), sdk.ZeroInt()), // we set this to 0 since the delegate will take care of this
Address: creator,
StakeAppliedBlock: stakeAppliedBlock,
Endpoints: endpointsVerified,
Geolocation: geolocation,
Chain: chainID,
Moniker: moniker,
DelegateTotal: sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), delegateTotal),
DelegateLimit: delegationLimit,
DelegateCommission: delegationCommission,
LastChange: uint64(ctx.BlockTime().UTC().Unix()),
}
k.epochStorageKeeper.AppendStakeEntryCurrent(ctx, chainID, stakeEntry)
err = k.dualstakingKeeper.DelegateFull(ctx, senderAddr.String(), validator, senderAddr.String(), chainID, amount)
if err != nil {
return utils.LavaFormatWarning("provider self delegation failed", err,
details...,
)
}
details = append(details, utils.Attribute{Key: "moniker", Value: moniker})
detailsMap := map[string]string{}
for _, atr := range details {
detailsMap[atr.Key] = fmt.Sprint(atr.Value)
}
utils.LogLavaEvent(ctx, logger, types.ProviderStakeEventName, detailsMap, "Adding Staked provider")
return err
}
func (k Keeper) validateGeoLocationAndApiInterfaces(endpoints []epochstoragetypes.Endpoint, geolocation int32, spec spectypes.Spec) (endpointsFormatted []epochstoragetypes.Endpoint, err error) {
expectedInterfaces := k.specKeeper.GetExpectedServicesForExpandedSpec(spec, true)
allowedInterfaces := k.specKeeper.GetExpectedServicesForExpandedSpec(spec, false)
geolocMapRequired := map[epochstoragetypes.EndpointService]struct{}{}
geolocMapAllowed := map[epochstoragetypes.EndpointService]struct{}{}
geolocations := len(planstypes.GetAllGeolocations())
geolocKey := func(intefaceName string, geolocation int32, addon, extension string) epochstoragetypes.EndpointService {
return epochstoragetypes.EndpointService{
ApiInterface: intefaceName + "_" + strconv.FormatInt(int64(geolocation), 10),
Addon: addon,
Extension: extension,
}
}
for idx := uint64(0); idx < uint64(geolocations); idx++ {
// geolocation is a bit mask for areas, each bit turns support for an area
geolocZone := geolocation & (1 << idx)
if geolocZone != 0 {
for expectedEndpointService := range expectedInterfaces {
key := geolocKey(expectedEndpointService.ApiInterface, geolocZone, expectedEndpointService.Addon, "")
geolocMapRequired[key] = struct{}{}
}
for expectedEndpointService := range allowedInterfaces {
key := geolocKey(expectedEndpointService.ApiInterface, geolocZone, expectedEndpointService.Addon, expectedEndpointService.Extension)
geolocMapAllowed[key] = struct{}{}
}
}
}
addonsMap, extensionsMap := spec.ServicesMap()
// check all endpoints only implement allowed interfaces
for idx, endpoint := range endpoints {
err = endpoint.SetServicesFromAddons(allowedInterfaces, addonsMap, extensionsMap) // support apiInterfaces/extensions inside addons list
if err != nil {
return nil, fmt.Errorf("provider implements addons not allowed in the spec: %s, endpoint: %+v, allowed interfaces: %+v", spec.Index, endpoint, allowedInterfaces)
}
endpoint.SetDefaultApiInterfaces(expectedInterfaces) // support empty apiInterfaces list
endpoints[idx] = endpoint
for _, endpointService := range endpoint.GetSupportedServices() {
key := geolocKey(endpointService.ApiInterface, endpoint.Geolocation, endpointService.Addon, endpointService.Extension)
if _, ok := geolocMapAllowed[key]; !ok {
return nil, fmt.Errorf("provider implements api interfaces not allowed in the spec: %s, current allowed: %+v", key, geolocMapAllowed)
}
}
}
// check all expected api interfaces are implemented
for _, endpoint := range endpoints {
for _, endpointService := range endpoint.GetSupportedServices() {
key := geolocKey(endpointService.ApiInterface, endpoint.Geolocation, endpointService.Addon, "")
delete(geolocMapRequired, key) // remove this from expected implementations
}
}
if len(geolocMapRequired) != 0 {
return nil, fmt.Errorf("servicer does not implement all expected interfaces for all geolocations: %+v, missing implementation count: %d", geolocMapRequired, len(geolocMapRequired))
}
// all interfaces and geolocations were implemented
return endpoints, nil
}
func (k Keeper) GetStakeEntry(ctx sdk.Context, chainID string, provider string) (epochstoragetypes.StakeEntry, error) {
stakeEntry, found := k.epochStorageKeeper.GetStakeEntryByAddressCurrent(ctx, chainID, provider)
if !found {
return epochstoragetypes.StakeEntry{}, utils.LavaFormatWarning("provider not staked on chain", fmt.Errorf("cannot get stake entry"),
utils.Attribute{Key: "chainID", Value: chainID},
utils.Attribute{Key: "provider", Value: provider},
)
}
return stakeEntry, nil
}
func (k Keeper) GetAllChainIDs(ctx sdk.Context) []string {
return k.specKeeper.GetAllChainIDs(ctx)
}