diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index 378d6fb5..42da0b3a 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -90,7 +90,8 @@ public class BlockChain { groupMemberCheckHeight, fixBatchRewardHeight, adminsReplaceFoundersHeight, - onlineValidationFailSafeHeight + onlineValidationFailSafeHeight, + nullGroupMembershipHeight } // Custom transaction fees @@ -672,6 +673,10 @@ public class BlockChain { return this.featureTriggers.get(FeatureTrigger.onlineValidationFailSafeHeight.name()).intValue(); } + public int getNullGroupMembershipHeight() { + return this.featureTriggers.get(FeatureTrigger.nullGroupMembershipHeight.name()).intValue(); + } + // More complex getters for aspects that change by height or timestamp public long getRewardAtHeight(int ourHeight) { diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBGroupRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBGroupRepository.java index 9c7521fc..a364ed32 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBGroupRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBGroupRepository.java @@ -24,7 +24,7 @@ public class HSQLDBGroupRepository implements GroupRepository { public GroupData fromGroupId(int groupId) throws DataException { String sql = "SELECT group_name, owner, description, created_when, updated_when, reference, is_open, " + "approval_threshold, min_block_delay, max_block_delay, creation_group_id, reduced_group_name " - + "FROM Groups WHERE group_id = ?"; + + "FROM `Groups` WHERE group_id = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, groupId)) { if (resultSet == null) @@ -62,7 +62,7 @@ public class HSQLDBGroupRepository implements GroupRepository { public GroupData fromGroupName(String groupName) throws DataException { String sql = "SELECT group_id, owner, description, created_when, updated_when, reference, is_open, " + "approval_threshold, min_block_delay, max_block_delay, creation_group_id, reduced_group_name " - + "FROM Groups WHERE group_name = ?"; + + "FROM `Groups` WHERE group_name = ?"; try (ResultSet resultSet = this.repository.checkedExecute(sql, groupName)) { if (resultSet == null) @@ -99,7 +99,7 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public boolean groupExists(int groupId) throws DataException { try { - return this.repository.exists("Groups", "group_id = ?", groupId); + return this.repository.exists("`Groups`", "group_id = ?", groupId); } catch (SQLException e) { throw new DataException("Unable to check for group in repository", e); } @@ -108,7 +108,7 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public boolean groupExists(String groupName) throws DataException { try { - return this.repository.exists("Groups", "group_name = ?", groupName); + return this.repository.exists("`Groups`", "group_name = ?", groupName); } catch (SQLException e) { throw new DataException("Unable to check for group in repository", e); } @@ -117,7 +117,7 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public boolean reducedGroupNameExists(String reducedGroupName) throws DataException { try { - return this.repository.exists("Groups", "reduced_group_name = ?", reducedGroupName); + return this.repository.exists("`Groups`", "reduced_group_name = ?", reducedGroupName); } catch (SQLException e) { throw new DataException("Unable to check for reduced group name in repository", e); } @@ -129,7 +129,7 @@ public class HSQLDBGroupRepository implements GroupRepository { sql.append("SELECT group_id, owner, group_name, description, created_when, updated_when, reference, is_open, " + "approval_threshold, min_block_delay, max_block_delay, creation_group_id, reduced_group_name " - + "FROM Groups ORDER BY group_name"); + + "FROM `Groups` ORDER BY group_name"); if (reverse != null && reverse) sql.append(" DESC"); @@ -181,7 +181,7 @@ public class HSQLDBGroupRepository implements GroupRepository { sql.append("SELECT group_id, group_name, description, created_when, updated_when, reference, is_open, " + "approval_threshold, min_block_delay, max_block_delay, creation_group_id, reduced_group_name " - + "FROM Groups WHERE owner = ? ORDER BY group_name"); + + "FROM `Groups` WHERE owner = ? ORDER BY group_name"); if (reverse != null && reverse) sql.append(" DESC"); @@ -231,7 +231,7 @@ public class HSQLDBGroupRepository implements GroupRepository { StringBuilder sql = new StringBuilder(512); sql.append("SELECT group_id, owner, group_name, description, created_when, updated_when, reference, is_open, " - + "approval_threshold, min_block_delay, max_block_delay, creation_group_id, reduced_group_name, admin FROM Groups " + + "approval_threshold, min_block_delay, max_block_delay, creation_group_id, reduced_group_name, admin FROM `Groups` " + "JOIN GroupMembers USING (group_id) " + "LEFT OUTER JOIN GroupAdmins ON GroupAdmins.group_id = GroupMembers.group_id AND GroupAdmins.admin = GroupMembers.address " + "WHERE address = ? ORDER BY group_name"); @@ -289,7 +289,7 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public void save(GroupData groupData) throws DataException { - HSQLDBSaver saveHelper = new HSQLDBSaver("Groups"); + HSQLDBSaver saveHelper = new HSQLDBSaver("`Groups`"); saveHelper.bind("group_id", groupData.getGroupId()).bind("owner", groupData.getOwner()).bind("group_name", groupData.getGroupName()) .bind("description", groupData.getDescription()).bind("created_when", groupData.getCreated()).bind("updated_when", groupData.getUpdated()) @@ -302,7 +302,7 @@ public class HSQLDBGroupRepository implements GroupRepository { if (groupData.getGroupId() == null) { // Fetch new groupId - try (ResultSet resultSet = this.repository.checkedExecute("SELECT group_id FROM Groups WHERE reference = ?", groupData.getReference())) { + try (ResultSet resultSet = this.repository.checkedExecute("SELECT group_id FROM `Groups` WHERE reference = ?", groupData.getReference())) { if (resultSet == null) throw new DataException("Unable to fetch new group ID from repository"); @@ -318,7 +318,7 @@ public class HSQLDBGroupRepository implements GroupRepository { public void delete(int groupId) throws DataException { try { // Remove group - this.repository.delete("Groups", "group_id = ?", groupId); + this.repository.delete("`Groups`", "group_id = ?", groupId); } catch (SQLException e) { throw new DataException("Unable to delete group info from repository", e); } @@ -328,7 +328,7 @@ public class HSQLDBGroupRepository implements GroupRepository { public void delete(String groupName) throws DataException { try { // Remove group - this.repository.delete("Groups", "group_name = ?", groupName); + this.repository.delete("`Groups`", "group_name = ?", groupName); } catch (SQLException e) { throw new DataException("Unable to delete group info from repository", e); } @@ -338,7 +338,7 @@ public class HSQLDBGroupRepository implements GroupRepository { @Override public String getOwner(int groupId) throws DataException { - try (ResultSet resultSet = this.repository.checkedExecute("SELECT owner FROM Groups WHERE group_id = ?", groupId)) { + try (ResultSet resultSet = this.repository.checkedExecute("SELECT owner FROM `Groups` WHERE group_id = ?", groupId)) { if (resultSet == null) return null; diff --git a/src/main/java/org/qortal/transaction/CancelGroupBanTransaction.java b/src/main/java/org/qortal/transaction/CancelGroupBanTransaction.java index f3511ded..95a267f3 100644 --- a/src/main/java/org/qortal/transaction/CancelGroupBanTransaction.java +++ b/src/main/java/org/qortal/transaction/CancelGroupBanTransaction.java @@ -2,6 +2,7 @@ package org.qortal.transaction; import org.qortal.account.Account; import org.qortal.asset.Asset; +import org.qortal.block.BlockChain; import org.qortal.crypto.Crypto; import org.qortal.data.group.GroupData; import org.qortal.data.transaction.CancelGroupBanTransactionData; @@ -12,6 +13,7 @@ import org.qortal.repository.Repository; import java.util.Collections; import java.util.List; +import java.util.Objects; public class CancelGroupBanTransaction extends Transaction { @@ -70,9 +72,26 @@ public class CancelGroupBanTransaction extends Transaction { if (!this.repository.getGroupRepository().adminExists(groupId, admin.getAddress())) return ValidationResult.NOT_GROUP_ADMIN; - // Can't unban if not group's current owner - if (!admin.getAddress().equals(groupData.getOwner())) - return ValidationResult.INVALID_GROUP_OWNER; + if( this.repository.getBlockRepository().getBlockchainHeight() < BlockChain.getInstance().getNullGroupMembershipHeight() ) { + // Can't cancel ban if not group's current owner + if (!admin.getAddress().equals(groupData.getOwner())) + return ValidationResult.INVALID_GROUP_OWNER; + } + // if( this.repository.getBlockRepository().getBlockchainHeight() >= BlockChain.getInstance().getNullGroupMembershipHeight() ) + else { + String groupOwner = this.repository.getGroupRepository().getOwner(groupId); + boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS); + + // if null ownership group, then check for admin approval + if(groupOwnedByNullAccount ) { + // Require approval if transaction relates to a group owned by the null account + if (!this.needsGroupApproval()) + return ValidationResult.GROUP_APPROVAL_REQUIRED; + } + // Can't cancel ban if not group's current owner + else if (!admin.getAddress().equals(groupData.getOwner())) + return ValidationResult.INVALID_GROUP_OWNER; + } Account member = getMember(); diff --git a/src/main/java/org/qortal/transaction/CancelGroupInviteTransaction.java b/src/main/java/org/qortal/transaction/CancelGroupInviteTransaction.java index d4306bbe..678aa411 100644 --- a/src/main/java/org/qortal/transaction/CancelGroupInviteTransaction.java +++ b/src/main/java/org/qortal/transaction/CancelGroupInviteTransaction.java @@ -2,6 +2,7 @@ package org.qortal.transaction; import org.qortal.account.Account; import org.qortal.asset.Asset; +import org.qortal.block.BlockChain; import org.qortal.crypto.Crypto; import org.qortal.data.group.GroupData; import org.qortal.data.transaction.CancelGroupInviteTransactionData; @@ -12,6 +13,7 @@ import org.qortal.repository.Repository; import java.util.Collections; import java.util.List; +import java.util.Objects; public class CancelGroupInviteTransaction extends Transaction { @@ -80,6 +82,16 @@ public class CancelGroupInviteTransaction extends Transaction { if (admin.getConfirmedBalance(Asset.QORT) < this.cancelGroupInviteTransactionData.getFee()) return ValidationResult.NO_BALANCE; + // if null ownership group, then check for admin approval + if( this.repository.getBlockRepository().getBlockchainHeight() >= BlockChain.getInstance().getNullGroupMembershipHeight() ) { + String groupOwner = this.repository.getGroupRepository().getOwner(groupId); + boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS); + + // Require approval if transaction relates to a group owned by the null account + if (groupOwnedByNullAccount && !this.needsGroupApproval()) + return ValidationResult.GROUP_APPROVAL_REQUIRED; + } + return ValidationResult.OK; } diff --git a/src/main/java/org/qortal/transaction/GroupBanTransaction.java b/src/main/java/org/qortal/transaction/GroupBanTransaction.java index 1716d206..143a66fb 100644 --- a/src/main/java/org/qortal/transaction/GroupBanTransaction.java +++ b/src/main/java/org/qortal/transaction/GroupBanTransaction.java @@ -2,6 +2,7 @@ package org.qortal.transaction; import org.qortal.account.Account; import org.qortal.asset.Asset; +import org.qortal.block.BlockChain; import org.qortal.crypto.Crypto; import org.qortal.data.group.GroupData; import org.qortal.data.transaction.GroupBanTransactionData; @@ -12,6 +13,7 @@ import org.qortal.repository.Repository; import java.util.Collections; import java.util.List; +import java.util.Objects; public class GroupBanTransaction extends Transaction { @@ -70,9 +72,25 @@ public class GroupBanTransaction extends Transaction { if (!this.repository.getGroupRepository().adminExists(groupId, admin.getAddress())) return ValidationResult.NOT_GROUP_ADMIN; - // Can't ban if not group's current owner - if (!admin.getAddress().equals(groupData.getOwner())) - return ValidationResult.INVALID_GROUP_OWNER; + if( this.repository.getBlockRepository().getBlockchainHeight() < BlockChain.getInstance().getNullGroupMembershipHeight() ) { + // Can't ban if not group's current owner + if (!admin.getAddress().equals(groupData.getOwner())) + return ValidationResult.INVALID_GROUP_OWNER; + } + // if( this.repository.getBlockRepository().getBlockchainHeight() >= BlockChain.getInstance().getNullGroupMembershipHeight() ) + else { + String groupOwner = this.repository.getGroupRepository().getOwner(groupId); + boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS); + + // if null ownership group, then check for admin approval + if(groupOwnedByNullAccount ) { + // Require approval if transaction relates to a group owned by the null account + if (!this.needsGroupApproval()) + return ValidationResult.GROUP_APPROVAL_REQUIRED; + } + else if (!admin.getAddress().equals(groupData.getOwner())) + return ValidationResult.INVALID_GROUP_OWNER; + } Account offender = getOffender(); diff --git a/src/main/java/org/qortal/transaction/GroupInviteTransaction.java b/src/main/java/org/qortal/transaction/GroupInviteTransaction.java index e58d1b9c..96179d1b 100644 --- a/src/main/java/org/qortal/transaction/GroupInviteTransaction.java +++ b/src/main/java/org/qortal/transaction/GroupInviteTransaction.java @@ -2,6 +2,7 @@ package org.qortal.transaction; import org.qortal.account.Account; import org.qortal.asset.Asset; +import org.qortal.block.BlockChain; import org.qortal.crypto.Crypto; import org.qortal.data.transaction.GroupInviteTransactionData; import org.qortal.data.transaction.TransactionData; @@ -11,6 +12,7 @@ import org.qortal.repository.Repository; import java.util.Collections; import java.util.List; +import java.util.Objects; public class GroupInviteTransaction extends Transaction { @@ -85,6 +87,16 @@ public class GroupInviteTransaction extends Transaction { if (admin.getConfirmedBalance(Asset.QORT) < this.groupInviteTransactionData.getFee()) return ValidationResult.NO_BALANCE; + // if null ownership group, then check for admin approval + if( this.repository.getBlockRepository().getBlockchainHeight() >= BlockChain.getInstance().getNullGroupMembershipHeight() ) { + String groupOwner = this.repository.getGroupRepository().getOwner(groupId); + boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS); + + // Require approval if transaction relates to a group owned by the null account + if (groupOwnedByNullAccount && !this.needsGroupApproval()) + return ValidationResult.GROUP_APPROVAL_REQUIRED; + } + return ValidationResult.OK; } diff --git a/src/main/java/org/qortal/transaction/GroupKickTransaction.java b/src/main/java/org/qortal/transaction/GroupKickTransaction.java index 3c426039..e13114fc 100644 --- a/src/main/java/org/qortal/transaction/GroupKickTransaction.java +++ b/src/main/java/org/qortal/transaction/GroupKickTransaction.java @@ -3,6 +3,7 @@ package org.qortal.transaction; import org.qortal.account.Account; import org.qortal.account.PublicKeyAccount; import org.qortal.asset.Asset; +import org.qortal.block.BlockChain; import org.qortal.crypto.Crypto; import org.qortal.data.group.GroupData; import org.qortal.data.transaction.GroupKickTransactionData; @@ -14,6 +15,7 @@ import org.qortal.repository.Repository; import java.util.Collections; import java.util.List; +import java.util.Objects; public class GroupKickTransaction extends Transaction { @@ -82,9 +84,26 @@ public class GroupKickTransaction extends Transaction { if (!admin.getAddress().equals(groupData.getOwner()) && groupRepository.adminExists(groupId, member.getAddress())) return ValidationResult.INVALID_GROUP_OWNER; - // Can't kick if not group's current owner - if (!admin.getAddress().equals(groupData.getOwner())) - return ValidationResult.INVALID_GROUP_OWNER; + if( this.repository.getBlockRepository().getBlockchainHeight() < BlockChain.getInstance().getNullGroupMembershipHeight() ) { + // Can't kick if not group's current owner + if (!admin.getAddress().equals(groupData.getOwner())) + return ValidationResult.INVALID_GROUP_OWNER; + } + // if( this.repository.getBlockRepository().getBlockchainHeight() >= BlockChain.getInstance().getNullGroupMembershipHeight() ) + else { + String groupOwner = this.repository.getGroupRepository().getOwner(groupId); + boolean groupOwnedByNullAccount = Objects.equals(groupOwner, Group.NULL_OWNER_ADDRESS); + + // if null ownership group, then check for admin approval + if(groupOwnedByNullAccount ) { + // Require approval if transaction relates to a group owned by the null account + if (!this.needsGroupApproval()) + return ValidationResult.GROUP_APPROVAL_REQUIRED; + } + // Can't kick if not group's current owner + else if (!admin.getAddress().equals(groupData.getOwner())) + return ValidationResult.INVALID_GROUP_OWNER; + } // Check creator has enough funds if (admin.getConfirmedBalance(Asset.QORT) < this.groupKickTransactionData.getFee()) diff --git a/src/main/java/org/qortal/transaction/Transaction.java b/src/main/java/org/qortal/transaction/Transaction.java index eb774252..f993194a 100644 --- a/src/main/java/org/qortal/transaction/Transaction.java +++ b/src/main/java/org/qortal/transaction/Transaction.java @@ -65,11 +65,11 @@ public abstract class Transaction { UPDATE_GROUP(23, true), ADD_GROUP_ADMIN(24, true), REMOVE_GROUP_ADMIN(25, true), - GROUP_BAN(26, false), - CANCEL_GROUP_BAN(27, false), - GROUP_KICK(28, false), - GROUP_INVITE(29, false), - CANCEL_GROUP_INVITE(30, false), + GROUP_BAN(26, true), + CANCEL_GROUP_BAN(27, true), + GROUP_KICK(28, true), + GROUP_INVITE(29, true), + CANCEL_GROUP_INVITE(30, true), JOIN_GROUP(31, false), LEAVE_GROUP(32, false), GROUP_APPROVAL(33, false), diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index 70622061..ec27efd4 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -115,7 +115,8 @@ "groupMemberCheckHeight": 1902700, "fixBatchRewardHeight": 1945900, "adminsReplaceFoundersHeight": 9999999, - "onlineValidationFailSafeHeight": 9999999 + "onlineValidationFailSafeHeight": 9999999, + "nullGroupMembershipHeight": 9999999 }, "checkpoints": [ { "height": 1136300, "signature": "3BbwawEF2uN8Ni5ofpJXkukoU8ctAPxYoFB7whq9pKfBnjfZcpfEJT4R95NvBDoTP8WDyWvsUvbfHbcr9qSZuYpSKZjUQTvdFf6eqznHGEwhZApWfvXu6zjGCxYCp65F4jsVYYJjkzbjmkCg5WAwN5voudngA23kMK6PpTNygapCzXt" } diff --git a/src/test/java/org/qortal/test/group/DevGroupAdminTests.java b/src/test/java/org/qortal/test/group/DevGroupAdminTests.java index 41352323..14f2b87c 100644 --- a/src/test/java/org/qortal/test/group/DevGroupAdminTests.java +++ b/src/test/java/org/qortal/test/group/DevGroupAdminTests.java @@ -4,7 +4,10 @@ import org.junit.After; 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.data.transaction.*; +import org.qortal.group.Group; import org.qortal.repository.DataException; import org.qortal.repository.Repository; import org.qortal.repository.RepositoryManager; @@ -16,6 +19,8 @@ import org.qortal.test.common.transaction.TestTransaction; import org.qortal.transaction.Transaction; import org.qortal.transaction.Transaction.ValidationResult; +import java.util.List; + import static org.junit.Assert.*; /** @@ -40,8 +45,14 @@ import static org.junit.Assert.*; */ public class DevGroupAdminTests extends Common { + public static final int NULL_GROUP_MEMBERSHIP_HEIGHT = BlockChain.getInstance().getNullGroupMembershipHeight(); private static final int DEV_GROUP_ID = 1; + public static final String ALICE = "alice"; + public static final String BOB = "bob"; + public static final String CHLOE = "chloe"; + public static final String DILBERT = "dilbert"; + @Before public void beforeTest() throws DataException { Common.useDefaultSettings(); @@ -55,8 +66,8 @@ public class DevGroupAdminTests extends Common { @Test public void testGroupKickMember() throws DataException { try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); - PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE); + PrivateKeyAccount bob = Common.getTestAccount(repository, BOB); // Dev group int groupId = DEV_GROUP_ID; @@ -80,16 +91,10 @@ public class DevGroupAdminTests extends Common { // Attempt to kick Bob result = groupKick(repository, alice, groupId, bob.getAddress()); - // Should be OK - assertEquals(ValidationResult.OK, result); + // Should not be OK, cannot kick member out of null owned group + assertNotSame(ValidationResult.OK, result); - // Confirm Bob no longer a member - assertFalse(isMember(repository, bob.getAddress(), groupId)); - - // Orphan last block - BlockUtils.orphanLastBlock(repository); - - // Confirm Bob now a member + // Confirm Bob remains a member assertTrue(isMember(repository, bob.getAddress(), groupId)); } } @@ -97,8 +102,8 @@ public class DevGroupAdminTests extends Common { @Test public void testGroupKickAdmin() throws DataException { try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); - PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE); + PrivateKeyAccount bob = Common.getTestAccount(repository, BOB); // Dev group int groupId = DEV_GROUP_ID; @@ -123,7 +128,7 @@ public class DevGroupAdminTests extends Common { assertEquals("incorrect transaction approval status", Transaction.ApprovalStatus.PENDING, approvalStatus); // Have Alice approve Bob's approval-needed transaction - GroupUtils.approveTransaction(repository, "alice", addGroupAdminTransactionData.getSignature(), true); + GroupUtils.approveTransaction(repository, ALICE, addGroupAdminTransactionData.getSignature(), true); // Mint a block so that the transaction becomes approved BlockUtils.mintBlock(repository); @@ -167,8 +172,8 @@ public class DevGroupAdminTests extends Common { @Test public void testGroupBanMember() throws DataException { try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); - PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE); + PrivateKeyAccount bob = Common.getTestAccount(repository, BOB); // Dev group int groupId = DEV_GROUP_ID; @@ -183,18 +188,13 @@ public class DevGroupAdminTests extends Common { // Attempt to ban Bob result = groupBan(repository, alice, groupId, bob.getAddress()); - // Should be OK - assertEquals(ValidationResult.OK, result); - - // Bob attempts to rejoin - result = joinGroup(repository, bob, groupId); - // Should NOT be OK + // Should not be OK, cannot ban someone from a null owned group assertNotSame(ValidationResult.OK, result); - // Orphan last block (Bob ban) - BlockUtils.orphanLastBlock(repository); - // Delete unconfirmed group-ban transaction - TransactionUtils.deleteUnconfirmedTransactions(repository); + // Bob attempts to join + result = joinGroup(repository, bob, groupId); + // Should be OK, but won't actually get him in the group + assertEquals(ValidationResult.OK, result); // Confirm Bob is not a member assertFalse(isMember(repository, bob.getAddress(), groupId)); @@ -204,65 +204,38 @@ public class DevGroupAdminTests extends Common { // Bob to join result = joinGroup(repository, bob, groupId); - // Should be OK - assertEquals(ValidationResult.OK, result); + // Should not be OK, bob should already be a member, he joined before the invite and + // the invite served as an approval + assertEquals(ValidationResult.ALREADY_GROUP_MEMBER, result); - // Confirm Bob now a member + // Confirm Bob now a member, now that he got an invite assertTrue(isMember(repository, bob.getAddress(), groupId)); // Attempt to ban Bob result = groupBan(repository, alice, groupId, bob.getAddress()); - // Should be OK - assertEquals(ValidationResult.OK, result); + // Should not be OK, because you can ban a member of a null owned group + assertNotSame(ValidationResult.OK, result); - // Confirm Bob no longer a member - assertFalse(isMember(repository, bob.getAddress(), groupId)); + // Confirm Bob is still a member + assertTrue(isMember(repository, bob.getAddress(), groupId)); // Bob attempts to rejoin result = joinGroup(repository, bob, groupId); - // Should NOT be OK + // Should NOT be OK, because he is already a member assertNotSame(ValidationResult.OK, result); // Cancel Bob's ban result = cancelGroupBan(repository, alice, groupId, bob.getAddress()); - // Should be OK - assertEquals(ValidationResult.OK, result); - - // Bob attempts to rejoin - result = joinGroup(repository, bob, groupId); - // Should be OK - assertEquals(ValidationResult.OK, result); - - // Orphan last block (Bob join) - BlockUtils.orphanLastBlock(repository); - // Delete unconfirmed join-group transaction - TransactionUtils.deleteUnconfirmedTransactions(repository); - - // Orphan last block (Cancel Bob ban) - BlockUtils.orphanLastBlock(repository); - // Delete unconfirmed cancel-ban transaction - TransactionUtils.deleteUnconfirmedTransactions(repository); - - // Bob attempts to rejoin - result = joinGroup(repository, bob, groupId); - // Should NOT be OK + // Should not be OK, because there was no ban to begin with assertNotSame(ValidationResult.OK, result); - - // Orphan last block (Bob ban) - BlockUtils.orphanLastBlock(repository); - // Delete unconfirmed group-ban transaction - TransactionUtils.deleteUnconfirmedTransactions(repository); - - // Confirm Bob now a member - assertTrue(isMember(repository, bob.getAddress(), groupId)); } } @Test public void testGroupBanAdmin() throws DataException { try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount alice = Common.getTestAccount(repository, "alice"); - PrivateKeyAccount bob = Common.getTestAccount(repository, "bob"); + PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE); + PrivateKeyAccount bob = Common.getTestAccount(repository, BOB); // Dev group int groupId = DEV_GROUP_ID; @@ -286,7 +259,7 @@ public class DevGroupAdminTests extends Common { assertEquals("incorrect transaction approval status", Transaction.ApprovalStatus.PENDING, approvalStatus); // Have Alice approve Bob's approval-needed transaction - GroupUtils.approveTransaction(repository, "alice", addGroupAdminTransactionData.getSignature(), true); + GroupUtils.approveTransaction(repository, ALICE, addGroupAdminTransactionData.getSignature(), true); // Mint a block so that the transaction becomes approved BlockUtils.mintBlock(repository); @@ -321,6 +294,302 @@ public class DevGroupAdminTests extends Common { } } + @Test + public void testAddAdmin2of3() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + + // establish accounts + PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE); + PrivateKeyAccount bob = Common.getTestAccount(repository, BOB); + PrivateKeyAccount chloe = Common.getTestAccount(repository, CHLOE); + PrivateKeyAccount dilbert = Common.getTestAccount(repository, DILBERT); + + // assert admin statuses + assertEquals(2, repository.getGroupRepository().countGroupAdmins(DEV_GROUP_ID).intValue()); + assertTrue(isAdmin(repository, Group.NULL_OWNER_ADDRESS, DEV_GROUP_ID)); + assertTrue(isAdmin(repository, alice.getAddress(), DEV_GROUP_ID)); + assertFalse(isAdmin(repository, bob.getAddress(), DEV_GROUP_ID)); + assertFalse(isAdmin(repository, chloe.getAddress(), DEV_GROUP_ID)); + assertFalse(isAdmin(repository, dilbert.getAddress(), DEV_GROUP_ID)); + + // confirm Bob is not a member + assertFalse(isMember(repository, bob.getAddress(), DEV_GROUP_ID)); + + // alice invites bob + ValidationResult result = groupInvite(repository, alice, DEV_GROUP_ID, bob.getAddress(), 3600); + assertSame(ValidationResult.OK, result); + + // bob joins + joinGroup(repository, bob, DEV_GROUP_ID); + + // confirm Bob is a member now, but still not an admin + assertTrue(isMember(repository, bob.getAddress(), DEV_GROUP_ID)); + assertFalse(isAdmin(repository, bob.getAddress(), DEV_GROUP_ID)); + + // bob creates transaction to add himself as an admin + TransactionData addGroupAdminTransactionData1 = addGroupAdmin(repository, bob, DEV_GROUP_ID, bob.getAddress()); + + // bob creates add admin transaction for himself, alice signs which is 50% approval while 40% is needed + signForGroupApproval(repository, addGroupAdminTransactionData1, List.of(alice)); + + // assert 3 admins in group and bob is an admin now + assertEquals(3, repository.getGroupRepository().countGroupAdmins(DEV_GROUP_ID).intValue() ); + assertTrue(isAdmin(repository, bob.getAddress(), DEV_GROUP_ID)); + + // bob invites chloe + result = groupInvite(repository, bob, DEV_GROUP_ID, chloe.getAddress(), 3600); + assertSame(ValidationResult.OK, result); + + // chloe joins + joinGroup(repository, chloe, DEV_GROUP_ID); + + // confirm Chloe is a member now, but still not an admin + assertTrue(isMember(repository, chloe.getAddress(), DEV_GROUP_ID)); + assertFalse(isAdmin(repository, chloe.getAddress(), DEV_GROUP_ID)); + + // chloe creates transaction to add herself as an admin + TransactionData addChloeAsGroupAdmin = addGroupAdmin(repository, chloe, DEV_GROUP_ID, chloe.getAddress()); + + // no one has signed, so it should be pending + Transaction.ApprovalStatus addChloeAsGroupAdminStatus1 = GroupUtils.getApprovalStatus(repository, addChloeAsGroupAdmin.getSignature()); + assertEquals( Transaction.ApprovalStatus.PENDING, addChloeAsGroupAdminStatus1); + + // signer 1 + Transaction.ApprovalStatus addChloeAsGroupAdminStatus2 = signForGroupApproval(repository, addChloeAsGroupAdmin, List.of(alice)); + + // 1 out of 3 has signed, so it should be pending, because it is less than 40% + assertEquals( Transaction.ApprovalStatus.PENDING, addChloeAsGroupAdminStatus2); + + // signer 2 + Transaction.ApprovalStatus addChloeAsGroupAdminStatus3 = signForGroupApproval(repository, addChloeAsGroupAdmin, List.of(bob)); + + // 2 out of 3 has signed, so it should be approved, because it is more than 40% + assertEquals( Transaction.ApprovalStatus.APPROVED, addChloeAsGroupAdminStatus3); + } + } + + @Test + public void testNullOwnershipMembership() throws DataException{ + try (final Repository repository = RepositoryManager.getRepository()) { + + Block block = BlockUtils.mintBlocks(repository, NULL_GROUP_MEMBERSHIP_HEIGHT); + assertEquals(NULL_GROUP_MEMBERSHIP_HEIGHT + 1, block.getBlockData().getHeight().intValue()); + + // establish accounts + PrivateKeyAccount alice = Common.getTestAccount(repository, ALICE); + PrivateKeyAccount bob = Common.getTestAccount(repository, BOB); + PrivateKeyAccount chloe = Common.getTestAccount(repository, CHLOE); + PrivateKeyAccount dilbert = Common.getTestAccount(repository, DILBERT); + + // assert admin statuses + assertEquals(2, repository.getGroupRepository().countGroupAdmins(DEV_GROUP_ID).intValue()); + assertTrue(isAdmin(repository, Group.NULL_OWNER_ADDRESS, DEV_GROUP_ID)); + assertTrue(isAdmin(repository, alice.getAddress(), DEV_GROUP_ID)); + assertFalse(isAdmin(repository, bob.getAddress(), DEV_GROUP_ID)); + assertFalse(isAdmin(repository, chloe.getAddress(), DEV_GROUP_ID)); + assertFalse(isAdmin(repository, dilbert.getAddress(), DEV_GROUP_ID)); + + // confirm Bob is not a member + assertFalse(isMember(repository, bob.getAddress(), DEV_GROUP_ID)); + + // alice invites bob, alice signs which is 50% approval while 40% is needed + TransactionData createInviteTransactionData = createGroupInviteForGroupApproval(repository, alice, DEV_GROUP_ID, bob.getAddress(), 3600); + Transaction.ApprovalStatus bobsInviteStatus = signForGroupApproval(repository, createInviteTransactionData, List.of(alice)); + + // assert approval + assertEquals(Transaction.ApprovalStatus.APPROVED, bobsInviteStatus); + + // bob joins + joinGroup(repository, bob, DEV_GROUP_ID); + + // confirm Bob is a member now, but still not an admin + assertTrue(isMember(repository, bob.getAddress(), DEV_GROUP_ID)); + assertFalse(isAdmin(repository, bob.getAddress(), DEV_GROUP_ID)); + + // bob creates transaction to add himself as an admin + TransactionData addGroupAdminTransactionData1 = addGroupAdmin(repository, bob, DEV_GROUP_ID, bob.getAddress()); + + // bob creates add admin transaction for himself, alice signs which is 50% approval while 40% is needed + signForGroupApproval(repository, addGroupAdminTransactionData1, List.of(alice)); + + // assert 3 admins in group and bob is an admin now + assertEquals(3, repository.getGroupRepository().countGroupAdmins(DEV_GROUP_ID).intValue()); + assertTrue(isAdmin(repository, bob.getAddress(), DEV_GROUP_ID)); + + // bob invites chloe, bob signs which is 33% approval while 40% is needed + TransactionData chloeInvite = createGroupInviteForGroupApproval(repository, bob, DEV_GROUP_ID, chloe.getAddress(), 3600); + Transaction.ApprovalStatus chloeInviteStatus = signForGroupApproval(repository, chloeInvite, List.of(bob)); + + // assert pending + assertEquals(Transaction.ApprovalStatus.PENDING, chloeInviteStatus); + + // alice signs which is 66% approval while 40% is needed + chloeInviteStatus = signForGroupApproval(repository, chloeInvite, List.of(alice)); + + // assert approval + assertEquals(Transaction.ApprovalStatus.APPROVED, chloeInviteStatus); + + // chloe joins + joinGroup(repository, chloe, DEV_GROUP_ID); + + // assert chloe is in the group + assertTrue(isMember(repository, bob.getAddress(), DEV_GROUP_ID)); + + // alice kicks chloe, alice signs which is 33% approval while 40% is needed + TransactionData chloeKick = createGroupKickForGroupApproval(repository, alice, DEV_GROUP_ID, chloe.getAddress(),"testing chloe kick"); + Transaction.ApprovalStatus chloeKickStatus = signForGroupApproval(repository, chloeKick, List.of(alice)); + + // assert pending + assertEquals(Transaction.ApprovalStatus.PENDING, chloeKickStatus); + + // assert chloe is still in the group + assertTrue(isMember(repository, bob.getAddress(), DEV_GROUP_ID)); + + // bob signs which is 66% approval while 40% is needed + chloeKickStatus = signForGroupApproval(repository, chloeKick, List.of(bob)); + + // assert approval + assertEquals(Transaction.ApprovalStatus.APPROVED, chloeKickStatus); + + // assert chloe is not in the group + assertFalse(isMember(repository, chloe.getAddress(), DEV_GROUP_ID)); + + // bob invites chloe, alice and bob signs which is 66% approval while 40% is needed + TransactionData chloeInviteAgain = createGroupInviteForGroupApproval(repository, bob, DEV_GROUP_ID, chloe.getAddress(), 3600); + Transaction.ApprovalStatus chloeInviteAgainStatus = signForGroupApproval(repository, chloeInviteAgain, List.of(alice, bob)); + + // assert approved + assertEquals(Transaction.ApprovalStatus.APPROVED, chloeInviteAgainStatus); + + // chloe joins again + joinGroup(repository, chloe, DEV_GROUP_ID); + + // assert chloe is in the group + assertTrue(isMember(repository, bob.getAddress(), DEV_GROUP_ID)); + + // alice bans chloe, alice signs which is 33% approval while 40% is needed + TransactionData chloeBan = createGroupBanForGroupApproval(repository, alice, DEV_GROUP_ID, chloe.getAddress(), "testing group ban", 3600); + Transaction.ApprovalStatus chloeBanStatus1 = signForGroupApproval(repository, chloeBan, List.of(alice)); + + // assert pending + assertEquals(Transaction.ApprovalStatus.PENDING, chloeBanStatus1); + + // bob signs which 66% approval while 40% is needed + Transaction.ApprovalStatus chloeBanStatus2 = signForGroupApproval(repository, chloeBan, List.of(bob)); + + // assert approved + assertEquals(Transaction.ApprovalStatus.APPROVED, chloeBanStatus2); + + // assert chloe is not in the group + assertFalse(isMember(repository, chloe.getAddress(), DEV_GROUP_ID)); + + // bob invites chloe, alice and bob signs which is 66% approval while 40% is needed + ValidationResult chloeInviteValidation = signAndImportGroupInvite(repository, bob, DEV_GROUP_ID, chloe.getAddress(), 3600); + + // assert banned status on invite attempt + assertEquals(ValidationResult.BANNED_FROM_GROUP, chloeInviteValidation); + + // bob cancel ban on chloe, bob signs which is 33% approval while 40% is needed + TransactionData chloeCancelBan = createCancelGroupBanForGroupApproval( repository, bob, DEV_GROUP_ID, chloe.getAddress()); + Transaction.ApprovalStatus chloeCancelBanStatus1 = signForGroupApproval(repository, chloeCancelBan, List.of(bob)); + + // assert pending + assertEquals(Transaction.ApprovalStatus.PENDING, chloeCancelBanStatus1); + + // alice signs which is 66% approval while 40% is needed + Transaction.ApprovalStatus chloeCancelBanStatus2 = signForGroupApproval(repository, chloeCancelBan, List.of(alice)); + + // assert approved + assertEquals(Transaction.ApprovalStatus.APPROVED, chloeCancelBanStatus2); + + // bob invites chloe, alice and bob signs which is 66% approval while 40% is needed + TransactionData chloeInvite4 = createGroupInviteForGroupApproval(repository, bob, DEV_GROUP_ID, chloe.getAddress(), 3600); + Transaction.ApprovalStatus chloeInvite4Status = signForGroupApproval(repository, chloeInvite4, List.of(alice, bob)); + + // assert approved + assertEquals(Transaction.ApprovalStatus.APPROVED, chloeInvite4Status); + + // chloe joins again + joinGroup(repository, chloe, DEV_GROUP_ID); + + // assert chloe is in the group + assertTrue(isMember(repository, chloe.getAddress(), DEV_GROUP_ID)); + + // bob invites dilbert, alice and bob signs which is 66% approval while 40% is needed + TransactionData dilbertInvite1 = createGroupInviteForGroupApproval(repository, bob, DEV_GROUP_ID, dilbert.getAddress(), 3600); + Transaction.ApprovalStatus dibertInviteStatus1 = signForGroupApproval(repository, dilbertInvite1, List.of(alice, bob)); + + // assert approved + assertEquals(Transaction.ApprovalStatus.APPROVED, dibertInviteStatus1); + + // alice cancels dilbert's invite, alice signs which is 33% approval while 40% is needed + TransactionData cancelDilbertInvite = createCancelInviteForGroupApproval(repository, alice, DEV_GROUP_ID, dilbert.getAddress()); + Transaction.ApprovalStatus cancelDilbertInviteStatus1 = signForGroupApproval(repository, cancelDilbertInvite, List.of(alice)); + + // assert pending + assertEquals(Transaction.ApprovalStatus.PENDING, cancelDilbertInviteStatus1); + + // dilbert joins before the group approves cancellation + joinGroup(repository, dilbert, DEV_GROUP_ID); + + // assert dilbert is in the group + assertTrue(isMember(repository, dilbert.getAddress(), DEV_GROUP_ID)); + + // alice kicks out dilbert, alice and bob sign which is 66% approval while 40% is needed + TransactionData kickDilbert = createGroupKickForGroupApproval(repository, alice, DEV_GROUP_ID, dilbert.getAddress(), "he is sneaky"); + Transaction.ApprovalStatus kickDilbertStatus = signForGroupApproval(repository, kickDilbert, List.of(alice, bob)); + + // assert approved + assertEquals(Transaction.ApprovalStatus.APPROVED, kickDilbertStatus); + + // assert dilbert is out of the group + assertFalse(isMember(repository, dilbert.getAddress(), DEV_GROUP_ID)); + + // bob invites dilbert again, alice and bob signs which is 66% approval while 40% is needed + TransactionData dilbertInvite2 = createGroupInviteForGroupApproval(repository, bob, DEV_GROUP_ID, dilbert.getAddress(), 3600); + Transaction.ApprovalStatus dibertInviteStatus2 = signForGroupApproval(repository, dilbertInvite2, List.of(alice, bob)); + + // assert approved + assertEquals(Transaction.ApprovalStatus.APPROVED, dibertInviteStatus2); + + // alice cancels dilbert's invite, alice and bob signs which is 66% approval while 40% is needed + TransactionData cancelDilbertInvite2 = createCancelInviteForGroupApproval(repository, alice, DEV_GROUP_ID, dilbert.getAddress()); + Transaction.ApprovalStatus cancelDilbertInviteStatus2 = signForGroupApproval(repository, cancelDilbertInvite2, List.of(alice, bob)); + + // assert approved + assertEquals(Transaction.ApprovalStatus.APPROVED, cancelDilbertInviteStatus2); + + // dilbert tries to join after the group approves cancellation + joinGroup(repository, dilbert, DEV_GROUP_ID); + + // assert dilbert is not in the group + assertFalse(isMember(repository, dilbert.getAddress(), DEV_GROUP_ID)); + } + } + + private Transaction.ApprovalStatus signForGroupApproval(Repository repository, TransactionData data, List signers) throws DataException { + + for (PrivateKeyAccount signer : signers) { + signTransactionDataForGroupApproval(repository, signer, data); + } + + BlockUtils.mintBlocks(repository, 2); + + // return approval status + return GroupUtils.getApprovalStatus(repository, data.getSignature()); + } + + private static void signTransactionDataForGroupApproval(Repository repository, PrivateKeyAccount signer, TransactionData transactionData) throws DataException { + byte[] reference = signer.getLastReference(); + long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1; + + BaseTransactionData baseTransactionData + = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, signer.getPublicKey(), GroupUtils.fee, null); + TransactionData groupApprovalTransactionData + = new GroupApprovalTransactionData(baseTransactionData, transactionData.getSignature(), true); + + TransactionUtils.signAndImportValid(repository, groupApprovalTransactionData, signer); + } private ValidationResult joinGroup(Repository repository, PrivateKeyAccount joiner, int groupId) throws DataException { JoinGroupTransactionData transactionData = new JoinGroupTransactionData(TestTransaction.generateBase(joiner), groupId); @@ -332,9 +601,31 @@ public class DevGroupAdminTests extends Common { return result; } - private void groupInvite(Repository repository, PrivateKeyAccount admin, int groupId, String invitee, int timeToLive) throws DataException { + private ValidationResult groupInvite(Repository repository, PrivateKeyAccount admin, int groupId, String invitee, int timeToLive) throws DataException { GroupInviteTransactionData transactionData = new GroupInviteTransactionData(TestTransaction.generateBase(admin), groupId, invitee, timeToLive); + ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin); + + if (result == ValidationResult.OK) + BlockUtils.mintBlock(repository); + + return result; + } + + private TransactionData createGroupInviteForGroupApproval(Repository repository, PrivateKeyAccount admin, int groupId, String invitee, int timeToLive) throws DataException { + GroupInviteTransactionData transactionData = new GroupInviteTransactionData(TestTransaction.generateBase(admin, groupId), groupId, invitee, timeToLive); TransactionUtils.signAndMint(repository, transactionData, admin); + return transactionData; + } + + private TransactionData createCancelInviteForGroupApproval(Repository repository, PrivateKeyAccount admin, int groupId, String inviteeToCancel) throws DataException { + CancelGroupInviteTransactionData transactionData = new CancelGroupInviteTransactionData(TestTransaction.generateBase(admin, groupId), groupId, inviteeToCancel); + TransactionUtils.signAndMint(repository, transactionData, admin); + return transactionData; + } + + private ValidationResult signAndImportGroupInvite(Repository repository, PrivateKeyAccount admin, int groupId, String invitee, int timeToLive) throws DataException { + GroupInviteTransactionData transactionData = new GroupInviteTransactionData(TestTransaction.generateBase(admin, groupId), groupId, invitee, timeToLive); + return TransactionUtils.signAndImport(repository, transactionData, admin); } private ValidationResult groupKick(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException { @@ -347,6 +638,13 @@ public class DevGroupAdminTests extends Common { return result; } + private TransactionData createGroupKickForGroupApproval(Repository repository, PrivateKeyAccount admin, int groupId, String kicked, String reason) throws DataException { + GroupKickTransactionData transactionData = new GroupKickTransactionData(TestTransaction.generateBase(admin, groupId), groupId, kicked, reason); + TransactionUtils.signAndMint(repository, transactionData, admin); + + return transactionData; + } + private ValidationResult groupBan(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException { GroupBanTransactionData transactionData = new GroupBanTransactionData(TestTransaction.generateBase(admin), groupId, member, "testing", 0); ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin); @@ -357,6 +655,13 @@ public class DevGroupAdminTests extends Common { return result; } + private TransactionData createGroupBanForGroupApproval(Repository repository, PrivateKeyAccount admin, int groupId, String banned, String reason, int timeToLive) throws DataException { + GroupBanTransactionData transactionData = new GroupBanTransactionData(TestTransaction.generateBase(admin, groupId), groupId, banned, reason, timeToLive); + TransactionUtils.signAndMint(repository, transactionData, admin); + + return transactionData; + } + private ValidationResult cancelGroupBan(Repository repository, PrivateKeyAccount admin, int groupId, String member) throws DataException { CancelGroupBanTransactionData transactionData = new CancelGroupBanTransactionData(TestTransaction.generateBase(admin), groupId, member); ValidationResult result = TransactionUtils.signAndImport(repository, transactionData, admin); @@ -367,6 +672,14 @@ public class DevGroupAdminTests extends Common { return result; } + private TransactionData createCancelGroupBanForGroupApproval(Repository repository, PrivateKeyAccount admin, int groupId, String unbanned ) throws DataException { + CancelGroupBanTransactionData transactionData = new CancelGroupBanTransactionData( TestTransaction.generateBase(admin, groupId), groupId, unbanned); + + TransactionUtils.signAndMint(repository, transactionData, admin); + + return transactionData; + } + private TransactionData addGroupAdmin(Repository repository, PrivateKeyAccount owner, int groupId, String member) throws DataException { AddGroupAdminTransactionData transactionData = new AddGroupAdminTransactionData(TestTransaction.generateBase(owner), groupId, member); transactionData.setTxGroupId(groupId); diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index 689b900b..b5666234 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -106,7 +106,8 @@ "removeOnlyMintWithNameHeight": 9999999999999, "fixBatchRewardHeight": 9999999999999, "adminsReplaceFoundersHeight": 9999999999999, - "onlineValidationFailSafeHeight": 9999999999999 + "onlineValidationFailSafeHeight": 9999999999999, + "nullGroupMembershipHeight": 20 }, "genesisInfo": { "version": 4,