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

Improving Leaderboard and Voting System. #289

Merged
merged 3 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
215 changes: 203 additions & 12 deletions Phantasma.Business/src/Blockchain/Contracts/Native/ConsensusContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public sealed class ConsensusContract : NativeContract
internal StorageList _pollList; // (Deprecated - Can't remove)
internal StorageMap _presences; // address, List<PollPresence> (Deprecated)
internal StorageMap _pollVotesPerAddress; // address, map<string, List<PollPresenceVotes>>
internal StorageMap _pollVotersRound; // string, map<int, List<Address>>
internal StorageMap _transactionMap; // string, Transaction
internal StorageMap _transactionMapRules; // string, List<Address>
internal StorageMap _transactionMapSigned; // string, Transaction
Expand Down Expand Up @@ -67,6 +68,8 @@ public void Migrate(Address from, Address target)
Runtime.Expect(Runtime.IsWitness(from), "invalid witness");

_presences.Migrate<Address, StorageList>(from, target);
// this cannot be here because we need to keep the old list
//_pollVotesPerAddress.Migrate<Address, StorageMap>(from, target);
}


Expand Down Expand Up @@ -322,6 +325,11 @@ public void InitPollV2(Address from, string subject, string organization, Consen
if (Runtime.ProtocolVersion >= 17)
{
var org = Runtime.GetOrganization(organization);
if (Runtime.ProtocolVersion >= 19)
{
Runtime.Expect(org != null, "invalid organization");
}

Runtime.Expect(org.IsMember(from), "must be member of organization: " + organization);
}

Expand Down Expand Up @@ -412,6 +420,13 @@ public void InitPollV2(Address from, string subject, string organization, Consen

_pollMap.Set(subject, poll);

if (Runtime.ProtocolVersion >= 19)
{
var votersRoundMap = _pollVotersRound.Get<string, StorageMap>(subject);
if (!votersRoundMap.ContainsKey<BigInteger>(poll.round))
votersRoundMap.Set<BigInteger, StorageList>(poll.round, new StorageList());
}

Runtime.Notify(EventKind.PollCreated, Address, subject);
}

Expand Down Expand Up @@ -635,6 +650,80 @@ private void AddNewVotes(Address from, ConsensusPoll poll, string subject, ref S
presences.Add(presenceVotes);
presencesMap.Set(subject, presences);
_pollVotesPerAddress.Set<Address, StorageMap>(from, presencesMap);

if (Runtime.ProtocolVersion >= 19)
{
var votersRoundMap = _pollVotersRound.Get<string, StorageMap>(subject);
if (votersRoundMap.ContainsKey<BigInteger>(poll.round))
{
var votersRound = votersRoundMap.Get<BigInteger, StorageList>(poll.round);
votersRound.Add(from);
votersRoundMap.Set<BigInteger, StorageList>(poll.round, votersRound);
}
}
}

/// <summary>
/// Update Votes Internally
/// </summary>
/// <param name="from"></param>
/// <param name="poll"></param>
/// <param name="subject"></param>
/// <param name="presences"></param>
/// <param name="presencesMap"></param>
/// <param name="choices"></param>
/// <param name="votingPower"></param>
private void UpdateVotesInternal(Address from, ConsensusPoll poll, string subject, ref StorageList presences,
ref StorageMap presencesMap, PollVote[] choices, BigInteger votingPower)
{
BigInteger lastPosition = presences.Count() - 1;
PollPresenceVotes presenceVotes = presences.Get<PollPresenceVotes>(lastPosition);

// Remove the user votes from the poll
for (int i = 0; i < presenceVotes.votes.Length; i++)
{
var targetIndex = (int)presenceVotes.votes[i].Choice.index;
poll.entries[targetIndex].votes -= presenceVotes.votes[i].NumberOfVotes;
}

// Add new ones
BigInteger choicePercentageAccumulation = 0;
Dictionary<BigInteger, bool> choiceIndexMap = new Dictionary<BigInteger, bool>();

for (int i = 0; i < choices.Length; i++)
{
Runtime.Expect(choices[i].percentage > 0 && choices[i].percentage < 101,
"choice percentage needs to be between 1 and 100");

Runtime.Expect(choices[i].index >= 0 && choices[i].index < poll.entries.Length,
"choice index is invalid");

Runtime.Expect(!choiceIndexMap.ContainsKey(choices[i].index), "Can't have the same choice index");
choiceIndexMap.Add(choices[i].index, true);

var votes = votingPower * choices[i].percentage / 100;
choicePercentageAccumulation += choices[i].percentage;
Runtime.Expect(votes > 0, "choice percentage is too low");
presenceVotes.votes[i] = new PollVotesValue()
{
Choice = choices[i],
NumberOfVotes = votes
};

var targetIndex = (int)choices[i].index;
poll.entries[targetIndex].votes += votes;
}

Runtime.Expect(choicePercentageAccumulation == 100,
$"choice percentage is too low or too high, it needs add up to 100, your value {choicePercentageAccumulation}");

// Update Poll Entries
_pollMap.Set(subject, poll);

// Update Address presences
presences.Replace(lastPosition, presenceVotes);
presencesMap.Set(subject, presences);
_pollVotesPerAddress.Set<Address, StorageMap>(from, presencesMap);
}

/// <summary>
Expand Down Expand Up @@ -703,27 +792,19 @@ private void UpdateVotes(Address from, ConsensusPoll poll, string subject, ref S
}

/// <summary>
/// Remove votes from the address in a poll
/// Remove votes from the address in a poll internally
/// </summary>
/// <param name="from"></param>
/// <param name="subject"></param>
public void RemoveVotes(Address from, string subject)
/// <param name="poll"></param>
private void RemoveVotesInternal(Address from, string subject, ConsensusPoll poll)
{
Runtime.Expect(Runtime.IsWitness(from), "invalid witness");
Runtime.Expect(_pollMap.ContainsKey(subject), "invalid poll subject");

var poll = FetchPoll(subject);
Runtime.Expect(poll.state == PollState.Active, "poll not active");

var organization = Runtime.GetOrganization(poll.organization);
Runtime.Expect(organization.IsMember(from), "must be member of organization: " + poll.organization);

Runtime.Expect(_pollVotesPerAddress.ContainsKey<Address>(from), "the address is not in the poll");
var presencesMap = _pollVotesPerAddress.Get<Address, StorageMap>(from);

Runtime.Expect(presencesMap.ContainsKey<string>(subject), "the address is not in the poll");
var presences = presencesMap.Get<string, StorageList>(subject);

BigInteger lastPosition = presences.Count() - 1;
PollPresenceVotes presenceVotes = presences.Get<PollPresenceVotes>(lastPosition);

Expand All @@ -743,6 +824,36 @@ public void RemoveVotes(Address from, string subject)
// Update Address presences
presences.RemoveAt(lastPosition);
presencesMap.Set(subject, presences);

if (Runtime.ProtocolVersion >= 19)
{
var votersRoundMap = _pollVotersRound.Get<string, StorageMap>(subject);
if (votersRoundMap.ContainsKey<BigInteger>(poll.round))
{
var votersRound = votersRoundMap.Get<BigInteger, StorageList>(poll.round);
votersRound.Remove(from);
votersRoundMap.Set<BigInteger, StorageList>(poll.round, votersRound);
}
}
}

/// <summary>
/// Remove votes from the address in a poll
/// </summary>
/// <param name="from"></param>
/// <param name="subject"></param>
public void RemoveVotes(Address from, string subject)
{
Runtime.Expect(Runtime.IsWitness(from), "invalid witness");
Runtime.Expect(_pollMap.ContainsKey(subject), "invalid poll subject");

var poll = FetchPoll(subject);
Runtime.Expect(poll.state == PollState.Active, "poll not active");

var organization = Runtime.GetOrganization(poll.organization);
Runtime.Expect(organization.IsMember(from), "must be member of organization: " + poll.organization);

RemoveVotesInternal(from, subject, poll);
}

/// <summary>
Expand Down Expand Up @@ -791,6 +902,86 @@ private bool HasAlreadyVoted(ConsensusPoll poll, string subject, StorageList pre

return presences.Get<PollPresenceVotes>(count - 1).round == poll.round;
}

/// <summary>
/// Calculate Power
/// </summary>
/// <param name="from"></param>
/// <param name="poll"></param>
/// <returns></returns>
private BigInteger CalculateVotingPower(Address from, ConsensusPoll poll)
{
BigInteger votingPower = 0;

if (poll.organization == DomainSettings.StakersOrganizationName ||
poll.organization == DomainSettings.MastersOrganizationName)
{
votingPower = Runtime
.CallNativeContext(NativeContractKind.Stake, nameof(StakeContract.GetAddressVotingPower), from)
.AsNumber();
}
else
{
votingPower = 100;
}

return votingPower;
}

/// <summary>
/// This method is used to update members of a poll
/// </summary>
/// <param name="from"></param>
/// <param name="subject"></param>
public void UpdatePollStatus(Address from, string subject)
{
Runtime.Expect(_pollMap.ContainsKey(subject), "invalid poll subject");
Runtime.Expect(Runtime.IsWitness(from), "invalid witness");
//Runtime.Expect(Runtime.IsKnownValidator(from), "invalid validator"); No need to be called by a validator

var poll = FetchPoll(subject);
Runtime.Expect(poll.state == PollState.Active, "poll not active");
var pollVotersMap = _pollVotersRound.Get<string, StorageMap>(subject);
Runtime.Expect(pollVotersMap.ContainsKey<BigInteger>(poll.round), "invalid round");
var pollVoters = pollVotersMap.Get<BigInteger, StorageList>(poll.round);
foreach (var voter in pollVoters.All<Address>())
{
// Check Staking - No Staking Remove Vote
var stake = Runtime.CallNativeContext(NativeContractKind.Stake, nameof(StakeContract.GetStake), voter).AsNumber();
if (stake <= 0)
{
RemoveVotesInternal(voter, subject, poll);
continue;
}

// Calculate Voting Power
BigInteger previousVotingPower = 0;
var presencesMap = _pollVotesPerAddress.Get<Address, StorageMap>(from);
var presences = presencesMap.Get<string, StorageList>(subject);
var presenceVotes = presences.Get<PollPresenceVotes>(presences.Count() - 1);
for (int i = 0; i < presenceVotes.votes.Length; i++)
{
var targetIndex = (int)presenceVotes.votes[i].Choice.index;
previousVotingPower += presenceVotes.votes[i].NumberOfVotes * 100 / presenceVotes.votes[i].Choice.percentage;
}

var currentVotingPower = CalculateVotingPower(voter, poll);

if (currentVotingPower == previousVotingPower)
{
continue;
}

// Update Poll Entries
var choices = new PollVote[presenceVotes.votes.Length];
for (int i = 0; i < presenceVotes.votes.Length; i++)
{
choices[i] = presenceVotes.votes[i].Choice;
}

UpdateVotesInternal(voter, poll, subject, ref presences, ref presencesMap, choices, currentVotingPower);
}
}

/// <summary>
/// Check if a value has consensus in a poll
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public void CreateLeaderboard(Address from, string name, BigInteger size)

Runtime.Notify(EventKind.LeaderboardCreate, from, name);
}

/// <summary>
/// Method used to reset a leaderboard
/// </summary>
Expand All @@ -85,6 +85,11 @@ public void ResetLeaderboard(Address from, string name)
var rows = _rows.Get<string, StorageList>(name);
rows.Clear();

if (Runtime.ProtocolVersion >= 19)
{
_leaderboards.Set(name, leaderboard);
}

Runtime.Notify(EventKind.LeaderboardReset, from, name);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
using System.IO;
using System.Numerics;
using Phantasma.Core.Cryptography;
using Phantasma.Core.Cryptography.Structs;
using Phantasma.Core.Domain.Interfaces;
using Phantasma.Core.Utils;

namespace Phantasma.Core.Domain.Contract.LeaderboardDetails.Structs
{
public struct Leaderboard
public struct Leaderboard : ISerializable
{
public string name;
public Address owner;
public BigInteger size;
public BigInteger round;

public void SerializeData(BinaryWriter writer)
{
writer.WriteVarString(name);
writer.WriteAddress(owner);
writer.WriteBigInteger(size);
writer.WriteBigInteger(round);
}

public void UnserializeData(BinaryReader reader)
{
name = reader.ReadVarString();
owner = reader.ReadAddress();
size = reader.ReadBigInteger();
round = reader.ReadBigInteger();
}
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
using System.IO;
using System.Numerics;
using Phantasma.Core.Cryptography;
using Phantasma.Core.Cryptography.Structs;
using Phantasma.Core.Domain.Interfaces;
using Phantasma.Core.Utils;

namespace Phantasma.Core.Domain.Contract.LeaderboardDetails.Structs;

public struct LeaderboardRow
public struct LeaderboardRow : ISerializable
{
public Address address;
public BigInteger score;
public void SerializeData(BinaryWriter writer)
{
writer.WriteAddress(address);
writer.WriteBigInteger(score);
}

public void UnserializeData(BinaryReader reader)
{
address = reader.ReadAddress();
score = reader.ReadBigInteger();
}
}
4 changes: 2 additions & 2 deletions Phantasma.Infrastructure/src/API/APIChainService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,11 @@ public LeaderboardResult GetLeaderboard(string name)
var nexus = NexusAPI.GetNexus();

var temp = nexus.RootChain.InvokeContractAtTimestamp(nexus.RootChain.Storage, Timestamp.Now, "ranking",
nameof(RankingContract.GetRows), name).ToObject();
nameof(RankingContract.GetRows), name);

try
{
var board = ((LeaderboardRow[])temp).ToArray();
LeaderboardRow[] board = temp.ToArray<LeaderboardRow>();

return new LeaderboardResult()
{
Expand Down
Loading