Skip to content
8 changes: 6 additions & 2 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ var maccPerms = map[string][]string{
feemarkettypes.ModuleName: nil,
erc20types.ModuleName: {authtypes.Minter, authtypes.Burner},
uexecutortypes.ModuleName: {authtypes.Minter, authtypes.Burner},
uvalidatortypes.ModuleName: nil,
}

var (
Expand Down Expand Up @@ -750,6 +751,9 @@ func NewChainApp(
runtime.NewKVStoreService(keys[uvalidatortypes.StoreKey]),
logger,
authtypes.NewModuleAddress(govtypes.ModuleName).String(),
app.BankKeeper,
app.AccountKeeper,
app.DistrKeeper,
app.StakingKeeper,
app.SlashingKeeper,
&app.UtssKeeper,
Expand Down Expand Up @@ -1038,7 +1042,7 @@ func NewChainApp(
uexecutor.NewAppModule(appCodec, app.UexecutorKeeper, app.EVMKeeper, app.FeeMarketKeeper, app.BankKeeper, app.AccountKeeper, app.UregistryKeeper, app.UtxverifierKeeper, app.UvalidatorKeeper),
utxverifier.NewAppModule(appCodec, app.UtxverifierKeeper, app.UregistryKeeper),
uregistry.NewAppModule(appCodec, app.UregistryKeeper, app.EVMKeeper),
uvalidator.NewAppModule(appCodec, app.UvalidatorKeeper, app.StakingKeeper, app.SlashingKeeper, &app.UtssKeeper),
uvalidator.NewAppModule(appCodec, app.UvalidatorKeeper, app.BankKeeper, app.AccountKeeper, app.DistrKeeper, app.StakingKeeper, app.SlashingKeeper, &app.UtssKeeper),
utss.NewAppModule(appCodec, app.UtssKeeper, app.UvalidatorKeeper),
)

Expand Down Expand Up @@ -1070,6 +1074,7 @@ func NewChainApp(
feemarkettypes.ModuleName,
evmtypes.ModuleName, // NOTE: EVM BeginBlocker must come after FeeMarket BeginBlocker

uvalidatortypes.ModuleName,
distrtypes.ModuleName,
slashingtypes.ModuleName,
evidencetypes.ModuleName,
Expand All @@ -1088,7 +1093,6 @@ func NewChainApp(
uexecutortypes.ModuleName,
utxverifiertypes.ModuleName,
uregistrytypes.ModuleName,
uvalidatortypes.ModuleName,
utsstypes.ModuleName,
)

Expand Down
143 changes: 143 additions & 0 deletions x/uvalidator/abci.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package module

import (
"context"
"time"

"cosmossdk.io/math"
abci "github.com/cometbft/cometbft/abci/types"
"github.com/cosmos/cosmos-sdk/telemetry"

sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/pushchain/push-chain-node/x/uvalidator/keeper"
"github.com/pushchain/push-chain-node/x/uvalidator/types"
)

// NOTE:
// This logic works well when community tax = 0.
// When community tax > 0, UVs effectively get boosted from the full fee amount,
// and community tax is only applied to the remaining (after UV boost allocation), reducing the community pool share.

// BoostMultiplier is the factor applied to UV power when calculating effective total power.
// This inflates the denominator so non-UVs get diluted.
const BoostMultiplier = "1.148"

// ExtraBoostPortion is the fractional part of the boost (0.148)
// used when allocating only the incremental boost to UVs.
const ExtraBoostPortion = "0.148"

func BeginBlocker(ctx sdk.Context, uvalidatorKeeper keeper.Keeper) error {
defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyBeginBlocker)

// determine the total power signing the block
var previousTotalPower int64
for _, voteInfo := range ctx.VoteInfos() {
previousTotalPower += voteInfo.Validator.Power
}

// full amount will be allocated to community pool by distribution module itself
if previousTotalPower == 0 {
return nil
}

height := ctx.BlockHeight()
if height > 1 {
if err := AllocateTokens(ctx, previousTotalPower, ctx.VoteInfos(), uvalidatorKeeper); err != nil {
return err
}
}

return nil
}

// AllocateTokens performs reward and fee distribution to all validators based
// on the F1 fee distribution specification.
func AllocateTokens(ctx context.Context, totalPreviousPower int64, bondedVotes []abci.VoteInfo, k keeper.Keeper) error {
// fetch and clear the collected fees for distribution, since this is
// called in BeginBlock, collected fees will be from the previous block
// (and distributed to the previous proposer)
feeCollector := k.AuthKeeper.GetModuleAccount(ctx, authtypes.FeeCollectorName)
feesCollectedInt := k.BankKeeper.GetAllBalances(ctx, feeCollector.GetAddress())
if feesCollectedInt.IsZero() {
return nil
}
feesCollected := sdk.NewDecCoinsFromCoins(feesCollectedInt...)

// transfer collected fees to the uvalidator module account
err := k.BankKeeper.SendCoinsFromModuleToModule(ctx, authtypes.FeeCollectorName, types.ModuleName, feesCollectedInt)
if err != nil {
return err
}

// First: calculate effective total power (standard + boost for UVs)
effectiveTotalPower := math.LegacyZeroDec()
for _, vote := range bondedVotes {
validator, err := k.StakingKeeper.ValidatorByConsAddr(ctx, vote.Validator.Address)
if err != nil {
return err
}

isUV, err := k.IsActiveUniversalValidator(ctx, validator.GetOperator())
if err != nil {
return err
}

power := math.LegacyNewDec(vote.Validator.Power)
if isUV {
power = power.Mul(math.LegacyMustNewDecFromStr(BoostMultiplier))
}

effectiveTotalPower = effectiveTotalPower.Add(power)
}

// Allocate 0.148x to the UVs proportionally using effective power
allocated := sdk.NewDecCoins()
for _, vote := range bondedVotes {
validator, err := k.StakingKeeper.ValidatorByConsAddr(ctx, vote.Validator.Address)
if err != nil {
return err
}

isUV, err := k.IsActiveUniversalValidator(ctx, validator.GetOperator())
if err != nil {
return err
}

if !isUV {
continue
}

// Use only the extra portion for UV allocation
power := math.LegacyNewDec(vote.Validator.Power)
power = power.Mul(math.LegacyMustNewDecFromStr(ExtraBoostPortion))

powerFraction := power.QuoTruncate(effectiveTotalPower)
reward := feesCollected.MulDecTruncate(powerFraction)

err = k.DistributionKeeper.AllocateTokensToValidator(ctx, validator, reward)
if err != nil {
return err
}

allocated = allocated.Add(reward...)
}

// Calculate and return remaining (including any truncation change)
extraCoins, extraChange := allocated.TruncateDecimal()
remainingCoins, _ := feesCollectedInt.SafeSub(extraCoins...)

remainingDec := sdk.NewDecCoinsFromCoins(remainingCoins...)
remainingDec = remainingDec.Add(extraChange...)
remainingFinal, _ := remainingDec.TruncateDecimal()

if !remainingFinal.IsZero() {
err = k.BankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, authtypes.FeeCollectorName, remainingFinal)
if err != nil {
return err
}
}

// Distribution's BeginBlocker will now run and allocate rest amount of fees proportionally to the voting power
return nil
}
13 changes: 8 additions & 5 deletions x/uvalidator/depinject.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,12 @@ type ModuleInputs struct {
StoreService store.KVStoreService
AddressCodec address.Codec

StakingKeeper stakingkeeper.Keeper
SlashingKeeper slashingkeeper.Keeper
UtssKeeper types.UtssKeeper
BankKeeper types.BankKeeper
AuthKeeper types.AccountKeeper
DistributionKeeper types.DistributionKeeper
StakingKeeper stakingkeeper.Keeper
SlashingKeeper slashingkeeper.Keeper
UtssKeeper types.UtssKeeper
}

type ModuleOutputs struct {
Expand All @@ -58,8 +61,8 @@ type ModuleOutputs struct {
func ProvideModule(in ModuleInputs) ModuleOutputs {
govAddr := authtypes.NewModuleAddress(govtypes.ModuleName).String()

k := keeper.NewKeeper(in.Cdc, in.StoreService, log.NewLogger(os.Stderr), govAddr, in.StakingKeeper, in.SlashingKeeper, in.UtssKeeper)
m := NewAppModule(in.Cdc, k, in.StakingKeeper, in.SlashingKeeper, in.UtssKeeper)
k := keeper.NewKeeper(in.Cdc, in.StoreService, log.NewLogger(os.Stderr), govAddr, in.BankKeeper, in.AuthKeeper, in.DistributionKeeper, in.StakingKeeper, in.SlashingKeeper, in.UtssKeeper)
m := NewAppModule(in.Cdc, k, in.BankKeeper, in.AuthKeeper, in.DistributionKeeper, in.StakingKeeper, in.SlashingKeeper, in.UtssKeeper)

return ModuleOutputs{Module: m, Keeper: k, Out: depinject.Out{}}
}
23 changes: 16 additions & 7 deletions x/uvalidator/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ type Keeper struct {
ExpiredBallotIDs collections.KeySet[string] // set of ballot IDs that have expired (not yet pruned)
FinalizedBallotIDs collections.KeySet[string] // set of ballot IDs that are PASSED or REJECTED

stakingKeeper types.StakingKeeper
slashingKeeper types.SlashingKeeper
utssKeeper types.UtssKeeper
StakingKeeper types.StakingKeeper
SlashingKeeper types.SlashingKeeper
UtssKeeper types.UtssKeeper
BankKeeper types.BankKeeper
AuthKeeper types.AccountKeeper
DistributionKeeper types.DistributionKeeper

authority string
hooks types.UValidatorHooks
Expand All @@ -47,6 +50,9 @@ func NewKeeper(
storeService storetypes.KVStoreService,
logger log.Logger,
authority string,
bankKeeper types.BankKeeper,
authKeeper types.AccountKeeper,
distributionKeeper types.DistributionKeeper,
stakingKeeper types.StakingKeeper,
slashingKeeper types.SlashingKeeper,
utssKeeper types.UtssKeeper,
Expand Down Expand Up @@ -92,10 +98,13 @@ func NewKeeper(
collections.StringKey,
),

authority: authority,
stakingKeeper: stakingKeeper,
slashingKeeper: slashingKeeper,
utssKeeper: utssKeeper,
authority: authority,
StakingKeeper: stakingKeeper,
SlashingKeeper: slashingKeeper,
UtssKeeper: utssKeeper,
BankKeeper: bankKeeper,
AuthKeeper: authKeeper,
DistributionKeeper: distributionKeeper,
}

return k
Expand Down
5 changes: 3 additions & 2 deletions x/uvalidator/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
distrkeeper "github.com/cosmos/cosmos-sdk/x/distribution/keeper"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
mintkeeper "github.com/cosmos/cosmos-sdk/x/mint/keeper"
minttypes "github.com/cosmos/cosmos-sdk/x/mint/types"
Expand Down Expand Up @@ -88,10 +89,10 @@ func SetupTest(t *testing.T) *testFixture {
registerBaseSDKModules(logger, f, encCfg, keys, accountAddressCodec, validatorAddressCodec, consensusAddressCodec)

// Setup Keeper.
f.k = keeper.NewKeeper(encCfg.Codec, runtime.NewKVStoreService(keys[types.ModuleName]), logger, f.govModAddr, f.stakingKeeper, slashingKeeper.Keeper{}, mockUtssKeeper{})
f.k = keeper.NewKeeper(encCfg.Codec, runtime.NewKVStoreService(keys[types.ModuleName]), logger, f.govModAddr, f.bankkeeper, f.accountkeeper, distrkeeper.Keeper{}, f.stakingKeeper, slashingKeeper.Keeper{}, mockUtssKeeper{})
f.msgServer = keeper.NewMsgServerImpl(f.k)
f.queryServer = keeper.NewQuerier(f.k)
f.appModule = module.NewAppModule(encCfg.Codec, f.k, f.stakingKeeper, slashingKeeper.Keeper{}, mockUtssKeeper{})
f.appModule = module.NewAppModule(encCfg.Codec, f.k, f.bankkeeper, f.accountkeeper, distrkeeper.Keeper{}, f.stakingKeeper, slashingKeeper.Keeper{}, mockUtssKeeper{})

return f
}
Expand Down
2 changes: 1 addition & 1 deletion x/uvalidator/keeper/msg_add_universal_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func (k Keeper) AddUniversalValidator(
}

// Ensure validator exists in staking module
validator, err := k.stakingKeeper.GetValidator(sdkCtx, valAddr)
validator, err := k.StakingKeeper.GetValidator(sdkCtx, valAddr)
if err != nil {
return fmt.Errorf("core validator not found: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion x/uvalidator/keeper/msg_remove_universal_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (k Keeper) RemoveUniversalValidator(

case types.UVStatus_UV_STATUS_PENDING_JOIN:
// Check if validator is part of the current TSS process
currentParticipants, err := k.utssKeeper.GetCurrentTssParticipants(ctx)
currentParticipants, err := k.UtssKeeper.GetCurrentTssParticipants(ctx)
if err != nil {
return fmt.Errorf("failed to fetch current TSS participants: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion x/uvalidator/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func (ms msgServer) UpdateUniversalValidator(ctx context.Context, msg *types.Msg
// Find validator controlled by this account
valAddr := sdk.ValAddress(signerAcc)

validator, err := ms.k.stakingKeeper.GetValidator(ctx, valAddr)
validator, err := ms.k.StakingKeeper.GetValidator(ctx, valAddr)
if err != nil {
return nil, errors.Wrap(err, "signer is not a validator operator")
}
Expand Down
2 changes: 1 addition & 1 deletion x/uvalidator/keeper/msg_update_universal_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (k Keeper) UpdateUniversalValidator(
}

// Ensure validator exists in staking module
_, err = k.stakingKeeper.GetValidator(sdkCtx, valAddr)
_, err = k.StakingKeeper.GetValidator(sdkCtx, valAddr)
if err != nil {
return fmt.Errorf("core validator not found: %w", err)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func (k Keeper) UpdateUniversalValidatorStatus(
) error {
sdkCtx := sdk.UnwrapSDKContext(ctx)

isOngoingTSS, err := k.utssKeeper.HasOngoingTss(ctx)
isOngoingTSS, err := k.UtssKeeper.HasOngoingTss(ctx)
if err != nil {
return fmt.Errorf("failed to check TSS state: %w", err)
}
Expand Down
27 changes: 27 additions & 0 deletions x/uvalidator/keeper/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,30 @@ func (k Keeper) GetUniversalValidator(

return val, true, nil
}

// IsActiveUniversalValidator returns true if the given validator address is
// currently registered as an active universal validator.
func (k Keeper) IsActiveUniversalValidator(
ctx context.Context,
validatorOperatorAddr string,
) (bool, error) {
valAddr, err := sdk.ValAddressFromBech32(validatorOperatorAddr)
if err != nil {
return false, err
}

exists, err := k.UniversalValidatorSet.Has(ctx, valAddr)
if err != nil {
return false, err
}
if !exists {
return false, nil
}

uv, err := k.UniversalValidatorSet.Get(ctx, valAddr)
if err != nil {
return false, fmt.Errorf("failed to get universal validator: %w", err)
}

return uv.LifecycleInfo.CurrentStatus == types.UVStatus_UV_STATUS_ACTIVE, nil
}
6 changes: 3 additions & 3 deletions x/uvalidator/keeper/voting.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (k Keeper) IsBondedUniversalValidator(ctx context.Context, universalValidat
}

// Ensure the universal validator exists in the staking module
validator, err := k.stakingKeeper.GetValidator(ctx, valAddr)
validator, err := k.StakingKeeper.GetValidator(ctx, valAddr)
if err != nil {
return false, fmt.Errorf("core validator not found: %w", err)
}
Expand Down Expand Up @@ -75,7 +75,7 @@ func (k Keeper) IsTombstonedUniversalValidator(ctx context.Context, universalVal
}

// Query the validator
validator, err := k.stakingKeeper.GetValidator(sdkCtx, valAddr)
validator, err := k.StakingKeeper.GetValidator(sdkCtx, valAddr)
if err != nil {
return false, fmt.Errorf("core validator not found: %w", err)
}
Expand All @@ -86,7 +86,7 @@ func (k Keeper) IsTombstonedUniversalValidator(ctx context.Context, universalVal
return false, fmt.Errorf("failed to get consensus address: %w", err)
}

return k.slashingKeeper.IsTombstoned(sdkCtx, consAddress), nil
return k.SlashingKeeper.IsTombstoned(sdkCtx, consAddress), nil
}

func (k Keeper) VoteOnBallot(
Expand Down
Loading
Loading