From 93dab1a3e3ada98181346774450e548aa2e41512 Mon Sep 17 00:00:00 2001 From: kennycud Date: Tue, 13 May 2025 11:13:55 -0700 Subject: [PATCH] detailed test case for the invite orphan vulnerability patch that was committed in 2/1/25 --- .../qortal/test/group/DevGroupAdminTests.java | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/src/test/java/org/qortal/test/group/DevGroupAdminTests.java b/src/test/java/org/qortal/test/group/DevGroupAdminTests.java index 38ad0c53..925e2f3e 100644 --- a/src/test/java/org/qortal/test/group/DevGroupAdminTests.java +++ b/src/test/java/org/qortal/test/group/DevGroupAdminTests.java @@ -1,6 +1,7 @@ package org.qortal.test.group; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.qortal.account.PrivateKeyAccount; @@ -369,6 +370,105 @@ public class DevGroupAdminTests extends Common { } } + @Test + public void testOrphanSecondInviteApproval() 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 chloeInvite1 = createGroupInviteForGroupApproval(repository, bob, DEV_GROUP_ID, chloe.getAddress(), 3600); + Transaction.ApprovalStatus chloeInvite1Status = signForGroupApproval(repository, chloeInvite1, List.of(bob)); + + // assert invite 1 pending + assertEquals(Transaction.ApprovalStatus.PENDING, chloeInvite1Status); + + // bob invites chloe again, bob signs which is 33% approval while 40% is needed + // since chloe is not a member yet, this invite is valie + TransactionData chloeInvite2 = createGroupInviteForGroupApproval(repository, bob, DEV_GROUP_ID, chloe.getAddress(), 3600); + Transaction.ApprovalStatus chloeInvite2Status = signForGroupApproval(repository, chloeInvite2, List.of(bob)); + + // assert invite 2 is pending + assertEquals(Transaction.ApprovalStatus.PENDING, chloeInvite2Status); + + // alice signs which is 66% approval while 40% is needed + chloeInvite1Status = signForGroupApproval(repository, chloeInvite1, List.of(alice)); + + // assert invite 1 approval + assertEquals(Transaction.ApprovalStatus.APPROVED, chloeInvite1Status); + + // chloe joins + joinGroup(repository, chloe, DEV_GROUP_ID); + + // assert chloe is in the group + assertTrue(isMember(repository, bob.getAddress(), DEV_GROUP_ID)); + + // alice signs invite 2 which is 66% approval while 40% is needed + chloeInvite2Status = signForGroupApproval(repository, chloeInvite2, List.of(alice)); + + // assert invite 2 approval + assertEquals(Transaction.ApprovalStatus.APPROVED, chloeInvite2Status); + + boolean exceptionThrown = false; + + try { + // confront the bug by orphaning the block of the second approval, approval after the join + // prior to the fix, this would raise an exception + BlockUtils.orphanLastBlock(repository); + } catch (DataException e) { + exceptionThrown = true; + } + + Assert.assertFalse(exceptionThrown); + + // assert chloe is still a member + assertTrue(isMember(repository, bob.getAddress(), DEV_GROUP_ID)); + } + } + @Test public void testNullOwnershipMembership() throws DataException{ try (final Repository repository = RepositoryManager.getRepository()) {