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

staking: more reward for more signatures #2647

Merged
merged 9 commits into from
Feb 6, 2020
4 changes: 4 additions & 0 deletions .changelog/2647.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
staking: More reward for more signatures.

We're adjusting fee distribution and the block proposing staking reward to incentivize proposers
to create blocks with more signatures.
122 changes: 77 additions & 45 deletions go/consensus/tendermint/apps/staking/fees.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,79 +9,111 @@ import (
stakingState "github.com/oasislabs/oasis-core/go/consensus/tendermint/apps/staking/state"
)

type disbursement struct {
id signature.PublicKey
weight int64
}

// disburseFees disburses fees.
//
// In case of errors the state may be inconsistent.
func (app *stakingApplication) disburseFees(ctx *abci.Context, stakeState *stakingState.MutableState, proposerEntity *signature.PublicKey, signingEntities []signature.PublicKey) error {
func (app *stakingApplication) disburseFees(ctx *abci.Context, stakeState *stakingState.MutableState, proposerEntity *signature.PublicKey, numEligibleValidators int, signingEntities []signature.PublicKey) error {
totalFees, err := stakeState.LastBlockFees()
if err != nil {
return fmt.Errorf("staking: failed to query last block fees: %w", err)
}

ctx.Logger().Debug("disbursing fees",
"total_amount", totalFees,
"numEligibleValidators", numEligibleValidators,
"numSigningEntities", len(signingEntities),
)
if totalFees.IsZero() {
// Nothing to disburse.
return nil
}

// (Replicated in staking `ConsensusParameters` struct. Keep both explanations in sync.)
// A block's fees are split into $n$ portions, one corresponding to each validator.
// For each validator $V$ that signs the block, $V$'s corresponding portion is disbursed between $V$ and the
// proposer $P$. The ratio of this split are controlled by `FeeSplitVote` and `FeeSplitPropose`.
// Portions corresponding to validators that don't sign the block go to the common pool.

consensusParameters, err := stakeState.ConsensusParameters()
if err != nil {
return fmt.Errorf("staking: failed to load consensus parameters: %w", err)
}

var rewardAccounts []disbursement
var totalWeight int64
for _, entityID := range signingEntities {
d := disbursement{
id: entityID,
// For now we just disburse equally.
weight: consensusParameters.FeeWeightVote,
}
if proposerEntity != nil && proposerEntity.Equal(entityID) {
d.weight += consensusParameters.FeeWeightPropose
}
rewardAccounts = append(rewardAccounts, d)
totalWeight += d.weight
// Calculate denom := (FeeSplitVote + FeeSplitPropose) * numEligibleValidators.
// This will be used to calculate each portion's vote fee and propose fee.
denom := consensusParameters.FeeSplitVote.Clone()
if err = denom.Add(&consensusParameters.FeeSplitPropose); err != nil {
return fmt.Errorf("add fee splits: %w", err)
}
if totalWeight == 0 {
return fmt.Errorf("staking: fee distribution weights add up to zero")
var nEVQ quantity.Quantity
if err = nEVQ.FromInt64(int64(numEligibleValidators)); err != nil {
return fmt.Errorf("import numEligibleValidators %d: %w", numEligibleValidators, err)
}
if err = denom.Mul(&nEVQ); err != nil {
return fmt.Errorf("multiply denom: %w", err)
}

// Calculate the amount of fees to disburse.
var totalWeightQ quantity.Quantity
_ = totalWeightQ.FromInt64(totalWeight)

feeShare := totalFees.Clone()
if err := feeShare.Quo(&totalWeightQ); err != nil {
return err
// Calculate each portion's vote fee and propose fee.
// Portion vote fee. perVIVote := (totalFees * FeeSplitVote) / denom.
perVIVote := totalFees.Clone()
if err = perVIVote.Mul(&consensusParameters.FeeSplitVote); err != nil {
return fmt.Errorf("multiply perVIVote: %w", err)
}
if err = perVIVote.Quo(denom); err != nil {
return fmt.Errorf("divide perVIVote: %w", err)
}
// Portion propose fee. perVIPropose := (totalFees * FeeSplitPropose) / denom.
perVIPropose := totalFees.Clone()
if err = perVIPropose.Mul(&consensusParameters.FeeSplitPropose); err != nil {
return fmt.Errorf("multiply perVIPropose: %w", err)
}
// The per-VoteInfo proposer share is first rounded (down), then multiplied by the number of shares.
// This keeps incentives from having nonuniform breakpoints at certain signature counts.
if err = perVIPropose.Quo(denom); err != nil {
return fmt.Errorf("divide perVIPropose: %w", err)
}
numSigningEntities := len(signingEntities)
var nSEQ quantity.Quantity
if err = nSEQ.FromInt64(int64(numSigningEntities)); err != nil {
return fmt.Errorf("import numSigningEntities %d: %w", numSigningEntities, err)
}
// Overall propose fee. proposeTotal := perVIPropose * numSigningEntities.
proposeTotal := perVIPropose.Clone()
if err = proposeTotal.Mul(&nSEQ); err != nil {
return fmt.Errorf("multiply proposeTotal: %w", err)
}
for _, d := range rewardAccounts {
var weightQ quantity.Quantity
_ = weightQ.FromInt64(d.weight)

// Calculate how much to disburse to this account.
disburseAmount := feeShare.Clone()
if err := disburseAmount.Mul(&weightQ); err != nil {
return fmt.Errorf("staking: failed to disburse fees: %w", err)
// Pay proposer.
if !proposeTotal.IsZero() {
if proposerEntity != nil {
// Perform the transfer.
acct := stakeState.Account(*proposerEntity)
if err = quantity.Move(&acct.General.Balance, totalFees, proposeTotal); err != nil {
ctx.Logger().Error("failed to disburse fees (propose)",
"err", err,
"to", *proposerEntity,
"amount", proposeTotal,
)
return fmt.Errorf("staking: failed to disburse fees (propose): %w", err)
}
stakeState.SetAccount(*proposerEntity, acct)
}
// Perform the transfer.
acct := stakeState.Account(d.id)
if err := quantity.Move(&acct.General.Balance, totalFees, disburseAmount); err != nil {
ctx.Logger().Error("failed to disburse fees",
"err", err,
"to", d.id,
"amount", disburseAmount,
)
return fmt.Errorf("staking: failed to disburse fees: %w", err)
}
// Pay voters.
if !perVIVote.IsZero() {
for _, voterEntity := range signingEntities {
// Perform the transfer.
acct := stakeState.Account(voterEntity)
if err = quantity.Move(&acct.General.Balance, totalFees, perVIVote); err != nil {
ctx.Logger().Error("failed to disburse fees (vote)",
"err", err,
"to", voterEntity,
"amount", perVIVote,
)
return fmt.Errorf("staking: failed to disburse fees (vote): %w", err)
}
stakeState.SetAccount(voterEntity, acct)
}
stakeState.SetAccount(d.id, acct)
}
// Any remainder goes to the common pool.
if !totalFees.IsZero() {
Expand Down
5 changes: 3 additions & 2 deletions go/consensus/tendermint/apps/staking/proposing_rewards.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func (app *stakingApplication) resolveEntityIDFromProposer(regState *registrySta
return proposingEntity
}

func (app *stakingApplication) rewardBlockProposing(ctx *abci.Context, stakeState *stakingState.MutableState, proposingEntity *signature.PublicKey) error {
func (app *stakingApplication) rewardBlockProposing(ctx *abci.Context, stakeState *stakingState.MutableState, proposingEntity *signature.PublicKey, numEligibleValidators int, numSigningEntities int) error {
if proposingEntity == nil {
return nil
}
Expand All @@ -45,7 +45,8 @@ func (app *stakingApplication) rewardBlockProposing(ctx *abci.Context, stakeStat
ctx.Logger().Info("rewardBlockProposing: this block does not belong to an epoch. no block proposing reward")
return nil
}
if err = stakeState.AddRewards(epoch, &params.RewardFactorBlockProposed, []signature.PublicKey{*proposingEntity}); err != nil {
// Reward the proposer based on the `(number of included votes) / (size of the validator set)` ratio.
if err = stakeState.AddRewardSingleAttenuated(epoch, &params.RewardFactorBlockProposed, numSigningEntities, numEligibleValidators, *proposingEntity); err != nil {
return fmt.Errorf("adding rewards: %w", err)
}
return nil
Expand Down
7 changes: 5 additions & 2 deletions go/consensus/tendermint/apps/staking/staking.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,18 @@ func (app *stakingApplication) BeginBlock(ctx *abci.Context, request types.Reque
proposingEntity := app.resolveEntityIDFromProposer(regState, request, ctx)

// Go through all signers of the previous block and resolve entities.
// numEligibleValidators is how many total validators are in the validator set, while
// signingEntities is from the validators which actually signed.
numEligibleValidators := len(request.GetLastCommitInfo().Votes)
signingEntities := app.resolveEntityIDsFromVotes(ctx, regState, request.GetLastCommitInfo())

// Disburse fees from previous block.
if err := app.disburseFees(ctx, stakeState, proposingEntity, signingEntities); err != nil {
if err := app.disburseFees(ctx, stakeState, proposingEntity, numEligibleValidators, signingEntities); err != nil {
return fmt.Errorf("staking: failed to disburse fees: %w", err)
}

// Add rewards for proposer.
if err := app.rewardBlockProposing(ctx, stakeState, proposingEntity); err != nil {
if err := app.rewardBlockProposing(ctx, stakeState, proposingEntity, numEligibleValidators, len(signingEntities)); err != nil {
return fmt.Errorf("staking: block proposing reward: %w", err)
}

Expand Down
97 changes: 96 additions & 1 deletion go/consensus/tendermint/apps/staking/state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ func (s *MutableState) TransferFromCommon(ctx *abci.Context, toID signature.Publ
return ret, nil
}

// AddRewards computes and transfers the staking rewards to active escrow accounts.
// AddRewards computes and transfers a staking reward to active escrow accounts.
// If an error occurs, the pool and affected accounts are left in an invalid state.
// This may fail due to the common pool running out of tokens. In this case, the
// returned error's cause will be `staking.ErrInsufficientBalance`, and it should
Expand Down Expand Up @@ -695,6 +695,101 @@ func (s *MutableState) AddRewards(time epochtime.EpochTime, factor *quantity.Qua
return nil
}

// AddRewardSingleAttenuated computes, scales, and transfers a staking reward to an active escrow account.
func (s *MutableState) AddRewardSingleAttenuated(time epochtime.EpochTime, factor *quantity.Quantity, attenuationNumerator, attenuationDenominator int, account signature.PublicKey) error {
steps, err := s.RewardSchedule()
if err != nil {
return err
}
var activeStep *staking.RewardStep
for _, step := range steps {
if time < step.Until {
activeStep = &step
break
}
}
if activeStep == nil {
// We're past the end of the schedule.
return nil
}

var numQ, denQ quantity.Quantity
if err = numQ.FromInt64(int64(attenuationNumerator)); err != nil {
return errors.Wrapf(err, "importing attenuation numerator %d", attenuationNumerator)
}
if err = denQ.FromInt64(int64(attenuationDenominator)); err != nil {
return errors.Wrapf(err, "importing attenuation denominator %d", attenuationDenominator)
}

commonPool, err := s.CommonPool()
if err != nil {
return errors.Wrap(err, "loading common pool")
}

ent := s.Account(account)

q := ent.Escrow.Active.Balance.Clone()
// Multiply first.
if err := q.Mul(factor); err != nil {
return errors.Wrap(err, "multiplying by reward factor")
}
if err := q.Mul(&activeStep.Scale); err != nil {
return errors.Wrap(err, "multiplying by reward step scale")
}
if err := q.Mul(&numQ); err != nil {
return errors.Wrap(err, "multiplying by attenuation numerator")
}
if err := q.Quo(staking.RewardAmountDenominator); err != nil {
return errors.Wrap(err, "dividing by reward amount denominator")
}
if err := q.Quo(&denQ); err != nil {
return errors.Wrap(err, "dividing by attenuation denominator")
}

if q.IsZero() {
return nil
}

var com *quantity.Quantity
rate := ent.Escrow.CommissionSchedule.CurrentRate(time)
if rate != nil {
com = q.Clone()
// Multiply first.
if err := com.Mul(rate); err != nil {
return errors.Wrap(err, "multiplying by commission rate")
}
if err := com.Quo(staking.CommissionRateDenominator); err != nil {
return errors.Wrap(err, "dividing by commission rate denominator")
}

if err := q.Sub(com); err != nil {
return errors.Wrap(err, "subtracting commission")
}
}

if !q.IsZero() {
if err := quantity.Move(&ent.Escrow.Active.Balance, commonPool, q); err != nil {
return errors.Wrap(err, "transferring to active escrow balance from common pool")
}
}

if com != nil && !com.IsZero() {
delegation := s.Delegation(account, account)

if err := ent.Escrow.Active.Deposit(&delegation.Shares, commonPool, com); err != nil {
return errors.Wrap(err, "depositing commission")
}

s.SetDelegation(account, account, delegation)
}

s.SetAccount(account, ent)

s.SetCommonPool(commonPool)

return nil
}

// NewMutableState creates a new mutable staking state wrapper.
func NewMutableState(tree *iavl.MutableTree) *MutableState {
inner := &abci.ImmutableState{Snapshot: tree.ImmutableTree}
Expand Down
10 changes: 10 additions & 0 deletions go/consensus/tendermint/apps/staking/state/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,16 @@ func TestRewardAndSlash(t *testing.T) {
commonPool, err = s.CommonPool()
require.NoError(t, err, "load common pool")
require.Equal(t, mustInitQuantityP(t, 9840), commonPool, "slash - common pool")

// Epoch 10 is during the first step.
require.NoError(t, s.AddRewardSingleAttenuated(10, mustInitQuantityP(t, 10), 5, 10, escrowID), "add attenuated rewards epoch 30")

// 5% gain.
escrowAccount = s.Account(escrowID)
require.Equal(t, mustInitQuantity(t, 283), escrowAccount.Escrow.Active.Balance, "attenuated reward - escrow active escrow")
commonPool, err = s.CommonPool()
require.NoError(t, err, "load common pool")
require.Equal(t, mustInitQuantityP(t, 9827), commonPool, "reward attenuated - common pool")
}

func TestEpochSigning(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion go/genesis/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func TestGenesisChainContext(t *testing.T) {
// on each run.
stableDoc.Staking = staking.Genesis{}

require.Equal(t, "c396c9a2af84b3e0026790280bf15cf23ca82fdb14b989bc924ed97cce6a0ffd", stableDoc.ChainContext())
require.Equal(t, "c0c1cdc46bc6eba60b4971ab9f25d1188d0829be01b33f13d6a1929d551cc900", stableDoc.ChainContext())
}

func TestGenesisSanityCheck(t *testing.T) {
Expand Down
7 changes: 4 additions & 3 deletions go/oasis-node/cmd/genesis/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"math"
"math/big"
Expand Down Expand Up @@ -494,9 +495,9 @@ func AppendKeyManagerState(doc *genesis.Document, statuses []string, l *logging.
func AppendStakingState(doc *genesis.Document, state string, l *logging.Logger) error {
stakingSt := staking.Genesis{
Ledger: make(map[signature.PublicKey]*staking.Account),
Parameters: staking.ConsensusParameters{
FeeWeightVote: 1,
},
}
if err := stakingSt.Parameters.FeeSplitVote.FromInt64(1); err != nil {
return fmt.Errorf("couldn't set default fee split: %w", err)
}

if state != "" {
Expand Down
20 changes: 13 additions & 7 deletions go/staking/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,23 +407,29 @@ type ConsensusParameters struct {
CommissionScheduleRules CommissionScheduleRules `json:"commission_schedule_rules,omitempty"`
Slashing map[SlashReason]Slash `json:"slashing,omitempty"`
GasCosts transaction.Costs `json:"gas_costs,omitempty"`
MinDelegationAmount quantity.Quantity `json:"min_delegation,omitempty"`
MinDelegationAmount quantity.Quantity `json:"min_delegation"`

DisableTransfers bool `json:"disable_transfers,omitempty"`
DisableDelegation bool `json:"disable_delegation,omitempty"`
UndisableTransfersFrom map[signature.PublicKey]bool `json:"undisable_transfers_from,omitempty"`

// The proportion of fees disbursed to entities of the nodes that voted for a block.
FeeWeightVote int64 `json:"fee_weight_vote,omitempty"`
// The proportion of fees disbursed to the entity of the node that proposed a block.
FeeWeightPropose int64 `json:"fee_weight_propose,omitempty"`
// (Replicated in staking app `disburseFees` method. Keep both explanations in sync.)
// A block's fees are split into $n$ portions, one corresponding to each validator.
// For each validator $V$ that signs the block, $V$'s corresponding portion is disbursed between $V$ and the
// proposer $P$. The ratio of this split are controlled by `FeeSplitVote` and `FeeSplitPropose`.
// Portions corresponding to validators that don't sign the block go to the common pool.

// FeeSplitVote is the proportion of block fee portions that go to the validator that signs.
FeeSplitVote quantity.Quantity `json:"fee_split_vote"`
// FeeSplitPropose is the proportion of block fee portions that go to the proposer.
FeeSplitPropose quantity.Quantity `json:"fee_split_propose"`

// RewardFactorEpochSigned is the factor for a reward distributed per epoch to
// entities that have signed at least a threshold fraction of the blocks.
RewardFactorEpochSigned quantity.Quantity `json:"reward_factor_epoch_signed,omitempty"`
RewardFactorEpochSigned quantity.Quantity `json:"reward_factor_epoch_signed"`
// RewardFactorBlockProposed is the factor for a reward distributed per block
// to the entity that proposed the block.
RewardFactorBlockProposed quantity.Quantity `json:"reward_factor_block_proposed,omitempty"`
RewardFactorBlockProposed quantity.Quantity `json:"reward_factor_block_proposed"`
}

const (
Expand Down
Loading