Skip to content

Commit

Permalink
feat(sequencer): hardforking when rotating to sentinel (#1455)
Browse files Browse the repository at this point in the history
Co-authored-by: danwt <[email protected]>
  • Loading branch information
mtsitrin and danwt authored Nov 12, 2024
1 parent 73636ee commit 93a59c8
Show file tree
Hide file tree
Showing 18 changed files with 186 additions and 127 deletions.
10 changes: 6 additions & 4 deletions app/keepers/keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,10 +347,6 @@ func (a *AppKeepers) InitKeepers(
a.ScopedIBCKeeper,
)

a.DenomMetadataKeeper = denommetadatamodulekeeper.NewKeeper(
a.BankKeeper,
)

a.RollappKeeper = rollappmodulekeeper.NewKeeper(
appCodec,
a.keys[rollappmoduletypes.StoreKey],
Expand Down Expand Up @@ -400,6 +396,11 @@ func (a *AppKeepers) InitKeepers(
a.RollappKeeper.SetSequencerKeeper(a.SequencerKeeper)
a.RollappKeeper.SetCanonicalClientKeeper(a.LightClientKeeper)

a.DenomMetadataKeeper = denommetadatamodulekeeper.NewKeeper(
a.BankKeeper,
a.RollappKeeper,
)

a.IncentivesKeeper = incentiveskeeper.NewKeeper(
a.keys[incentivestypes.StoreKey],
a.GetSubspace(incentivestypes.ModuleName),
Expand Down Expand Up @@ -630,6 +631,7 @@ func (a *AppKeepers) SetupHooks() {
a.DymNSKeeper.GetRollAppHooks(),
a.LightClientKeeper.RollappHooks(),
a.IROKeeper,
a.DenomMetadataKeeper.RollappHooks(),
))
}

Expand Down
1 change: 1 addition & 0 deletions x/denommetadata/ibc_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ func (im IBCModule) OnRecvPacket(
}

// OnAcknowledgementPacket adds the token metadata to the rollapp if it doesn't exist
// It marks the completion of the denom metadata registration process on the rollapp
func (im IBCModule) OnAcknowledgementPacket(
ctx sdk.Context,
packet channeltypes.Packet,
Expand Down
6 changes: 6 additions & 0 deletions x/denommetadata/ibc_middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,12 @@ type mockRollappKeeper struct {
err error
}

// ClearRegisteredDenoms implements types.RollappKeeper.
func (m *mockRollappKeeper) ClearRegisteredDenoms(ctx sdk.Context, rollappID string) error {
m.registeredDenoms = make(map[string]struct{})
return nil
}

func (m *mockRollappKeeper) SetRollapp(_ sdk.Context, rollapp rollapptypes.Rollapp) {
m.returnRollapp = &rollapp
}
Expand Down
4 changes: 3 additions & 1 deletion x/denommetadata/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import (
// Keeper of the denommetadata store
type Keeper struct {
bankKeeper types.BankKeeper
rk types.RollappKeeper
hooks types.MultiDenomMetadataHooks
}

// NewKeeper returns a new instance of the denommetadata keeper
func NewKeeper(bankKeeper types.BankKeeper) *Keeper {
func NewKeeper(bankKeeper types.BankKeeper, rk types.RollappKeeper) *Keeper {
return &Keeper{
bankKeeper: bankKeeper,
rk: rk,
hooks: nil,
}
}
Expand Down
22 changes: 22 additions & 0 deletions x/denommetadata/keeper/rollback.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package keeper

import (
sdk "github.com/cosmos/cosmos-sdk/types"
rollapptypes "github.com/dymensionxyz/dymension/v3/x/rollapp/types"
)

var _ rollapptypes.RollappHooks = rollappHook{}

type rollappHook struct {
rollapptypes.StubRollappCreatedHooks
k Keeper
}

func (k Keeper) RollappHooks() rollapptypes.RollappHooks {
return rollappHook{k: k}
}

// OnHardFork implements the RollappHooks interface
func (hook rollappHook) OnHardFork(ctx sdk.Context, rollappID string, _ uint64) error {
return hook.k.rk.ClearRegisteredDenoms(ctx, rollappID)
}
1 change: 1 addition & 0 deletions x/denommetadata/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ type RollappKeeper interface {
) (data rollapptypes.TransferData, err error)
SetRegisteredDenom(ctx sdk.Context, rollappID, denom string) error
HasRegisteredDenom(ctx sdk.Context, rollappID, denom string) (bool, error)
ClearRegisteredDenoms(ctx sdk.Context, rollappID string) error
}
5 changes: 5 additions & 0 deletions x/rollapp/keeper/registered_denoms.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@ func (k Keeper) IterateRegisteredDenoms(ctx sdk.Context, rollappID string, cb fu
return cb(item.K2())
})
}

func (k Keeper) ClearRegisteredDenoms(ctx sdk.Context, rollappID string) error {
rng := collections.NewPrefixedPairRange[string, string](rollappID)
return k.registeredRollappDenoms.Clear(ctx, rng)
}
22 changes: 13 additions & 9 deletions x/rollapp/keeper/sequencer_hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,31 @@ type SequencerHooks struct {
*Keeper
}

func (h SequencerHooks) AfterChooseNewProposer(ctx sdk.Context, rollapp string, before, after sequencertypes.Sequencer) {
// AfterRecoveryFromHalt is called after a new sequencer is set the proposer for an halted rollapp.
// We assume the rollapp had forked once halted
func (h SequencerHooks) AfterRecoveryFromHalt(ctx sdk.Context, rollapp string, _, newSeq sequencertypes.Sequencer) error {
// Start the liveness clock from zero
// NOTE: it could make more sense if liveness was a property of the sequencer rather than the rollapp
// TODO: tech debt https://github.com/dymensionxyz/dymension/issues/1357

ra := h.Keeper.MustGetRollapp(ctx, rollapp)
h.Keeper.ResetLivenessClock(ctx, &ra)
if !after.Sentinel() {
h.Keeper.ScheduleLivenessEvent(ctx, &ra)
}
h.Keeper.ScheduleLivenessEvent(ctx, &ra)
h.Keeper.SetRollapp(ctx, ra)

// recover from halt
// if the rollapp has a state info, set the next proposer to this sequencer
if before.Sentinel() && !after.Sentinel() {
sInfo, _ := h.Keeper.GetLatestStateInfo(ctx, rollapp)
sInfo.NextProposer = after.Address
h.Keeper.SetStateInfo(ctx, sInfo)
sInfo, ok := h.Keeper.GetLatestStateInfo(ctx, rollapp)
if !ok {
return nil
}
sInfo.NextProposer = newSeq.Address
h.Keeper.SetStateInfo(ctx, sInfo)

return nil
}

// AfterKickProposer is called after a sequencer is kicked from being a proposer.
// We hard fork the rollapp to the latest state so it'll be ready for the next proposer
func (h SequencerHooks) AfterKickProposer(ctx sdk.Context, kicked sequencertypes.Sequencer) error {
err := h.Keeper.HardForkToLatest(ctx, kicked.RollappId)
if err != nil {
Expand Down
16 changes: 3 additions & 13 deletions x/sequencer/keeper/bond.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/dymensionxyz/dymension/v3/x/sequencer/types"
"github.com/dymensionxyz/gerr-cosmos/gerrc"
"github.com/dymensionxyz/sdk-utils/utils/ucoin"
)

Expand Down Expand Up @@ -41,17 +40,14 @@ func (k Keeper) TryUnbond(ctx sdk.Context, seq *types.Sequencer, amt sdk.Coin) e
return errorsmod.Wrap(err, "refund")
}
if seq.Tokens.IsZero() {
return errorsmod.Wrap(k.unbond(ctx, seq), "unbond")
k.unbond(ctx, seq)
}
k.SetSequencer(ctx, *seq)
return nil
}

// set unbonded status and clear proposer/successor if necessary
func (k Keeper) unbond(ctx sdk.Context, seq *types.Sequencer) error {
if k.IsSuccessor(ctx, *seq) {
return gerrc.ErrInternal.Wrap(`unbond next proposer: it shouldnt be possible because
they cannot do frauds and they cannot unbond gracefully`)
}
func (k Keeper) unbond(ctx sdk.Context, seq *types.Sequencer) {
seq.Status = types.Unbonded

ctx.EventManager().EmitEvent(
Expand All @@ -60,10 +56,4 @@ they cannot do frauds and they cannot unbond gracefully`)
sdk.NewAttribute(types.AttributeKeySequencer, seq.Address),
),
)
if k.IsProposer(ctx, *seq) {
k.SetProposer(ctx, seq.RollappId, types.SentinelSeqAddr)
// we assume the current successor will not be happy if the proposer suddenly unbonds
k.SetSuccessor(ctx, seq.RollappId, types.SentinelSeqAddr)
}
return nil
}
42 changes: 23 additions & 19 deletions x/sequencer/keeper/fraud.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@ func (k Keeper) TryKickProposer(ctx sdk.Context, kicker types.Sequencer) error {
if !k.Kickable(ctx, proposer) {
return errorsmod.Wrap(gerrc.ErrFailedPrecondition, "not kickable")
}
// FIXME: slash remaining funds if any?
if err := k.unbond(ctx, &proposer); err != nil {
return errorsmod.Wrap(err, "unbond")

// clear the proposer
k.SetProposer(ctx, ra, types.SentinelSeqAddr)

err := k.TryUnbond(ctx, &proposer, proposer.TokensCoin())
if err != nil {
return errorsmod.Wrap(err, "try unbond")
}
k.SetSequencer(ctx, proposer)

// This will call hard fork on the rollapp, which will also optOut all sequencers
err := k.hooks.AfterKickProposer(ctx, proposer)
err = k.hooks.AfterKickProposer(ctx, proposer)
if err != nil {
return errorsmod.Wrap(err, "kick proposer callbacks")
}
Expand All @@ -37,6 +41,12 @@ func (k Keeper) TryKickProposer(ctx sdk.Context, kicker types.Sequencer) error {
}
k.SetSequencer(ctx, kicker)

// this will choose kicker as next proposer, since he is the only opted in and bonded
// sequencer remaining.
if err := k.RecoverFromHalt(ctx, ra); err != nil {
return errorsmod.Wrap(err, "choose proposer")
}

if err := uevent.EmitTypedEvent(ctx, &types.EventKickedProposer{
Rollapp: ra,
Kicker: kicker.Address,
Expand All @@ -45,12 +55,6 @@ func (k Keeper) TryKickProposer(ctx sdk.Context, kicker types.Sequencer) error {
return err
}

// this will choose kicker as next proposer, since he is the only opted in and bonded
// sequencer remaining.
if err := k.UpdateProposerIfNeeded(ctx, ra); err != nil {
return errorsmod.Wrap(err, "choose proposer")
}

return nil
}

Expand All @@ -74,24 +78,24 @@ func (k Keeper) SlashLiveness(ctx sdk.Context, rollappID string) error {

// Takes an optional rewardee addr who will receive some bounty
func (k Keeper) PunishSequencer(ctx sdk.Context, seqAddr string, rewardee *sdk.AccAddress) error {
var (
rewardMul = sdk.ZeroDec()
addr = []byte(nil)
)

seq, err := k.RealSequencer(ctx, seqAddr)
if err != nil {
return err
}

if rewardee != nil {
rewardMul := sdk.MustNewDecFromStr("0.5") // TODO: parameterise
err = k.slash(ctx, &seq, seq.TokensCoin(), rewardMul, *rewardee)
} else {
err = k.slash(ctx, &seq, seq.TokensCoin(), sdk.ZeroDec(), nil)
}
if err != nil {
return errorsmod.Wrap(err, "slash")
rewardMul = sdk.MustNewDecFromStr("0.5") // TODO: parameterise
addr = *rewardee
}

err = k.unbond(ctx, &seq)
err = k.slash(ctx, &seq, seq.TokensCoin(), rewardMul, addr)
if err != nil {
return errorsmod.Wrap(err, "unbond")
return errorsmod.Wrap(err, "slash")
}

k.SetSequencer(ctx, seq)
Expand Down
14 changes: 8 additions & 6 deletions x/sequencer/keeper/fraud_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,24 @@ func (s *SequencerTestSuite) TestSlashLivenessFlow() {
s.Require().True(ok)
}

// check the basic properties and that funds are allocated to the right place
func (s *SequencerTestSuite) TestFraud() {
// TestPunishSequencer tests the punish sequencer flow
// tokens are slashed and distributed to rewardee
func (s *SequencerTestSuite) TestPunishSequencer() {
ra := s.createRollapp()

s.Run("unbonded and not proposer anymore", func() {
s.Run("kickable after punish", func() {
s.createSequencerWithBond(s.Ctx, ra.RollappId, alice, bond)
seq := s.seq(alice)

s.k().SetProposer(s.Ctx, ra.RollappId, seq.Address)
s.Require().False(s.k().Kickable(s.Ctx, seq))

err := s.k().PunishSequencer(s.Ctx, seq.Address, nil)
s.Require().NoError(err)

seq = s.seq(alice)
s.Require().False(s.k().IsProposer(s.Ctx, seq))
s.Require().True(s.k().IsProposer(s.Ctx, s.k().SentinelSequencer(s.Ctx)))
s.Require().False(seq.Bonded())
// assert alice is now kickable
s.Require().True(s.k().Kickable(s.Ctx, seq))
})
s.Run("without rewardee", func() {
s.createSequencerWithBond(s.Ctx, ra.RollappId, bob, bond)
Expand Down
7 changes: 5 additions & 2 deletions x/sequencer/keeper/msg_server_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,11 @@ func (k msgServer) CreateSequencer(goCtx context.Context, msg *types.MsgCreateSe
return nil, err
}

if err := k.UpdateProposerIfNeeded(ctx, msg.RollappId); err != nil {
return nil, err
proposer := k.GetProposer(ctx, msg.RollappId)
if proposer.Sentinel() {
if err := k.RecoverFromHalt(ctx, msg.RollappId); err != nil {
return nil, err
}
}

ctx.EventManager().EmitEvent(
Expand Down
8 changes: 5 additions & 3 deletions x/sequencer/keeper/msg_server_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"fmt"

errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/dymensionxyz/sdk-utils/utils/uevent"

Expand Down Expand Up @@ -56,8 +55,11 @@ func (k msgServer) UpdateOptInStatus(goCtx context.Context,
k.SetSequencer(ctx, seq)

// maybe set as proposer if one is needed
if err := k.UpdateProposerIfNeeded(ctx, seq.RollappId); err != nil {
return nil, errorsmod.Wrap(err, "choose proposer")
proposer := k.GetProposer(ctx, seq.RollappId)
if proposer.Sentinel() {
if err := k.RecoverFromHalt(ctx, seq.RollappId); err != nil {
return nil, err
}
}
return &types.MsgUpdateOptInStatus{}, nil
}
Loading

0 comments on commit 93a59c8

Please sign in to comment.