Skip to content

Commit

Permalink
allow 4 decimal places in slashing rateparamter (#491)
Browse files Browse the repository at this point in the history
closed: babylonlabs-io/pm#182

Changes:
- allow slashing rate to have 4 decimal places
- add parameter validation to check whether minimal slashing output is
not dust under BTC standard rules
  • Loading branch information
KonradStaniec committed Feb 7, 2025
1 parent 6659e30 commit 8b7ce11
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 13 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ from `Finalized` to `Forgotten`
- [#457](https://github.com/babylonlabs-io/babylon/pull/457) Remove staking msg server and update gentx to generate
`MsgWrappedCreateValidator`
- [#476](https://github.com/babylonlabs-io/babylon/pull/476) Bump cometbft to `v0.38.17`
- [#491](https://github.com/babylonlabs-io/babylon/pull/491) Allow slashing rate
to have 4 decimal places
- [#488](https://github.com/babylonlabs-io/babylon/pull/488) Fix duplicate BLS key registration in testnet command

## v1.0.0-rc4
Expand Down
2 changes: 1 addition & 1 deletion app/upgrades/v1/mainnet/btcstaking_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const BtcStakingParamsStr = `[
"c45753e856ad0abb06f68947604f11476c157d13b7efd54499eaa0f6918cf716"
],
"covenant_quorum": 3,
"min_staking_value_sat": 1000,
"min_staking_value_sat": 10000,
"max_staking_value_sat": 10000000000,
"min_staking_time_blocks": 10,
"max_staking_time_blocks": 65535,
Expand Down
4 changes: 2 additions & 2 deletions btcstaking/staking.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func buildSlashingTxFromOutpoint(
}

// Validate slashing rate
if !IsRateValid(slashingRate) {
if !IsSlashingRateValid(slashingRate) {
return nil, ErrInvalidSlashingRate
}

Expand Down Expand Up @@ -429,7 +429,7 @@ func CheckSlashingTxMatchFundingTx(
}

// Check if slashing rate is in the valid range (0,1)
if !IsRateValid(slashingRate) {
if !IsSlashingRateValid(slashingRate) {
return ErrInvalidSlashingRate
}

Expand Down
4 changes: 2 additions & 2 deletions btcstaking/staking_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func FuzzGeneratingValidStakingSlashingTx(f *testing.F) {
minFee := 2000
// generate a random slashing rate with random precision,
// this will include both valid and invalid ranges, so we can test both cases
randomPrecision := r.Int63n(4) // [0,3]
randomPrecision := r.Int63n(6) // [0,3]
slashingRate := sdkmath.LegacyNewDecWithPrec(int64(datagen.RandomInt(r, 1001)), randomPrecision) // [0,1000] / 10^{randomPrecision}

for i := 0; i < stakingTxNumOutputs; i++ {
Expand Down Expand Up @@ -165,7 +165,7 @@ func testSlashingTx(
&chaincfg.MainNetParams,
)

if btcstaking.IsRateValid(slashingRate) {
if btcstaking.IsSlashingRateValid(slashingRate) {
// If the slashing rate is valid i.e., in the range (0,1) with at most 2 decimal places,
// it is still possible that the slashing transaction is invalid. The following checks will confirm that
// slashing tx is not constructed if
Expand Down
8 changes: 4 additions & 4 deletions btcstaking/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,15 +529,15 @@ func (i *UnbondingInfo) SlashingPathSpendInfo() (*SpendInfo, error) {
return i.scriptHolder.scriptSpendInfoByName(i.slashingPathLeafHash)
}

// IsRateValid checks if the given rate is between the valid range i.e., (0,1) with a precision of at most 2 decimal places.
func IsRateValid(rate sdkmath.LegacyDec) bool {
// IsSlashingRateValid checks if the given rate is between the valid range i.e., (0,1) with a precision of at most 4 decimal places.
func IsSlashingRateValid(rate sdkmath.LegacyDec) bool {
// Check if the slashing rate is between 0 and 1
if rate.LTE(sdkmath.LegacyZeroDec()) || rate.GTE(sdkmath.LegacyOneDec()) {
return false
}

// Multiply by 100 to move the decimal places and check if precision is at most 2 decimal places
multipliedRate := rate.Mul(sdkmath.LegacyNewDec(100))
// Multiply by 10000 to move the decimal places and check if precision is at most 4 decimal places
multipliedRate := rate.Mul(sdkmath.LegacyNewDec(10000))

// Truncate the rate to remove decimal places
truncatedRate := multipliedRate.TruncateDec()
Expand Down
75 changes: 75 additions & 0 deletions btcstaking/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package btcstaking_test

import (
"testing"

sdkmath "cosmossdk.io/math"
"github.com/babylonlabs-io/babylon/btcstaking"
"github.com/stretchr/testify/require"
)

func TestIsSlashingRateValid(t *testing.T) {
testCases := []struct {
name string
rate sdkmath.LegacyDec
expected bool
}{
{
name: "valid rate - 0.5",
rate: sdkmath.LegacyNewDecWithPrec(5, 1), // 0.5
expected: true,
},
{
name: "valid rate - 0.25",
rate: sdkmath.LegacyNewDecWithPrec(25, 2), // 0.25
expected: true,
},
{
name: "valid rate - 0.255",
rate: sdkmath.LegacyNewDecWithPrec(255, 3), // 0.255
expected: true,
},
{
name: "valid rate - 0.2555",
rate: sdkmath.LegacyNewDecWithPrec(2555, 4), // 0.2555
expected: true,
},
{
name: "valid rate - 0.99",
rate: sdkmath.LegacyNewDecWithPrec(99, 2), // 0.99
expected: true,
},
{
name: "invalid rate - 0",
rate: sdkmath.LegacyZeroDec(),
expected: false,
},
{
name: "invalid rate - 1",
rate: sdkmath.LegacyOneDec(),
expected: false,
},
{
name: "invalid rate - negative",
rate: sdkmath.LegacyNewDecWithPrec(-5, 1), // -0.5
expected: false,
},
{
name: "invalid rate - too many decimal places",
rate: sdkmath.LegacyNewDecWithPrec(25555, 5), // 0.25555
expected: false,
},
{
name: "invalid rate - greater than 1",
rate: sdkmath.LegacyNewDecWithPrec(15, 1), // 1.5
expected: false,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := btcstaking.IsSlashingRateValid(tc.rate)
require.Equal(t, tc.expected, result)
})
}
}
2 changes: 1 addition & 1 deletion testutil/btcstaking-helper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func (h *Helper) GenAndApplyCustomParams(
err = h.BTCStakingKeeper.SetParams(h.Ctx, types.Params{
CovenantPks: bbn.NewBIP340PKsFromBTCPKs(covenantPKs),
CovenantQuorum: 3,
MinStakingValueSat: 1000,
MinStakingValueSat: 10000,
MaxStakingValueSat: int64(4 * 10e8),
MinStakingTimeBlocks: 400,
MaxStakingTimeBlocks: 10000,
Expand Down
2 changes: 1 addition & 1 deletion x/btcstaking/types/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestGenesisState_Validate(t *testing.T) {
&types.Params{
CovenantPks: types.DefaultParams().CovenantPks,
CovenantQuorum: types.DefaultParams().CovenantQuorum,
MinStakingValueSat: 1000,
MinStakingValueSat: 10000,
MaxStakingValueSat: 100000000,
MinStakingTimeBlocks: 100,
MaxStakingTimeBlocks: 1000,
Expand Down
34 changes: 32 additions & 2 deletions x/btcstaking/types/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/mempool"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/cometbft/cometbft/crypto/tmhash"
paramtypes "github.com/cosmos/cosmos-sdk/x/params/types"
"gopkg.in/yaml.v2"
Expand Down Expand Up @@ -64,7 +66,7 @@ func DefaultParams() Params {
return Params{
CovenantPks: bbn.NewBIP340PKsFromBTCPKs(pks),
CovenantQuorum: quorum,
MinStakingValueSat: 1000,
MinStakingValueSat: 10000,
MaxStakingValueSat: 10 * 10e8,
MinStakingTimeBlocks: 400, // this should be larger than minUnbonding
MaxStakingTimeBlocks: math.MaxUint16,
Expand Down Expand Up @@ -167,6 +169,30 @@ func validateStakingTime(minStakingTime, maxStakingTime uint32) error {
return nil
}

func validateNoDustSlashingOutput(p *Params) error {
// OP_RETURN scripts are allowed to be dust by BTC standard rules
if len(p.SlashingPkScript) > 0 && p.SlashingPkScript[0] == txscript.OP_RETURN {
return nil
}

slashingRateFloat64, err := p.SlashingRate.Float64()
if err != nil {
return fmt.Errorf("error converting slashing rate to float64: %w", err)
}

minUnbondingOutputValue := p.MinStakingValueSat - p.UnbondingFeeSat

minSlashingAmount := btcutil.Amount(minUnbondingOutputValue).MulF64(slashingRateFloat64)

minSlashingOutput := wire.NewTxOut(int64(minSlashingAmount), p.SlashingPkScript)

if mempool.IsDust(minSlashingOutput, mempool.DefaultMinRelayTxFee) {
return fmt.Errorf("invalid parameters configuration. Minimum slashing output is dust")
}

return nil
}

// Validate validates the set of params
func (p Params) Validate() error {
if p.CovenantQuorum == 0 {
Expand Down Expand Up @@ -195,10 +221,14 @@ func (p Params) Validate() error {
return err
}

if !btcstaking.IsRateValid(p.SlashingRate) {
if !btcstaking.IsSlashingRateValid(p.SlashingRate) {
return btcstaking.ErrInvalidSlashingRate
}

if err := validateNoDustSlashingOutput(&p); err != nil {
return err
}

if err := validateUnbondingTime(p.UnbondingTimeBlocks); err != nil {
return err
}
Expand Down
95 changes: 95 additions & 0 deletions x/btcstaking/types/params_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ package types_test
import (
"testing"

sdkmath "cosmossdk.io/math"
"github.com/babylonlabs-io/babylon/x/btcstaking/types"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -124,3 +129,93 @@ func TestHeightToVersionMap(t *testing.T) {
})
}
}

func TestParamsValidateNoDustSlashing(t *testing.T) {
testCases := []struct {
name string
modifyParams func(p *types.Params)
expectedError string
}{
{
name: "valid non-dust output",
modifyParams: func(p *types.Params) {
p.MinStakingValueSat = 100000 // 0.001 BTC
p.UnbondingFeeSat = 1000 // 0.00001 BTC
p.SlashingRate = sdkmath.LegacyNewDecWithPrec(1, 1) // 0.1 (10%)
},
expectedError: "",
},
{
name: "dust output due to small staking amount",
modifyParams: func(p *types.Params) {
p.MinStakingValueSat = 1000 // 0.00001 BTC (very small)
p.UnbondingFeeSat = 100 // 0.000001 BTC
p.SlashingRate = sdkmath.LegacyNewDecWithPrec(1, 1) // 0.1 (10%)
},
expectedError: "invalid parameters configuration. Minimum slashing output is dust",
},
{
name: "OP_RETURN output is allowed to be dust",
modifyParams: func(p *types.Params) {
p.MinStakingValueSat = 1000 // 0.00001 BTC (very small)
p.UnbondingFeeSat = 100 // 0.000001 BTC
p.SlashingRate = sdkmath.LegacyNewDecWithPrec(1, 1) // 0.1 (10%)
p.SlashingPkScript = []byte{txscript.OP_RETURN}
},
expectedError: "",
},
{
name: "dust output due to low slashing rate",
modifyParams: func(p *types.Params) {
p.MinStakingValueSat = 500000 // 0.001 BTC
p.UnbondingFeeSat = 40000 // 0.00001 BTC
p.SlashingRate = sdkmath.LegacyNewDecWithPrec(1, 3) // 0.001 (0.1%)
},
expectedError: "invalid parameters configuration. Minimum slashing output is dust",
},
{
// this test has similar parameters as the previous one but pk script is from
// p2pwkh addrss which has higher dust threshold
name: "pk script with witness has higher dust threshold",
modifyParams: func(p *types.Params) {
btcSk, err := btcec.NewPrivateKey()
require.NoError(t, err)
// Create P2WPKH address
p2wpkhAddr, err := btcutil.NewAddressWitnessPubKeyHash(
btcutil.Hash160(btcSk.PubKey().SerializeCompressed()),
&chaincfg.MainNetParams,
)
require.NoError(t, err)

// Get the pkScript for the P2WPKH address
p.SlashingPkScript, err = txscript.PayToAddrScript(p2wpkhAddr)
require.NoError(t, err)
p.MinStakingValueSat = 500000 // 0.001 BTC
p.UnbondingFeeSat = 40000 // 0.00001 BTC
p.SlashingRate = sdkmath.LegacyNewDecWithPrec(1, 3) // 0.001 (0.1%)
},
expectedError: "",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
params := types.DefaultParams()
tc.modifyParams(&params)

err := params.Validate()

if tc.expectedError == "" {
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), tc.expectedError)
}
})
}
}

func TestDefaultParamsAreValid(t *testing.T) {
params := types.DefaultParams()
require.NoError(t, params.Validate())
}

0 comments on commit 8b7ce11

Please sign in to comment.