supporting multiple minting groups instead of supporting one and only one minting group

This commit is contained in:
kennycud 2025-02-03 18:19:56 -08:00
parent 9017db725e
commit 91ceafe0e3
10 changed files with 473 additions and 26 deletions

View File

@ -14,6 +14,7 @@ import org.qortal.repository.NameRepository;
import org.qortal.repository.Repository;
import org.qortal.settings.Settings;
import org.qortal.utils.Base58;
import org.qortal.utils.Groups;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@ -227,7 +228,7 @@ public class Account {
}
int level = accountData.getLevel();
int groupIdToMint = BlockChain.getInstance().getMintingGroupId();
List<Integer> groupIdsToMint = Groups.getGroupIdsToMint( BlockChain.getInstance(), blockchainHeight );
int nameCheckHeight = BlockChain.getInstance().getOnlyMintWithNameHeight();
int groupCheckHeight = BlockChain.getInstance().getGroupMemberCheckHeight();
int removeNameCheckHeight = BlockChain.getInstance().getRemoveOnlyMintWithNameHeight();
@ -261,9 +262,9 @@ public class Account {
if (blockchainHeight >= groupCheckHeight && blockchainHeight < removeNameCheckHeight) {
List<NameData> myName = nameRepository.getNamesByOwner(myAddress);
if (Account.isFounder(accountData.getFlags())) {
return accountData.getBlocksMintedPenalty() == 0 && !myName.isEmpty() && (isGroupValidated || groupRepository.memberExists(groupIdToMint, myAddress));
return accountData.getBlocksMintedPenalty() == 0 && !myName.isEmpty() && (isGroupValidated || Groups.memberExistsInAnyGroup(groupRepository, groupIdsToMint, myAddress));
} else {
return level >= levelToMint && !myName.isEmpty() && (isGroupValidated || groupRepository.memberExists(groupIdToMint, myAddress));
return level >= levelToMint && !myName.isEmpty() && (isGroupValidated || Groups.memberExistsInAnyGroup(groupRepository, groupIdsToMint, myAddress));
}
}
@ -272,9 +273,9 @@ public class Account {
// Account's address is a member of the minter group
if (blockchainHeight >= removeNameCheckHeight) {
if (Account.isFounder(accountData.getFlags())) {
return accountData.getBlocksMintedPenalty() == 0 && (isGroupValidated || groupRepository.memberExists(groupIdToMint, myAddress));
return accountData.getBlocksMintedPenalty() == 0 && (isGroupValidated || Groups.memberExistsInAnyGroup(groupRepository, groupIdsToMint, myAddress));
} else {
return level >= levelToMint && (isGroupValidated || groupRepository.memberExists(groupIdToMint, myAddress));
return level >= levelToMint && (isGroupValidated || Groups.memberExistsInAnyGroup(groupRepository, groupIdsToMint, myAddress));
}
}

View File

@ -39,6 +39,7 @@ import org.qortal.transform.block.BlockTransformer;
import org.qortal.transform.transaction.TransactionTransformer;
import org.qortal.utils.Amounts;
import org.qortal.utils.Base58;
import org.qortal.utils.Groups;
import org.qortal.utils.NTP;
import java.io.ByteArrayOutputStream;
@ -150,7 +151,7 @@ public class Block {
final BlockChain blockChain = BlockChain.getInstance();
ExpandedAccount(Repository repository, RewardShareData rewardShareData) throws DataException {
ExpandedAccount(Repository repository, RewardShareData rewardShareData, int blockHeight) throws DataException {
this.rewardShareData = rewardShareData;
this.sharePercent = this.rewardShareData.getSharePercent();
@ -159,7 +160,12 @@ public class Block {
this.isMinterFounder = Account.isFounder(mintingAccountData.getFlags());
this.isRecipientAlsoMinter = this.rewardShareData.getRecipient().equals(this.mintingAccount.getAddress());
this.isMinterMember = repository.getGroupRepository().memberExists(BlockChain.getInstance().getMintingGroupId(), this.mintingAccount.getAddress());
this.isMinterMember
= Groups.memberExistsInAnyGroup(
repository.getGroupRepository(),
Groups.getGroupIdsToMint(BlockChain.getInstance(), blockHeight),
this.mintingAccount.getAddress()
);
if (this.isRecipientAlsoMinter) {
// Self-share: minter is also recipient
@ -435,9 +441,9 @@ public class Block {
if (height >= BlockChain.getInstance().getGroupMemberCheckHeight()) {
onlineAccounts.removeIf(a -> {
try {
int groupId = BlockChain.getInstance().getMintingGroupId();
List<Integer> groupIdsToMint = Groups.getGroupIdsToMint(BlockChain.getInstance(), height);
String address = Account.getRewardShareMintingAddress(repository, a.getPublicKey());
boolean isMinterGroupMember = repository.getGroupRepository().memberExists(groupId, address);
boolean isMinterGroupMember = Groups.memberExistsInAnyGroup(repository.getGroupRepository(), groupIdsToMint, address);
return !isMinterGroupMember;
} catch (DataException e) {
// Something went wrong, so remove the account
@ -753,7 +759,7 @@ public class Block {
List<ExpandedAccount> expandedAccounts = new ArrayList<>();
for (RewardShareData rewardShare : this.cachedOnlineRewardShares) {
expandedAccounts.add(new ExpandedAccount(repository, rewardShare));
expandedAccounts.add(new ExpandedAccount(repository, rewardShare, this.blockData.getHeight()));
}
this.cachedExpandedAccounts = expandedAccounts;
@ -2485,11 +2491,10 @@ public class Block {
try (final Repository repository = RepositoryManager.getRepository()) {
GroupRepository groupRepository = repository.getGroupRepository();
List<Integer> mintingGroupIds = Groups.getGroupIdsToMint(BlockChain.getInstance(), this.blockData.getHeight());
// all minter admins
List<String> minterAdmins
= groupRepository.getGroupAdmins(BlockChain.getInstance().getMintingGroupId()).stream()
.map(GroupAdminData::getAdmin)
.collect(Collectors.toList());
List<String> minterAdmins = Groups.getAllAdmins(groupRepository, mintingGroupIds);
// all minter admins that are online
List<ExpandedAccount> onlineMinterAdminAccounts

View File

@ -212,7 +212,13 @@ public class BlockChain {
private int minAccountLevelToRewardShare;
private int maxRewardSharesPerFounderMintingAccount;
private int founderEffectiveMintingLevel;
private int mintingGroupId;
public static class IdsForHeight {
public int height;
public List<Integer> ids;
}
private List<IdsForHeight> mintingGroupIds;
/** Minimum time to retain online account signatures (ms) for block validity checks. */
private long onlineAccountSignaturesMinLifetime;
@ -544,8 +550,8 @@ public class BlockChain {
return this.onlineAccountSignaturesMaxLifetime;
}
public int getMintingGroupId() {
return this.mintingGroupId;
public List<IdsForHeight> getMintingGroupIds() {
return mintingGroupIds;
}
public CiyamAtSettings getCiyamAtSettings() {

View File

@ -25,6 +25,7 @@ import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.utils.Base58;
import org.qortal.utils.Groups;
import org.qortal.utils.NTP;
import org.qortal.utils.NamedThreadFactory;
@ -225,11 +226,14 @@ public class OnlineAccountsManager {
Set<OnlineAccountData> onlineAccountsToAdd = new HashSet<>();
Set<OnlineAccountData> onlineAccountsToRemove = new HashSet<>();
try (final Repository repository = RepositoryManager.getRepository()) {
int blockHeight = repository.getBlockRepository().getBlockchainHeight();
List<String> mintingGroupMemberAddresses
= repository.getGroupRepository()
.getGroupMembers(BlockChain.getInstance().getMintingGroupId()).stream()
.map(GroupMemberData::getMember)
.collect(Collectors.toList());
= Groups.getAllMembers(
repository.getGroupRepository(),
Groups.getGroupIdsToMint(BlockChain.getInstance(), blockHeight)
);
for (OnlineAccountData onlineAccountData : this.onlineAccountsImportQueue) {
if (isStopping)

View File

@ -53,10 +53,10 @@ public class Blocks {
// all minting group member addresses
List<String> mintingGroupAddresses
= repository.getGroupRepository()
.getGroupMembers(BlockChain.getInstance().getMintingGroupId()).stream()
.map(GroupMemberData::getMember)
.collect(Collectors.toList());
= Groups.getAllMembers(
repository.getGroupRepository(),
Groups.getGroupIdsToMint(BlockChain.getInstance(), blockData.getHeight())
);
// all names, indexed by address
Map<String, String> nameByAddress

View File

@ -0,0 +1,122 @@
package org.qortal.utils;
import org.qortal.block.BlockChain;
import org.qortal.data.group.GroupAdminData;
import org.qortal.data.group.GroupMemberData;
import org.qortal.repository.DataException;
import org.qortal.repository.GroupRepository;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Class Groups
*
* A utility class for group related functionality.
*/
public class Groups {
/**
* Does the member exist in any of these groups?
*
* @param groupRepository the group data repository
* @param groupsIds the group Ids to look for the address
* @param address the address
*
* @return true if the address is in any of the groups listed otherwise false
* @throws DataException
*/
public static boolean memberExistsInAnyGroup(GroupRepository groupRepository, List<Integer> groupsIds, String address) throws DataException {
// if any of the listed groups have the address as a member, then return true
for( Integer groupIdToMint : groupsIds) {
if( groupRepository.memberExists(groupIdToMint, address) ) {
return true;
}
}
// if none of the listed groups have the address as a member, then return false
return false;
}
/**
* Get All Members
*
* Get all the group members from a list of groups.
*
* @param groupRepository the group data repository
* @param groupIds the list of group Ids to look at
*
* @return the list of all members belonging to any of the groups, no duplicates
* @throws DataException
*/
public static List<String> getAllMembers( GroupRepository groupRepository, List<Integer> groupIds ) throws DataException {
// collect all the members in a set, the set keeps out duplicates
Set<String> allMembers = new HashSet<>();
// add all members from each group to the all members set
for( int groupId : groupIds ) {
allMembers.addAll( groupRepository.getGroupMembers(groupId).stream().map(GroupMemberData::getMember).collect(Collectors.toList()));
}
return new ArrayList<>(allMembers);
}
/**
* Get All Admins
*
* Get all the admins from a list of groups.
*
* @param groupRepository the group data repository
* @param groupIds the list of group Ids to look at
*
* @return the list of all admins to any of the groups, no duplicates
* @throws DataException
*/
public static List<String> getAllAdmins( GroupRepository groupRepository, List<Integer> groupIds ) throws DataException {
// collect all the admins in a set, the set keeps out duplicates
Set<String> allAdmins = new HashSet<>();
// collect admins for each group
for( int groupId : groupIds ) {
allAdmins.addAll( groupRepository.getGroupAdmins(groupId).stream().map(GroupAdminData::getAdmin).collect(Collectors.toList()) );
}
return new ArrayList<>(allAdmins);
}
/**
* Get Group Ids To Mint
*
* @param blockchain the blockchain
* @param blockchainHeight the block height to mint
*
* @return the group Ids for the minting groups at the height given
*/
public static List<Integer> getGroupIdsToMint(BlockChain blockchain, int blockchainHeight) {
// sort heights lowest to highest
Comparator<BlockChain.IdsForHeight> compareByHeight = Comparator.comparingInt(entry -> entry.height);
// sort heights highest to lowest
Comparator<BlockChain.IdsForHeight> compareByHeightReversed = compareByHeight.reversed();
// get highest height that is less than the blockchain height
Optional<BlockChain.IdsForHeight> ids = blockchain.getMintingGroupIds().stream()
.filter(entry -> entry.height < blockchainHeight)
.sorted(compareByHeightReversed)
.findFirst();
if( ids.isPresent()) {
return ids.get().ids;
}
else {
return new ArrayList<>(0);
}
}
}

View File

@ -38,7 +38,9 @@
"blockRewardBatchStartHeight": 1508000,
"blockRewardBatchSize": 1000,
"blockRewardBatchAccountsBlockCount": 25,
"mintingGroupId": 694,
"mintingGroupIds": [
{ "height": 0, "ids": [ 694 ]}
],
"rewardsByHeight": [
{ "height": 1, "reward": 5.00 },
{ "height": 259201, "reward": 4.75 },

View File

@ -0,0 +1,102 @@
package org.qortal.test.utils;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.data.transaction.CreateGroupTransactionData;
import org.qortal.data.transaction.GroupInviteTransactionData;
import org.qortal.data.transaction.JoinGroupTransactionData;
import org.qortal.data.transaction.LeaveGroupTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.test.common.TransactionUtils;
import org.qortal.test.common.transaction.TestTransaction;
/**
* Class GroupsTestUtils
*
* Utility methods for testing the Groups class.
*/
public class GroupsTestUtils {
/**
* Create Group
*
* @param repository the data repository
* @param owner the group owner
* @param groupName the group name
* @param isOpen true if the group is public, false for private
*
* @return the group Id
* @throws DataException
*/
public static Integer createGroup(Repository repository, PrivateKeyAccount owner, String groupName, boolean isOpen) throws DataException {
String description = groupName + " (description)";
Group.ApprovalThreshold approvalThreshold = Group.ApprovalThreshold.ONE;
int minimumBlockDelay = 10;
int maximumBlockDelay = 1440;
CreateGroupTransactionData transactionData = new CreateGroupTransactionData(TestTransaction.generateBase(owner), groupName, description, isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay);
TransactionUtils.signAndMint(repository, transactionData, owner);
return repository.getGroupRepository().fromGroupName(groupName).getGroupId();
}
/**
* Join Group
*
* @param repository the data repository
* @param joiner the address for the account joining the group
* @param groupId the Id for the group to join
*
* @throws DataException
*/
public static void joinGroup(Repository repository, PrivateKeyAccount joiner, int groupId) throws DataException {
JoinGroupTransactionData transactionData = new JoinGroupTransactionData(TestTransaction.generateBase(joiner), groupId);
TransactionUtils.signAndMint(repository, transactionData, joiner);
}
/**
* Group Invite
*
* @param repository the data repository
* @param admin the admin account to sign the invite
* @param groupId the Id of the group to invite to
* @param invitee the recipient address for the invite
* @param timeToLive the time length of the invite
*
* @throws DataException
*/
public static void groupInvite(Repository repository, PrivateKeyAccount admin, int groupId, String invitee, int timeToLive) throws DataException {
GroupInviteTransactionData transactionData = new GroupInviteTransactionData(TestTransaction.generateBase(admin), groupId, invitee, timeToLive);
TransactionUtils.signAndMint(repository, transactionData, admin);
}
/**
* Leave Group
*
* @param repository the data repository
* @param leaver the account leaving
* @param groupId the Id of the group being left
*
* @throws DataException
*/
public static void leaveGroup(Repository repository, PrivateKeyAccount leaver, int groupId) throws DataException {
LeaveGroupTransactionData transactionData = new LeaveGroupTransactionData(TestTransaction.generateBase(leaver), groupId);
TransactionUtils.signAndMint(repository, transactionData, leaver);
}
/**
* Is Member?
*
* @param repository the data repository
* @param address the account address
* @param groupId the group Id
*
* @return true if the account is a member of the group, otherwise false
* @throws DataException
*/
public static boolean isMember(Repository repository, String address, int groupId) throws DataException {
return repository.getGroupRepository().memberExists(groupId, address);
}
}

View File

@ -0,0 +1,199 @@
package org.qortal.test.utils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.block.Block;
import org.qortal.block.BlockChain;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.utils.Groups;
import java.util.List;
import static org.junit.Assert.*;
public class GroupsTests extends Common {
public static final String ALICE = "alice";
public static final String BOB = "bob";
public static final String CHLOE = "chloe";
public static final String DILBERT = "dilbert";
private static final int HEIGHT_1 = 5;
private static final int HEIGHT_2 = 8;
private static final int HEIGHT_3 = 12;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@After
public void afterTest() throws DataException {
Common.orphanCheck();
}
@Test
public void testGetGroupIdsToMintSimple() {
List<Integer> ids = Groups.getGroupIdsToMint(BlockChain.getInstance(), 0);
Assert.assertNotNull(ids);
Assert.assertEquals(0, ids.size());
}
@Test
public void testGetGroupIdsToMintComplex() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
Block block1 = BlockUtils.mintBlocks(repository, HEIGHT_1);
int height1 = block1.getBlockData().getHeight().intValue();
assertEquals(HEIGHT_1 + 1, height1);
List<Integer> ids1 = Groups.getGroupIdsToMint(BlockChain.getInstance(), height1);
Assert.assertEquals(1, ids1.size() );
Assert.assertTrue( ids1.contains( 694 ) );
Block block2 = BlockUtils.mintBlocks(repository, HEIGHT_2 - HEIGHT_1);
int height2 = block2.getBlockData().getHeight().intValue();
assertEquals( HEIGHT_2 + 1, height2);
List<Integer> ids2 = Groups.getGroupIdsToMint(BlockChain.getInstance(), height2);
Assert.assertEquals(2, ids2.size() );
Assert.assertTrue( ids2.contains( 694 ) );
Assert.assertTrue( ids2.contains( 800 ) );
Block block3 = BlockUtils.mintBlocks(repository, HEIGHT_3 - HEIGHT_2);
int height3 = block3.getBlockData().getHeight().intValue();
assertEquals( HEIGHT_3 + 1, height3);
List<Integer> ids3 = Groups.getGroupIdsToMint(BlockChain.getInstance(), height3);
Assert.assertEquals( 1, ids3.size() );
Assert.assertTrue( ids3.contains( 800 ) );
}
}
@Test
public void testMemberExistsInAnyGroupSimple() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, "alice");
PrivateKeyAccount bob = Common.getTestAccount(repository, "bob");
// Create group
int groupId = GroupsTestUtils.createGroup(repository, alice, "closed-group", false);
// Confirm Bob is not a member
Assert.assertFalse( Groups.memberExistsInAnyGroup(repository.getGroupRepository(), List.of(groupId), bob.getAddress()) );
// Bob to join
GroupsTestUtils.joinGroup(repository, bob, groupId);
// Confirm Bob still not a member
assertFalse(GroupsTestUtils.isMember(repository, bob.getAddress(), groupId));
// Have Alice 'invite' Bob to confirm membership
GroupsTestUtils.groupInvite(repository, alice, groupId, bob.getAddress(), 0); // non-expiring invite
// Confirm Bob now a member
Assert.assertTrue( Groups.memberExistsInAnyGroup(repository.getGroupRepository(), List.of(groupId), bob.getAddress()) );
}
}
@Test
public void testGroupsListedFunctionality() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE);
PrivateKeyAccount bob = Common.getTestAccount(repository, BOB);
PrivateKeyAccount chloe = Common.getTestAccount(repository, CHLOE);
PrivateKeyAccount dilbert = Common.getTestAccount(repository, DILBERT);
// Create groups
int group1Id = GroupsTestUtils.createGroup(repository, alice, "group-1", false);
int group2Id = GroupsTestUtils.createGroup(repository, bob, "group-2", false);
// test memberExistsInAnyGroup
Assert.assertTrue(Groups.memberExistsInAnyGroup(repository.getGroupRepository(), List.of(group1Id, group2Id), alice.getAddress()));
Assert.assertFalse(Groups.memberExistsInAnyGroup(repository.getGroupRepository(), List.of(group1Id, group2Id), chloe.getAddress()));
// alice is a member
Assert.assertTrue(GroupsTestUtils.isMember(repository, alice.getAddress(), group1Id));
List<String> allMembersBeforeJoin = Groups.getAllMembers(repository.getGroupRepository(), List.of(group1Id));
// assert one member
Assert.assertNotNull(allMembersBeforeJoin);
Assert.assertEquals(1, allMembersBeforeJoin.size());
List<String> allAdminsBeforeJoin = Groups.getAllAdmins(repository.getGroupRepository(), List.of(group1Id));
// assert one admin
Assert.assertNotNull(allAdminsBeforeJoin);
Assert.assertEquals( 1, allAdminsBeforeJoin.size());
// Bob to join
GroupsTestUtils.joinGroup(repository, bob, group1Id);
// Have Alice 'invite' Bob to confirm membership
GroupsTestUtils.groupInvite(repository, alice, group1Id, bob.getAddress(), 0); // non-expiring invite
List<String> allMembersAfterJoin = Groups.getAllMembers(repository.getGroupRepository(), List.of(group1Id));
// alice and bob are members
Assert.assertNotNull(allMembersAfterJoin);
Assert.assertEquals(2, allMembersAfterJoin.size());
List<String> allAdminsAfterJoin = Groups.getAllAdmins(repository.getGroupRepository(), List.of(group1Id));
// assert still one admin
Assert.assertNotNull(allAdminsAfterJoin);
Assert.assertEquals(1, allAdminsAfterJoin.size());
List<String> allAdminsFor2Groups = Groups.getAllAdmins(repository.getGroupRepository(), List.of(group1Id, group2Id));
// assert 2 admins when including the second group
Assert.assertNotNull(allAdminsFor2Groups);
Assert.assertEquals(2, allAdminsFor2Groups.size());
List<String> allMembersFor2Groups = Groups.getAllMembers(repository.getGroupRepository(), List.of(group1Id, group2Id));
// assert 2 members when including the seconds group
Assert.assertNotNull(allMembersFor2Groups);
Assert.assertEquals(2, allMembersFor2Groups.size());
GroupsTestUtils.leaveGroup(repository, bob, group1Id);
List<String> allMembersForAfterBobLeavesGroup1InAllGroups = Groups.getAllMembers(repository.getGroupRepository(), List.of(group1Id, group2Id));
// alice and bob are members of one group still
Assert.assertNotNull(allMembersForAfterBobLeavesGroup1InAllGroups);
Assert.assertEquals(2, allMembersForAfterBobLeavesGroup1InAllGroups.size());
GroupsTestUtils.groupInvite(repository, alice, group1Id, chloe.getAddress(), 3600);
GroupsTestUtils.groupInvite(repository, bob, group2Id, chloe.getAddress(), 3600);
GroupsTestUtils.joinGroup(repository, chloe, group1Id);
GroupsTestUtils.joinGroup(repository, chloe, group2Id);
List<String> allMembersAfterDilbert = Groups.getAllMembers((repository.getGroupRepository()), List.of(group1Id, group2Id));
// 3 accounts are now members of one group or another
Assert.assertNotNull(allMembersAfterDilbert);
Assert.assertEquals(3, allMembersAfterDilbert.size());
}
}
}

View File

@ -31,6 +31,12 @@
"blockRewardBatchStartHeight": 999999000,
"blockRewardBatchSize": 10,
"blockRewardBatchAccountsBlockCount": 3,
"mintingGroupIds": [
{ "height": 0, "ids": []},
{ "height": 5, "ids": [694]},
{ "height": 8, "ids": [694, 800]},
{ "height": 12, "ids": [800]}
],
"rewardsByHeight": [
{ "height": 1, "reward": 100 },
{ "height": 11, "reward": 10 },