Skip to content

Commit

Permalink
feat: accomodate ADR-25 to add jailed status to fp instance (#56)
Browse files Browse the repository at this point in the history
Partially closes #5 by introducing `jailed` status to the fp instance.
The jailing status can be found in two ways:
1. The status update loop periodically checks whether the fp is slashed
or jailed
2. Upon `ErrFpAlreadyJailed` error when submitting finality signature

Once jailing is detected, the status of the fp instance will be set to
`jailed` and terminated.
  • Loading branch information
gitferry authored Sep 19, 2024
1 parent 7a5db87 commit 8c63dae
Show file tree
Hide file tree
Showing 14 changed files with 126 additions and 92 deletions.
8 changes: 3 additions & 5 deletions clientcontroller/babylon.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,16 +257,14 @@ func (bc *BabylonController) SubmitBatchFinalitySigs(
return &types.TxResponse{TxHash: res.TxHash, Events: res.Events}, nil
}

func (bc *BabylonController) QueryFinalityProviderSlashed(fpPk *btcec.PublicKey) (bool, error) {
func (bc *BabylonController) QueryFinalityProviderSlashedOrJailed(fpPk *btcec.PublicKey) (slashed bool, jailed bool, err error) {
fpPubKey := bbntypes.NewBIP340PubKeyFromBTCPK(fpPk)
res, err := bc.bbnClient.QueryClient.FinalityProvider(fpPubKey.MarshalHex())
if err != nil {
return false, fmt.Errorf("failed to query the finality provider %s: %v", fpPubKey.MarshalHex(), err)
return false, false, fmt.Errorf("failed to query the finality provider %s: %v", fpPubKey.MarshalHex(), err)
}

slashed := res.FinalityProvider.SlashedBtcHeight > 0

return slashed, nil
return res.FinalityProvider.SlashedBtcHeight > 0, res.FinalityProvider.Jailed, nil
}

// QueryFinalityProviderVotingPower queries the voting power of the finality provider at a given height
Expand Down
4 changes: 2 additions & 2 deletions clientcontroller/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ type ClientController interface {
// QueryFinalityProviderVotingPower queries the voting power of the finality provider at a given height
QueryFinalityProviderVotingPower(fpPk *btcec.PublicKey, blockHeight uint64) (uint64, error)

// QueryFinalityProviderSlashed queries if the finality provider is slashed
QueryFinalityProviderSlashed(fpPk *btcec.PublicKey) (bool, error)
// QueryFinalityProviderSlashedOrJailed queries if the finality provider is slashed or jailed
QueryFinalityProviderSlashedOrJailed(fpPk *btcec.PublicKey) (slashed bool, jailed bool, err error)

// QueryLatestFinalizedBlocks returns the latest finalized blocks
QueryLatestFinalizedBlocks(count uint64) ([]*types.BlockInfo, error)
Expand Down
1 change: 1 addition & 0 deletions clientcontroller/retry_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var unrecoverableErrors = []*sdkErr.Error{
finalitytypes.ErrPubRandNotFound,
finalitytypes.ErrTooFewPubRand,
btcstakingtypes.ErrFpAlreadySlashed,
btcstakingtypes.ErrFpAlreadyJailed,
}

// IsUnrecoverable returns true when the error is in the unrecoverableErrors list
Expand Down
2 changes: 1 addition & 1 deletion eotsmanager/proto/eotsmanager.pb.go

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

104 changes: 55 additions & 49 deletions finality-provider/proto/finality_providers.pb.go

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

2 changes: 2 additions & 0 deletions finality-provider/proto/finality_providers.proto
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ enum FinalityProviderStatus {
INACTIVE = 3 [(gogoproto.enumvalue_customname) = "INACTIVE"];
// SLASHED defines a finality provider that has been slashed
SLASHED = 4 [(gogoproto.enumvalue_customname) = "SLASHED"];
// JAILED defines a finality provider that has been jailed
JAILED = 5 [(gogoproto.enumvalue_customname) = "JAILED"];
}

message SignMessageFromChainKeyRequest {
Expand Down
9 changes: 5 additions & 4 deletions finality-provider/service/fp_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -907,14 +907,15 @@ func (fp *FinalityProviderInstance) GetVotingPowerWithRetry(height uint64) (uint
return power, nil
}

func (fp *FinalityProviderInstance) GetFinalityProviderSlashedWithRetry() (bool, error) {
func (fp *FinalityProviderInstance) GetFinalityProviderSlashedOrJailedWithRetry() (bool, bool, error) {
var (
slashed bool
jailed bool
err error
)

if err := retry.Do(func() error {
slashed, err = fp.cc.QueryFinalityProviderSlashed(fp.GetBtcPk())
slashed, jailed, err = fp.cc.QueryFinalityProviderSlashedOrJailed(fp.GetBtcPk())
if err != nil {
return err
}
Expand All @@ -927,8 +928,8 @@ func (fp *FinalityProviderInstance) GetFinalityProviderSlashedWithRetry() (bool,
zap.Error(err),
)
})); err != nil {
return false, err
return false, false, err
}

return slashed, nil
return slashed, jailed, nil
}
31 changes: 27 additions & 4 deletions finality-provider/service/fp_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ func (fpm *FinalityProviderManager) monitorCriticalErr() {
zap.String("pk", criticalErr.fpBtcPk.MarshalHex()))
continue
}
if strings.Contains(criticalErr.err.Error(), btcstakingtypes.ErrFpAlreadyJailed.Error()) {
fpm.setFinalityProviderJailed(fpi)
fpm.logger.Debug("the finality-provider has been jailed",
zap.String("pk", criticalErr.fpBtcPk.MarshalHex()))
continue
}
fpm.logger.Fatal(instanceTerminatingMsg,
zap.String("pk", criticalErr.fpBtcPk.MarshalHex()), zap.Error(criticalErr.err))
case <-fpm.quit:
Expand Down Expand Up @@ -166,25 +172,35 @@ func (fpm *FinalityProviderManager) monitorStatusUpdate() {
}
continue
}
slashed, err := fpi.GetFinalityProviderSlashedWithRetry()
slashed, jailed, err := fpi.GetFinalityProviderSlashedOrJailedWithRetry()
if err != nil {
fpm.logger.Debug(
"failed to get the slashed height",
"failed to get the slashed or jailed status",
zap.String("fp_btc_pk", fpi.GetBtcPkHex()),
zap.Error(err),
)
continue
}
// power == 0 and slashed == true, set status to SLASHED and stop and remove the finality-provider instance
// power == 0 and slashed == true, set status to SLASHED, stop, and remove the finality-provider instance
if slashed {
fpm.setFinalityProviderSlashed(fpi)
fpm.logger.Debug(
fpm.logger.Warn(
"the finality-provider is slashed",
zap.String("fp_btc_pk", fpi.GetBtcPkHex()),
zap.String("old_status", oldStatus.String()),
)
continue
}
// power == 0 and jailed == true, set status to JAILED, stop, and remove the finality-provider instance
if jailed {
fpm.setFinalityProviderJailed(fpi)
fpm.logger.Warn(
"the finality-provider is jailed",
zap.String("fp_btc_pk", fpi.GetBtcPkHex()),
zap.String("old_status", oldStatus.String()),
)
continue
}
// power == 0 and slashed_height == 0, change to INACTIVE if the current status is ACTIVE
if oldStatus == proto.FinalityProviderStatus_ACTIVE {
fpi.MustSetStatus(proto.FinalityProviderStatus_INACTIVE)
Expand All @@ -208,6 +224,13 @@ func (fpm *FinalityProviderManager) setFinalityProviderSlashed(fpi *FinalityProv
}
}

func (fpm *FinalityProviderManager) setFinalityProviderJailed(fpi *FinalityProviderInstance) {
fpi.MustSetStatus(proto.FinalityProviderStatus_JAILED)
if err := fpm.removeFinalityProviderInstance(fpi.GetBtcPkBIP340()); err != nil {
panic(fmt.Errorf("failed to terminate a jailed finality-provider %s: %w", fpi.GetBtcPkHex(), err))
}
}

func (fpm *FinalityProviderManager) StartFinalityProvider(fpPk *bbntypes.BIP340PubKey, passphrase string) error {
if !fpm.isStarted.Load() {
fpm.isStarted.Store(true)
Expand Down
20 changes: 15 additions & 5 deletions finality-provider/service/fp_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (
)

var (
eventuallyWaitTimeOut = 1 * time.Second
eventuallyWaitTimeOut = 5 * time.Second
eventuallyPollTime = 10 * time.Millisecond
)

Expand Down Expand Up @@ -61,9 +61,17 @@ func FuzzStatusUpdate(f *testing.F) {
votingPower := uint64(r.Intn(2))
mockClientController.EXPECT().QueryFinalityProviderVotingPower(gomock.Any(), currentHeight).Return(votingPower, nil).AnyTimes()
mockClientController.EXPECT().SubmitFinalitySig(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(&types.TxResponse{TxHash: ""}, nil).AnyTimes()
var slashedHeight uint64
var isSlashedOrJailed int
if votingPower == 0 {
mockClientController.EXPECT().QueryFinalityProviderSlashed(gomock.Any()).Return(true, nil).AnyTimes()
// 0 means is slashed, 1 means is jailed, 2 means neither slashed nor jailed
isSlashedOrJailed = r.Intn(3)
if isSlashedOrJailed == 0 {
mockClientController.EXPECT().QueryFinalityProviderSlashedOrJailed(gomock.Any()).Return(true, false, nil).AnyTimes()
} else if isSlashedOrJailed == 1 {
mockClientController.EXPECT().QueryFinalityProviderSlashedOrJailed(gomock.Any()).Return(false, true, nil).AnyTimes()
} else {
mockClientController.EXPECT().QueryFinalityProviderSlashedOrJailed(gomock.Any()).Return(false, false, nil).AnyTimes()
}
}

err := vm.StartFinalityProvider(fpPk, passphrase)
Expand All @@ -76,9 +84,11 @@ func FuzzStatusUpdate(f *testing.F) {
if votingPower > 0 {
waitForStatus(t, fpIns, proto.FinalityProviderStatus_ACTIVE)
} else {
if slashedHeight == 0 && fpIns.GetStatus() == proto.FinalityProviderStatus_ACTIVE {
if isSlashedOrJailed == 2 && fpIns.GetStatus() == proto.FinalityProviderStatus_ACTIVE {
waitForStatus(t, fpIns, proto.FinalityProviderStatus_INACTIVE)
} else if slashedHeight > 0 {
} else if isSlashedOrJailed == 1 {
waitForStatus(t, fpIns, proto.FinalityProviderStatus_JAILED)
} else if isSlashedOrJailed == 0 {
waitForStatus(t, fpIns, proto.FinalityProviderStatus_SLASHED)
}
}
Expand Down
Loading

0 comments on commit 8c63dae

Please sign in to comment.