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: CNS-957-provider-jail #1443

Merged
merged 23 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions proto/lavanet/lava/epochstorage/stake_entry.proto
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ message StakeEntry {
uint64 last_change = 12;
BlockReport block_report = 13;
string vault = 14;
uint64 jails = 16;
int64 jail_time = 17;
oren-lava marked this conversation as resolved.
Show resolved Hide resolved
}

// BlockReport holds the most up-to-date info regarding blocks of the provider
Expand Down
2 changes: 1 addition & 1 deletion x/dualstaking/keeper/delegate.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ func (k Keeper) modifyStakeEntryDelegation(ctx sdk.Context, delegator, provider,
details["min_spec_stake"] = k.specKeeper.GetMinStake(ctx, chainID).String()
utils.LogLavaEvent(ctx, k.Logger(ctx), types.FreezeFromUnbond, details, "freezing provider due to stake below min spec stake")
stakeEntry.Freeze()
} else if delegator == stakeEntry.Vault && stakeEntry.IsFrozen() {
} else if delegator == stakeEntry.Vault && stakeEntry.IsFrozen() && !stakeEntry.IsJailed(ctx.BlockTime().UTC().Unix()) {
stakeEntry.UnFreeze(k.epochstorageKeeper.GetCurrentNextEpoch(ctx) + 1)
}

Expand Down
4 changes: 4 additions & 0 deletions x/epochstorage/types/stake_entry.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

142 changes: 109 additions & 33 deletions x/epochstorage/types/stake_entry.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions x/pairing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ type StakeEntry struct {
DelegateTotal types.Coin // total delegation to the provider (without self delegation)
DelegateLimit types.Coin // delegation total limit
DelegateCommission uint64 // commission from delegation rewards
Jails uint64 // number of times the provider have been jailed
Yaroms marked this conversation as resolved.
Show resolved Hide resolved
JailTime int64 // the time when the jail period is finished the he can come back to service
Yaroms marked this conversation as resolved.
Show resolved Hide resolved
}
```

Expand Down Expand Up @@ -95,6 +97,12 @@ A provider can unstake and retrieve their coins. When a provider unstakes, they

Freeze Mode enables the Provider to temporarily suspend their node's operation during maintenance to avoid bad Quality of Service (QoS). Freeze/Unfreeze is applied on the next Epoch. The Provider can initiate multiple Freeze actions with one command.

#### Jail
oren-lava marked this conversation as resolved.
Show resolved Hide resolved

If a provider is down and is being reported as such by users it will be jailed.
The first 3 times jail is temporary for 1H and will be removed automatically.
After 3 consecutive jails the provider will e jailed for 24H and set to a frozen state, to continue activity the provider must send unfreeze transaction after jail time is over.

### Pairing

The Pairing Engine is a core component of the Lava Network, responsible for connecting consumers with the most suitable service providers. It operates on a complex array of inputs, including the strictest policies defined at the plan, subscription, and project levels. These policies set the boundaries for service provisioning, ensuring that consumers' specific requirements are met while adhering to the network's overarching rules.
Expand Down
25 changes: 19 additions & 6 deletions x/pairing/keeper/msg_server_unfreeze.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ func (k msgServer) UnfreezeProvider(goCtx context.Context, msg *types.MsgUnfreez
return nil, utils.LavaFormatWarning("Unfreeze_cant_get_stake_entry", types.FreezeStakeEntryNotFoundError, []utils.Attribute{{Key: "chainID", Value: chainId}, {Key: "providerAddress", Value: msg.GetCreator()}}...)
}

// the provider is not frozen (active or jailed), continue to other chainIDs
if !stakeEntry.IsFrozen() {
continue
}

minStake := k.Keeper.specKeeper.GetMinStake(ctx, chainId)
if stakeEntry.EffectiveStake().LT(minStake.Amount) {
return nil, utils.LavaFormatWarning("Unfreeze_insufficient_stake", types.UnFreezeInsufficientStakeError,
Expand All @@ -31,13 +36,21 @@ func (k msgServer) UnfreezeProvider(goCtx context.Context, msg *types.MsgUnfreez
}...)
}

if stakeEntry.StakeAppliedBlock > unfreezeBlock {
// unfreeze the provider by making the StakeAppliedBlock the current block. This will let the provider be added to the pairing list in the next epoch, when current entries becomes the front of epochStorage
stakeEntry.UnFreeze(unfreezeBlock)
k.epochStorageKeeper.ModifyStakeEntryCurrent(ctx, chainId, stakeEntry)
unfrozen_chains = append(unfrozen_chains, chainId)
if stakeEntry.IsJailed(ctx.BlockTime().UTC().Unix()) {
return nil, utils.LavaFormatWarning("Unfreeze_jailed_provider", types.UnFreezeJailedStakeError,
[]utils.Attribute{
{Key: "chainID", Value: chainId},
{Key: "providerAddress", Value: msg.GetCreator()},
{Key: "jailEnd", Value: stakeEntry.JailTime},
}...)
}
// else case does not throw an error because we don't want to fail unfreezing other chains

// unfreeze the provider by making the StakeAppliedBlock the current block. This will let the provider be added to the pairing list in the next epoch, when current entries becomes the front of epochStorage
stakeEntry.UnFreeze(unfreezeBlock)
stakeEntry.JailTime = 0
stakeEntry.Jails = 0
k.epochStorageKeeper.ModifyStakeEntryCurrent(ctx, chainId, stakeEntry)
unfrozen_chains = append(unfrozen_chains, chainId)
}
utils.LogLavaEvent(ctx, ctx.Logger(), "unfreeze_provider", map[string]string{"providerAddress": msg.GetCreator(), "chainIDs": strings.Join(unfrozen_chains, ",")}, "Provider Unfreeze")
return &types.MsgUnfreezeProviderResponse{}, nil
Expand Down
31 changes: 25 additions & 6 deletions x/pairing/keeper/unresponsive_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"math"
"strconv"
"strings"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/lavanet/lava/utils"
Expand Down Expand Up @@ -227,14 +228,32 @@ func (k Keeper) getCurrentProviderStakeStorageList(ctx sdk.Context) []epochstora

// Function that punishes providers. Current punishment is freeze
func (k Keeper) punishUnresponsiveProvider(ctx sdk.Context, epochs []uint64, provider, chainID string, complaintCU uint64, servicedCU uint64, providerEpochCuMap map[uint64]types.ProviderEpochComplainerCu) error {
// freeze the unresponsive provider
err := k.FreezeProvider(ctx, provider, []string{chainID}, "unresponsiveness")
if err != nil {
utils.LavaFormatError("unable to freeze provider entry due to unresponsiveness", err,
utils.Attribute{Key: "provider", Value: provider},
utils.Attribute{Key: "chainID", Value: chainID},
if !utils.IsBech32Address(provider) {
return utils.LavaFormatWarning("Freeze_get_provider_address", fmt.Errorf("invalid address"),
utils.Attribute{Key: "providerAddress", Value: provider},
)
}

stakeEntry, found := k.epochStorageKeeper.GetStakeEntryByAddressCurrent(ctx, chainID, provider)
Yaroms marked this conversation as resolved.
Show resolved Hide resolved
if !found {
return utils.LavaFormatWarning("Freeze_cant_get_stake_entry", types.FreezeStakeEntryNotFoundError, []utils.Attribute{{Key: "chainID", Value: chainID}, {Key: "providerAddress", Value: provider}}...)
}

// if last jail was more than 24H ago, reset the jails counter
if !stakeEntry.IsJailed(ctx.BlockTime().UTC().Unix() + int64(24*time.Hour)) {
Yaroms marked this conversation as resolved.
Show resolved Hide resolved
stakeEntry.Jails = 0
}
stakeEntry.Jails++

if stakeEntry.Jails > 2 {
Yaroms marked this conversation as resolved.
Show resolved Hide resolved
stakeEntry.Freeze()
stakeEntry.JailTime = ctx.BlockTime().UTC().Unix() + int64(24*time.Hour)
} else {
stakeEntry.JailTime = ctx.BlockTime().UTC().Unix() + int64(time.Hour)
stakeEntry.StakeAppliedBlock = uint64(ctx.BlockHeight()) + uint64(time.Hour/k.downtimeKeeper.GetParams(ctx).EpochDuration)*k.epochStorageKeeper.EpochBlocksRaw(ctx)
}
k.epochStorageKeeper.ModifyStakeEntryCurrent(ctx, chainID, stakeEntry)

utils.LogLavaEvent(ctx, k.Logger(ctx), types.ProviderJailedEventName,
map[string]string{
"provider_address": provider,
Expand Down
19 changes: 7 additions & 12 deletions x/pairing/keeper/unresponsive_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,14 @@ import (
"github.com/lavanet/lava/utils/lavaslices"
oren-lava marked this conversation as resolved.
Show resolved Hide resolved
"github.com/lavanet/lava/utils/rand"
"github.com/lavanet/lava/utils/sigs"
epochstoragetypes "github.com/lavanet/lava/x/epochstorage/types"
"github.com/lavanet/lava/x/pairing/types"
"github.com/stretchr/testify/require"
)

func (ts *tester) checkProviderFreeze(provider string, shouldFreeze bool) {
func (ts *tester) checkProviderJailed(provider string, shouldFreeze bool) {
stakeEntry, stakeStorageFound := ts.Keepers.Epochstorage.GetStakeEntryByAddressCurrent(ts.Ctx, ts.spec.Name, provider)
require.True(ts.T, stakeStorageFound)
if shouldFreeze {
require.Equal(ts.T, uint64(epochstoragetypes.FROZEN_BLOCK), stakeEntry.StakeAppliedBlock)
} else {
require.NotEqual(ts.T, uint64(epochstoragetypes.FROZEN_BLOCK), stakeEntry.StakeAppliedBlock)
}
require.Equal(ts.T, shouldFreeze, stakeEntry.IsJailed(ts.Ctx.BlockTime().UTC().Unix()))
}

func (ts *tester) checkComplainerReset(provider string, epoch uint64) {
Expand Down Expand Up @@ -122,7 +117,7 @@ func TestUnresponsivenessStressTest(t *testing.T) {
ts.AdvanceEpochs(largerConst)

for i := 0; i < unresponsiveCount; i++ {
ts.checkProviderFreeze(providers[i].Addr.String(), true)
ts.checkProviderJailed(providers[i].Addr.String(), true)
ts.checkComplainerReset(providers[i].Addr.String(), relayEpoch)
}

Expand Down Expand Up @@ -187,7 +182,7 @@ func TestFreezingProviderForUnresponsiveness(t *testing.T) {

ts.AdvanceEpochs(largerConst)

ts.checkProviderFreeze(provider1, true)
ts.checkProviderJailed(provider1, true)
ts.checkComplainerReset(provider1, relayEpoch)
ts.checkProviderStaked(provider0)
}
Expand Down Expand Up @@ -244,7 +239,7 @@ func TestFreezingProviderForUnresponsivenessContinueComplainingAfterFreeze(t *te

ts.AdvanceEpochs(largerConst)

ts.checkProviderFreeze(provider1, true)
ts.checkProviderJailed(provider1, true)
ts.checkComplainerReset(provider1, relayEpoch)

ts.AdvanceEpochs(2)
Expand All @@ -266,7 +261,7 @@ func TestFreezingProviderForUnresponsivenessContinueComplainingAfterFreeze(t *te
}

// test the provider is still frozen
ts.checkProviderFreeze(provider1, true)
ts.checkProviderJailed(provider1, true)
}

func TestNotFreezingProviderForUnresponsivenessWithMinProviders(t *testing.T) {
Expand Down Expand Up @@ -341,6 +336,6 @@ func TestNotFreezingProviderForUnresponsivenessWithMinProviders(t *testing.T) {
ts.AdvanceEpochs(largerConst)

// test the unresponsive provider1 hasn't froze
ts.checkProviderFreeze(provider1Provider, play.shouldBeFrozen)
ts.checkProviderJailed(provider1Provider, play.shouldBeFrozen)
}
}
1 change: 1 addition & 0 deletions x/pairing/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ var (
UnFreezeInsufficientStakeError = sdkerrors.New("UnFreezeInsufficientStakeError Error", 697, "Could not unfreeze provider due to insufficient stake. Stake must be above minimum stake to unfreeze")
InvalidCreatorAddressError = sdkerrors.New("InvalidCreatorAddressError Error", 698, "The creator address is invalid")
AmountCoinError = sdkerrors.New("AmountCoinError Error", 699, "Amount limit coin is invalid")
UnFreezeJailedStakeError = sdkerrors.New("UnFreezeJailedStakeError Error", 700, "Could not unfreeze provider due to being jailed")
)
Loading
Loading