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

go staking: three-way fee split #2794

Merged
merged 12 commits into from
Mar 30, 2020
16 changes: 16 additions & 0 deletions .changelog/2794.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
go/staking: Three-way fee split

We should give more to the previous block proposer, which is the block
that first ran the transactions that paid the fees in the
`LastBlockFees`.
Currently they only get paid as a voter.

See
[oasis-core#2794](https://github.com/oasislabs/oasis-core/pull/2794)
for a description of the new fee split.

Instructions for genesis document maintainers:

1. Rename `fee_split_vote` to `fee_split_weight_vote` and
`fee_split_propose` to `fee_split_weight_next_propose` and
add `fee_split_weight_propose` in `.staking.params`.
195 changes: 118 additions & 77 deletions go/consensus/tendermint/apps/staking/fees.go
Original file line number Diff line number Diff line change
@@ -9,124 +9,165 @@ import (
stakingState "github.com/oasislabs/oasis-core/go/consensus/tendermint/apps/staking/state"
)

// disburseFees disburses fees.
// disburseFeesP disburses fees to the proposer and persists the voters' and next proposer's shares of the fees.
//
// In case of errors the state may be inconsistent.
func (app *stakingApplication) disburseFees(ctx *abci.Context, stakeState *stakingState.MutableState, proposerEntity *signature.PublicKey, numEligibleValidators int, signingEntities []signature.PublicKey) error {
totalFees, err := stakeState.LastBlockFees()
func (app *stakingApplication) disburseFeesP(ctx *abci.Context, stakeState *stakingState.MutableState, proposerEntity *signature.PublicKey, totalFees *quantity.Quantity) error {
ctx.Logger().Debug("disbursing proposer fees",
"total_amount", totalFees,
)
if totalFees.IsZero() {
stakeState.SetLastBlockFees(totalFees)
return nil
}

consensusParameters, err := stakeState.ConsensusParameters()
if err != nil {
return fmt.Errorf("ConsensusParameters: %w", err)
}

// Compute how much to persist for voters and the next proposer.
weightVQ := consensusParameters.FeeSplitWeightVote.Clone()
if err = weightVQ.Add(&consensusParameters.FeeSplitWeightNextPropose); err != nil {
return fmt.Errorf("add FeeSplitWeightNextPropose: %w", err)
}
weightPVQ := weightVQ.Clone()
if err = weightPVQ.Add(&consensusParameters.FeeSplitWeightPropose); err != nil {
return fmt.Errorf("add FeeSplitWeightPropose: %w", err)
}
feePersistAmt := totalFees.Clone()
if err = feePersistAmt.Mul(weightVQ); err != nil {
return fmt.Errorf("multiply feePersistAmt: %w", err)
}
if feePersistAmt.Quo(weightPVQ) != nil {
return fmt.Errorf("divide feePersistAmt: %w", err)
}

// Persist voters' and next proposer's shares of the fees.
feePersist := quantity.NewQuantity()
if err = quantity.Move(feePersist, totalFees, feePersistAmt); err != nil {
return fmt.Errorf("move feePersist: %w", err)
}
stakeState.SetLastBlockFees(feePersist)

// Pay the proposer.
feeProposerAmt := totalFees.Clone()
if proposerEntity != nil {
acct := stakeState.Account(*proposerEntity)
if err = quantity.Move(&acct.General.Balance, totalFees, feeProposerAmt); err != nil {
return fmt.Errorf("move feeProposerAmt: %w", err)
}
stakeState.SetAccount(*proposerEntity, acct)
}

// Put the rest into the common pool (in case there is no proposer entity to pay).
if !totalFees.IsZero() {
remaining := totalFees.Clone()
commonPool, err := stakeState.CommonPool()
if err != nil {
return fmt.Errorf("CommonPool: %w", err)
}
if err = quantity.Move(commonPool, totalFees, remaining); err != nil {
return fmt.Errorf("move remaining: %w", err)
}
stakeState.SetCommonPool(commonPool)
}

return nil
}

// disburseFeesVQ disburses persisted fees to the voters and next proposer.
//
// In case of errors the state may be inconsistent.
func (app *stakingApplication) disburseFeesVQ(ctx *abci.Context, stakeState *stakingState.MutableState, proposerEntity *signature.PublicKey, numEligibleValidators int, votingEntities []signature.PublicKey) error {
lastBlockFees, 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),
ctx.Logger().Debug("disbursing signer and next proposer fees",
"total_amount", lastBlockFees,
"num_eligible_validators", numEligibleValidators,
"num_voting_entities", len(votingEntities),
)
if totalFees.IsZero() {
if lastBlockFees.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)
return fmt.Errorf("ConsensusParameters: %w", err)
}

// 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)
}
// Compute the portion associated with each eligible validator's share of the fees, and within that, how much goes
// to the voter and how much goes to the next proposer.
perValidator := lastBlockFees.Clone()
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)
if err = perValidator.Quo(&nEVQ); err != nil {
return fmt.Errorf("divide perValidator: %w", 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)
denom := consensusParameters.FeeSplitWeightVote.Clone()
if err = denom.Add(&consensusParameters.FeeSplitWeightNextPropose); err != nil {
return fmt.Errorf("add FeeSplitWeightNextPropose: %w", err)
}
if err = perVIVote.Quo(denom); err != nil {
return fmt.Errorf("divide perVIVote: %w", err)
shareNextProposer := perValidator.Clone()
if err = shareNextProposer.Mul(&consensusParameters.FeeSplitWeightNextPropose); err != nil {
return fmt.Errorf("multiply shareNextProposer: %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)
if err = shareNextProposer.Quo(denom); err != nil {
return fmt.Errorf("divide shareNextProposer: %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)
shareVote := perValidator.Clone()
if err = shareVote.Sub(shareNextProposer); err != nil {
return fmt.Errorf("subtract shareVote: %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)

// Multiply to get the next proposer's total payment.
numVotingEntities := len(votingEntities)
var nVEQ quantity.Quantity
if err = nVEQ.FromInt64(int64(numVotingEntities)); err != nil {
return fmt.Errorf("import numVotingEntities %d: %w", numVotingEntities, err)
}
// Overall propose fee. proposeTotal := perVIPropose * numSigningEntities.
proposeTotal := perVIPropose.Clone()
if err = proposeTotal.Mul(&nSEQ); err != nil {
return fmt.Errorf("multiply proposeTotal: %w", err)
nextProposerTotal := shareNextProposer.Clone()
if err = nextProposerTotal.Mul(&nVEQ); err != nil {
return fmt.Errorf("multiply nextProposerTotal: %w", err)
}

// Pay proposer.
if !proposeTotal.IsZero() {
// Pay the next proposer.
if !nextProposerTotal.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)
if err = quantity.Move(&acct.General.Balance, lastBlockFees, nextProposerTotal); err != nil {
return fmt.Errorf("move nextProposerTotal: %w", err)
}
stakeState.SetAccount(*proposerEntity, acct)
}
}
// Pay voters.
if !perVIVote.IsZero() {
for _, voterEntity := range signingEntities {
// Perform the transfer.

// Pay the voters.
if !shareVote.IsZero() {
for _, voterEntity := range votingEntities {
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)
if err = quantity.Move(&acct.General.Balance, lastBlockFees, shareVote); err != nil {
return fmt.Errorf("move shareVote: %w", err)
}
stakeState.SetAccount(voterEntity, acct)
}
}
// Any remainder goes to the common pool.
if !totalFees.IsZero() {

// Put the rest into the common pool.
if !lastBlockFees.IsZero() {
remaining := lastBlockFees.Clone()
commonPool, err := stakeState.CommonPool()
if err != nil {
return fmt.Errorf("staking: failed to query common pool: %w", err)
return fmt.Errorf("CommonPool: %w", err)
}
if err := quantity.Move(commonPool, totalFees, totalFees); err != nil {
ctx.Logger().Error("failed to move remainder to common pool",
"err", err,
"amount", totalFees,
)
return fmt.Errorf("staking: failed to move to common pool: %w", err)
if err = quantity.Move(commonPool, lastBlockFees, remaining); err != nil {
return fmt.Errorf("move remaining: %w", err)
}
stakeState.SetCommonPool(commonPool)
}
20 changes: 11 additions & 9 deletions go/consensus/tendermint/apps/staking/staking.go
Original file line number Diff line number Diff line change
@@ -59,24 +59,24 @@ func (app *stakingApplication) BeginBlock(ctx *abci.Context, request types.Reque
// Look up the proposer's entity.
proposingEntity := app.resolveEntityIDFromProposer(regState, request, ctx)

// Go through all signers of the previous block and resolve entities.
// Go through all voters 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.
// votingEntities is from the validators which actually voted.
numEligibleValidators := len(request.GetLastCommitInfo().Votes)
signingEntities := app.resolveEntityIDsFromVotes(ctx, regState, request.GetLastCommitInfo())
votingEntities := app.resolveEntityIDsFromVotes(ctx, regState, request.GetLastCommitInfo())

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

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

// Track signing for rewards.
if err := app.updateEpochSigning(ctx, stakeState, signingEntities); err != nil {
if err := app.updateEpochSigning(ctx, stakeState, votingEntities); err != nil {
return fmt.Errorf("staking: failed to update epoch signing info: %w", err)
}

@@ -147,8 +147,10 @@ func (app *stakingApplication) ForeignExecuteTx(ctx *abci.Context, other abci.Ap
}

func (app *stakingApplication) EndBlock(ctx *abci.Context, request types.RequestEndBlock) (types.ResponseEndBlock, error) {
// Persist any block fees so we can transfer them in the next block.
stakingState.PersistBlockFees(ctx)
fees := stakingState.BlockFees(ctx)
if err := app.disburseFeesP(ctx, stakingState.NewMutableState(ctx.State()), stakingState.BlockProposer(ctx), &fees); err != nil {
return types.ResponseEndBlock{}, fmt.Errorf("disburse fees proposer: %w", err)
}

if changed, epoch := app.state.EpochChanged(ctx); changed {
return types.ResponseEndBlock{}, app.onEpochChange(ctx, epoch)
23 changes: 18 additions & 5 deletions go/consensus/tendermint/apps/staking/state/gas.go
Original file line number Diff line number Diff line change
@@ -100,11 +100,24 @@ func AuthenticateAndPayFees(
return nil
}

// PersistBlockFees persists the accumulated fee balance for the current block.
func PersistBlockFees(ctx *abci.Context) {
// BlockFees returns the accumulated fee balance for the current block.
func BlockFees(ctx *abci.Context) quantity.Quantity {
// Fetch accumulated fees in the current block.
fees := ctx.BlockContext().Get(feeAccumulatorKey{}).(*feeAccumulator).balance
return ctx.BlockContext().Get(feeAccumulatorKey{}).(*feeAccumulator).balance
}

state := NewMutableState(ctx.State())
state.SetLastBlockFees(&fees)
// proposerKey is the block context key.
type proposerKey struct{}

func (pk proposerKey) NewDefault() interface{} {
var empty *signature.PublicKey
return empty
}

func SetBlockProposer(ctx *abci.Context, p *signature.PublicKey) {
ctx.BlockContext().Set(proposerKey{}, p)
}

func BlockProposer(ctx *abci.Context) *signature.PublicKey {
return ctx.BlockContext().Get(proposerKey{}).(*signature.PublicKey)
}
2 changes: 1 addition & 1 deletion go/genesis/genesis_test.go
Original file line number Diff line number Diff line change
@@ -130,7 +130,7 @@ func TestGenesisChainContext(t *testing.T) {
// on each run.
stableDoc.Staking = staking.Genesis{}

require.Equal(t, "813021fca91663966fa73935b88165713d133ab2073f205050749e5b279fd869", stableDoc.ChainContext())
require.Equal(t, "77bc3d261fd95ff38712293ce9c8b93f20464ab8bc1f99c8f9cdf6aff4706730", stableDoc.ChainContext())
}

func TestGenesisSanityCheck(t *testing.T) {
2 changes: 1 addition & 1 deletion go/oasis-node/cmd/genesis/genesis.go
Original file line number Diff line number Diff line change
@@ -475,7 +475,7 @@ func AppendStakingState(doc *genesis.Document, state string, l *logging.Logger)
stakingSt := staking.Genesis{
Ledger: make(map[signature.PublicKey]*staking.Account),
}
if err := stakingSt.Parameters.FeeSplitVote.FromInt64(1); err != nil {
if err := stakingSt.Parameters.FeeSplitWeightVote.FromInt64(1); err != nil {
return fmt.Errorf("couldn't set default fee split: %w", err)
}

16 changes: 6 additions & 10 deletions go/staking/api/api.go
Original file line number Diff line number Diff line change
@@ -535,16 +535,12 @@ type ConsensusParameters struct {
DisableDelegation bool `json:"disable_delegation,omitempty"`
UndisableTransfersFrom map[signature.PublicKey]bool `json:"undisable_transfers_from,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"`
// FeeSplitWeightPropose is the proportion of block fee portions that go to the proposer.
FeeSplitWeightPropose quantity.Quantity `json:"fee_split_weight_propose"`
// FeeSplitWeightVote is the proportion of block fee portions that go to the validator that votes.
FeeSplitWeightVote quantity.Quantity `json:"fee_split_weight_vote"`
// FeeSplitWeightNextPropose is the proportion of block fee portions that go to the next block's proposer.
FeeSplitWeightNextPropose quantity.Quantity `json:"fee_split_weight_next_propose"`

// RewardFactorEpochSigned is the factor for a reward distributed per epoch to
// entities that have signed at least a threshold fraction of the blocks.
Loading