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

refactor(zetacore): delete ballots after maturity #2863

Draft
wants to merge 16 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
* [2725](https://github.com/zeta-chain/node/pull/2725) - refactor SetCctxAndNonceToCctxAndInboundHashToCctx to receive tsspubkey as an argument
* [2802](https://github.com/zeta-chain/node/pull/2802) - set default liquidity cap for new ZRC20s
* [2826](https://github.com/zeta-chain/node/pull/2826) - remove unused code from emissions module and add new parameter for fixed block reward amount
* [2863](https://github.com/zeta-chain/node/pull/2863) - delete ballots after they mature

### Tests

Expand Down
5 changes: 5 additions & 0 deletions proto/zetachain/zetacore/observer/ballot.proto
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,8 @@ message BallotListForHeight {
int64 height = 1;
repeated string ballots_index_list = 2;
}

message VoterList {
string voter_address = 1;
VoteType vote_type = 2;
}
8 changes: 8 additions & 0 deletions proto/zetachain/zetacore/observer/events.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package zetachain.zetacore.observer;
import "gogoproto/gogo.proto";
import "zetachain/zetacore/observer/crosschain_flags.proto";
import "zetachain/zetacore/observer/observer.proto";
import "zetachain/zetacore/observer/ballot.proto";

option go_package = "github.com/zeta-chain/node/x/observer/types";

Expand All @@ -15,6 +16,13 @@ message EventBallotCreated {
string ballot_type = 5;
}

message EventBallotDeleted {
string msg_type_url = 1;
string ballot_identifier = 2;
string ballot_type = 3;
repeated VoterList voters = 4 [ (gogoproto.nullable) = false ];
}

message EventKeygenBlockUpdated {
string msg_type_url = 1;
string keygen_block = 2;
Expand Down
6 changes: 1 addition & 5 deletions proto/zetachain/zetacore/observer/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -246,14 +246,10 @@ message QueryHasVotedRequest {
message QueryHasVotedResponse { bool has_voted = 1; }

message QueryBallotByIdentifierRequest { string ballot_identifier = 1; }
message VoterList {
string voter_address = 1;
VoteType vote_type = 2;
}

message QueryBallotByIdentifierResponse {
string ballot_identifier = 1;
repeated VoterList voters = 2;
repeated VoterList voters = 2 [ (gogoproto.nullable) = false ];
ObservationType observation_type = 3;
BallotStatus ballot_status = 4;
}
Expand Down
5 changes: 5 additions & 0 deletions testutil/keeper/mocks/emissions/observer.go

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

29 changes: 29 additions & 0 deletions typescript/zetachain/zetacore/observer/ballot_pb.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,32 @@ export declare class BallotListForHeight extends Message<BallotListForHeight> {
static equals(a: BallotListForHeight | PlainMessage<BallotListForHeight> | undefined, b: BallotListForHeight | PlainMessage<BallotListForHeight> | undefined): boolean;
}

/**
* @generated from message zetachain.zetacore.observer.VoterList
*/
export declare class VoterList extends Message<VoterList> {
/**
* @generated from field: string voter_address = 1;
*/
voterAddress: string;

/**
* @generated from field: zetachain.zetacore.observer.VoteType vote_type = 2;
*/
voteType: VoteType;

constructor(data?: PartialMessage<VoterList>);

static readonly runtime: typeof proto3;
static readonly typeName = "zetachain.zetacore.observer.VoterList";
static readonly fields: FieldList;

static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): VoterList;

static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): VoterList;

static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): VoterList;

static equals(a: VoterList | PlainMessage<VoterList> | undefined, b: VoterList | PlainMessage<VoterList> | undefined): boolean;
}

40 changes: 40 additions & 0 deletions typescript/zetachain/zetacore/observer/events_pb.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf";
import { Message, proto3 } from "@bufbuild/protobuf";
import type { VoterList } from "./ballot_pb.js";
import type { GasPriceIncreaseFlags } from "./crosschain_flags_pb.js";

/**
Expand Down Expand Up @@ -51,6 +52,45 @@ export declare class EventBallotCreated extends Message<EventBallotCreated> {
static equals(a: EventBallotCreated | PlainMessage<EventBallotCreated> | undefined, b: EventBallotCreated | PlainMessage<EventBallotCreated> | undefined): boolean;
}

/**
* @generated from message zetachain.zetacore.observer.EventBallotDeleted
*/
export declare class EventBallotDeleted extends Message<EventBallotDeleted> {
/**
* @generated from field: string msg_type_url = 1;
*/
msgTypeUrl: string;

/**
* @generated from field: string ballot_identifier = 2;
*/
ballotIdentifier: string;

/**
* @generated from field: string ballot_type = 3;
*/
ballotType: string;

/**
* @generated from field: repeated zetachain.zetacore.observer.VoterList voters = 4;
*/
voters: VoterList[];

constructor(data?: PartialMessage<EventBallotDeleted>);

static readonly runtime: typeof proto3;
static readonly typeName = "zetachain.zetacore.observer.EventBallotDeleted";
static readonly fields: FieldList;

static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): EventBallotDeleted;

static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): EventBallotDeleted;

static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): EventBallotDeleted;

static equals(a: EventBallotDeleted | PlainMessage<EventBallotDeleted> | undefined, b: EventBallotDeleted | PlainMessage<EventBallotDeleted> | undefined): boolean;
}

/**
* @generated from message zetachain.zetacore.observer.EventKeygenBlockUpdated
*/
Expand Down
31 changes: 1 addition & 30 deletions typescript/zetachain/zetacore/observer/query_pb.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { ChainNonces } from "./chain_nonces_pb.js";
import type { PageRequest, PageResponse } from "../../../cosmos/base/query/v1beta1/pagination_pb.js";
import type { PendingNonces } from "./pending_nonces_pb.js";
import type { TSS } from "./tss_pb.js";
import type { BallotStatus, VoteType } from "./ballot_pb.js";
import type { BallotStatus, VoterList } from "./ballot_pb.js";
import type { LastObserverCount, ObservationType } from "./observer_pb.js";
import type { Chain } from "../pkg/chains/chains_pb.js";
import type { ChainParams, ChainParamsList } from "./params_pb.js";
Expand Down Expand Up @@ -596,35 +596,6 @@ export declare class QueryBallotByIdentifierRequest extends Message<QueryBallotB
static equals(a: QueryBallotByIdentifierRequest | PlainMessage<QueryBallotByIdentifierRequest> | undefined, b: QueryBallotByIdentifierRequest | PlainMessage<QueryBallotByIdentifierRequest> | undefined): boolean;
}

/**
* @generated from message zetachain.zetacore.observer.VoterList
*/
export declare class VoterList extends Message<VoterList> {
/**
* @generated from field: string voter_address = 1;
*/
voterAddress: string;

/**
* @generated from field: zetachain.zetacore.observer.VoteType vote_type = 2;
*/
voteType: VoteType;

constructor(data?: PartialMessage<VoterList>);

static readonly runtime: typeof proto3;
static readonly typeName = "zetachain.zetacore.observer.VoterList";
static readonly fields: FieldList;

static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): VoterList;

static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): VoterList;

static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): VoterList;

static equals(a: VoterList | PlainMessage<VoterList> | undefined, b: VoterList | PlainMessage<VoterList> | undefined): boolean;
}

/**
* @generated from message zetachain.zetacore.observer.QueryBallotByIdentifierResponse
*/
Expand Down
8 changes: 6 additions & 2 deletions x/emissions/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/zeta-chain/node/cmd/zetacored/config"
"github.com/zeta-chain/node/x/emissions/keeper"
"github.com/zeta-chain/node/x/emissions/types"
observertypes "github.com/zeta-chain/node/x/observer/types"
)

func BeginBlocker(ctx sdk.Context, keeper keeper.Keeper) {
Expand Down Expand Up @@ -102,11 +103,13 @@ func DistributeObserverRewards(
if len(ballotIdentifiers) == 0 {
return nil
}
ballots := make([]observertypes.Ballot, 0, len(ballotIdentifiers))
kingpinXD marked this conversation as resolved.
Show resolved Hide resolved
for _, ballotIdentifier := range ballotIdentifiers {
ballot, found := keeper.GetObserverKeeper().GetBallot(ctx, ballotIdentifier)
if !found {
continue
}
ballots = append(ballots, ballot)
totalRewardsUnits += ballot.BuildRewardsDistribution(rewardsDistributer)
}
rewardPerUnit := sdkmath.ZeroInt()
Expand Down Expand Up @@ -161,8 +164,9 @@ func DistributeObserverRewards(
}
}
types.EmitObserverEmissions(ctx, finalDistributionList)
// TODO : Delete Ballots after distribution
// https://github.com/zeta-chain/node/issues/942

// Clear the matured ballots
keeper.GetObserverKeeper().ClearMaturedBallotsAndBallotList(ctx, ballots, params.BallotMaturityBlocks)
return nil
}

Expand Down
22 changes: 21 additions & 1 deletion x/emissions/abci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ func TestBeginBlocker(t *testing.T) {
})

t.Run("successfully distribute rewards", func(t *testing.T) {
numberOfTestBlocks := 100
//Arrange
numberOfTestBlocks := 10
k, ctx, sk, zk := keepertest.EmissionsKeeper(t)
observerSet := sample.ObserverSet(10)
zk.ObserverKeeper.SetObserverSet(ctx, observerSet)
Expand Down Expand Up @@ -238,6 +239,9 @@ func TestBeginBlocker(t *testing.T) {

params, found := k.GetParams(ctx)
require.True(t, found)
// Set the ballot maturity blocks to numberOfTestBlocks so that the ballot mature at the end of the for loop which produces blocks
params.BallotMaturityBlocks = int64(numberOfTestBlocks)
err = k.SetParams(ctx, params)

// Get the rewards distribution, this is a fixed amount based on total block rewards and distribution percentages
validatorRewardsForABlock, observerRewardsForABlock, tssSignerRewardsForABlock := emissionstypes.GetRewardsDistributions(
Expand All @@ -247,6 +251,11 @@ func TestBeginBlocker(t *testing.T) {
distributedRewards := observerRewardsForABlock.Add(validatorRewardsForABlock).Add(tssSignerRewardsForABlock)
require.True(t, blockRewards.TruncateInt().GT(distributedRewards))

require.Len(t, zk.ObserverKeeper.GetAllBallots(ctx), len(ballotList))
_, found = zk.ObserverKeeper.GetBallotListForHeight(ctx, 0)
require.True(t, found)

// Act
for i := 0; i < numberOfTestBlocks; i++ {
emissionPoolBeforeBlockDistribution := sk.BankKeeper.GetBalance(ctx, emissionPool, config.BaseDenom).Amount
// produce a block
Expand Down Expand Up @@ -277,12 +286,23 @@ func TestBeginBlocker(t *testing.T) {
ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1)
}

// Assert

// 1. Assert Observer rewards, these are distributed at the block in which the ballots mature.
// numberOfTestBlocks is the same maturity blocks for the ballots

// We can simplify the calculation as the rewards are distributed equally among all the observers
rewardPerUnit := observerRewardsForABlock.Quo(
sdk.NewInt(int64(len(ballotList) * len(observerSet.ObserverList))),
)
emissionAmount := rewardPerUnit.Mul(sdk.NewInt(int64(len(ballotList))))

// 2 . Assert ballots and ballot list are deleted on maturity
require.Len(t, zk.ObserverKeeper.GetAllBallots(ctx), 0)
_, found = zk.ObserverKeeper.GetBallotListForHeight(ctx, 0)
require.False(t, found)

//3. Assert amounts in undistributed pools
// Check if the rewards are distributed equally among all the observers
for _, observer := range observerSet.ObserverList {
observerEmission, found := k.GetWithdrawableEmission(ctx, observer)
Expand Down
1 change: 1 addition & 0 deletions x/emissions/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type AccountKeeper interface {
type ObserverKeeper interface {
GetBallot(ctx sdk.Context, index string) (val observertypes.Ballot, found bool)
GetMaturedBallots(ctx sdk.Context, maturityBlocks int64) (val observertypes.BallotListForHeight, found bool)
ClearMaturedBallotsAndBallotList(ctx sdk.Context, ballots []observertypes.Ballot, maturityBlocks int64)
}

// BankKeeper defines the expected interface needed to retrieve account balances.
Expand Down
36 changes: 32 additions & 4 deletions x/observer/keeper/ballot.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ func (k Keeper) SetBallotList(ctx sdk.Context, ballotlist *types.BallotListForHe
store.Set(types.BallotListKeyPrefix(ballotlist.Height), b)
}

func (k Keeper) DeleteBallot(ctx sdk.Context, index string) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.VoterKey))
store.Delete([]byte(index))
}

func (k Keeper) DeleteBallotListForHeight(ctx sdk.Context, height int64) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.BallotListKey))
store.Delete(types.BallotListKeyPrefix(height))
}

func (k Keeper) GetBallot(ctx sdk.Context, index string) (val types.Ballot, found bool) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.VoterKey))
b := store.Get(types.KeyPrefix(index))
Expand All @@ -30,18 +40,21 @@ func (k Keeper) GetBallot(ctx sdk.Context, index string) (val types.Ballot, foun
return val, true
}

func (k Keeper) GetBallotList(ctx sdk.Context, height int64) (val types.BallotListForHeight, found bool) {
func (k Keeper) GetBallotListForHeight(ctx sdk.Context, height int64) (val types.BallotListForHeight, found bool) {
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.BallotListKey))
b := store.Get(types.BallotListKeyPrefix(height))
if b == nil {
return val, false
return types.BallotListForHeight{
Height: height,
BallotsIndexList: nil,
}, false
}
k.cdc.MustUnmarshal(b, &val)
return val, true
}

func (k Keeper) GetMaturedBallots(ctx sdk.Context, maturityBlocks int64) (val types.BallotListForHeight, found bool) {
return k.GetBallotList(ctx, ctx.BlockHeight()-maturityBlocks)
return k.GetBallotListForHeight(ctx, GetMaturedBallotHeight(ctx, maturityBlocks))
}

func (k Keeper) GetAllBallots(ctx sdk.Context) (voters []*types.Ballot) {
Expand All @@ -58,10 +71,25 @@ func (k Keeper) GetAllBallots(ctx sdk.Context) (voters []*types.Ballot) {

// AddBallotToList adds a ballot to the list of ballots for a given height.
func (k Keeper) AddBallotToList(ctx sdk.Context, ballot types.Ballot) {
list, found := k.GetBallotList(ctx, ballot.BallotCreationHeight)
list, found := k.GetBallotListForHeight(ctx, ballot.BallotCreationHeight)
if !found {
list = types.BallotListForHeight{Height: ballot.BallotCreationHeight, BallotsIndexList: []string{}}
}
list.BallotsIndexList = append(list.BallotsIndexList, ballot.BallotIdentifier)
k.SetBallotList(ctx, &list)
}

lumtis marked this conversation as resolved.
Show resolved Hide resolved
// ClearMaturedBallotsAndBallotList deletes all matured ballots and the list of ballots for a given height.
// It also emits an event for each ballot deleted.
func (k Keeper) ClearMaturedBallotsAndBallotList(ctx sdk.Context, ballots []types.Ballot, maturityBlocks int64) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @swift1337 , we should add more encapsulation, the external module shouldn't need to provide a list of ballot

-> You provide an height
-> You can get all ballots from that height in the function
-> You delete all the ballots

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The primary reason for not doing that is that the ballots are already fetched once, and I would want to avoid reading the same data from the store twice.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need to reread the store for ballots? The ballot list from height give the ID list that can be directly used in the delete function?

This is small optimization in any case I would consider the encapsulation of the code more important

Copy link
Contributor Author

@kingpinXD kingpinXD Sep 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is true, but the delete ballot event uses other fields of the ballot.

We could alternatively fire the event from abci.go, but I feel that firing it from the for loop is better.
IMO, the current design looks cleaner.

Fetch ballotslist -> Fetch Ballot -> distribute rewards -> delete them.

The new flow would be
Fetch ballots -> distribute rewards -> Fetch ballotslist ->Fetch Ballots -> delete them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current interface doesn't respect encapsulation principle. We delete ballots from a list and a BallotListForHeight value, while the list of ballots can be derived from this height. Both values should be deleted atomically from the same inputs, the current interface allows an external module to create an inconsistent state for the ballot

for _, ballot := range ballots {
k.DeleteBallot(ctx, ballot.BallotIdentifier)
EmitEventBallotDeleted(ctx, ballot)
kingpinXD marked this conversation as resolved.
Show resolved Hide resolved
}
k.DeleteBallotListForHeight(ctx, GetMaturedBallotHeight(ctx, maturityBlocks))
return
}

func GetMaturedBallotHeight(ctx sdk.Context, maturityBlocks int64) int64 {
kingpinXD marked this conversation as resolved.
Show resolved Hide resolved
return ctx.BlockHeight() - maturityBlocks
}
Loading
Loading