diff --git a/src/main/java/org/qora/block/Block.java b/src/main/java/org/qora/block/Block.java index cafa44c8..e5788f0e 100644 --- a/src/main/java/org/qora/block/Block.java +++ b/src/main/java/org/qora/block/Block.java @@ -1145,6 +1145,9 @@ public class Block { for (TransactionData transactionData : approvalExpiringTransactions) { transactionData.setApprovalStatus(ApprovalStatus.EXPIRED); transactionRepository.save(transactionData); + + // Update group-approval decision height for transaction in repository + transactionRepository.updateApprovalHeight(transactionData.getSignature(), this.blockData.getHeight()); } // Search for pending transactions within min/max block delay range @@ -1159,7 +1162,7 @@ public class Block { if (isApproved == null) continue; // approve/reject threshold not yet met - // Update approval height for transaction in repository + // Update group-approval decision height for transaction in repository transactionRepository.updateApprovalHeight(transactionData.getSignature(), this.blockData.getHeight()); if (!isApproved) { @@ -1321,15 +1324,16 @@ public class Block { List transactions = transactionRepository.getApprovalTransactionDecidedAtHeight(this.blockData.getHeight()); for (TransactionData transactionData : transactions) { - // Orphan/un-process transaction + // Orphan/un-process transaction (if approved) Transaction transaction = Transaction.fromData(repository, transactionData); - transaction.orphan(); + if (transactionData.getApprovalStatus() == ApprovalStatus.APPROVED) + transaction.orphan(); // Revert back to PENDING transactionData.setApprovalStatus(ApprovalStatus.PENDING); transactionRepository.save(transactionData); - // Undo approval decision height + // Remove group-approval decision height transactionRepository.updateApprovalHeight(transactionData.getSignature(), null); } } diff --git a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index 39ce7f4a..9c8abb12 100644 --- a/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -679,7 +679,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { String sql = "SELECT signature FROM Transactions " + "JOIN Groups on Groups.group_id = Transactions.tx_group_id " + "WHERE Transactions.approval_status = ? " - + "AND Transactions.block_height >= ? - Groups.min_block_delay"; + + "AND Transactions.block_height < ? - Groups.min_block_delay"; List transactions = new ArrayList(); diff --git a/src/test/java/org/qora/test/common/GroupUtils.java b/src/test/java/org/qora/test/common/GroupUtils.java index ab46d69f..9fff9cd0 100644 --- a/src/test/java/org/qora/test/common/GroupUtils.java +++ b/src/test/java/org/qora/test/common/GroupUtils.java @@ -63,4 +63,8 @@ public class GroupUtils { return repository.getTransactionRepository().fromSignature(signature).getApprovalStatus(); } + public static Integer getApprovalHeight(Repository repository, byte[] signature) throws DataException { + return repository.getTransactionRepository().fromSignature(signature).getApprovalHeight(); + } + } diff --git a/src/test/java/org/qora/test/common/TransactionUtils.java b/src/test/java/org/qora/test/common/TransactionUtils.java index 1debfa24..6378446e 100644 --- a/src/test/java/org/qora/test/common/TransactionUtils.java +++ b/src/test/java/org/qora/test/common/TransactionUtils.java @@ -17,6 +17,7 @@ import org.qora.transaction.Transaction.ValidationResult; public class TransactionUtils { + /** Signs transaction using given account and imports into unconfirmed pile. */ public static void signAsUnconfirmed(Repository repository, TransactionData transactionData, PrivateKeyAccount signingAccount) throws DataException { Transaction transaction = Transaction.fromData(repository, transactionData); transaction.sign(signingAccount); @@ -36,6 +37,7 @@ public class TransactionUtils { transaction.importAsUnconfirmed(); } + /** Signs transaction using given account and forges a new block, using "alice" account. */ public static void signAndForge(Repository repository, TransactionData transactionData, PrivateKeyAccount signingAccount) throws DataException { signAsUnconfirmed(repository, transactionData, signingAccount); diff --git a/src/test/java/org/qora/test/group/GroupApprovalTests.java b/src/test/java/org/qora/test/group/GroupApprovalTests.java index eac1aa97..fc7ead27 100644 --- a/src/test/java/org/qora/test/group/GroupApprovalTests.java +++ b/src/test/java/org/qora/test/group/GroupApprovalTests.java @@ -32,6 +32,9 @@ public class GroupApprovalTests extends Common { private static final BigDecimal amount = BigDecimal.valueOf(5000L).setScale(8); private static final BigDecimal fee = BigDecimal.ONE.setScale(8); + private static final int minBlockDelay = 5; + private static final int maxBlockDelay = 10; + @Before public void beforeTest() throws DataException { @@ -57,12 +60,26 @@ public class GroupApprovalTests extends Common { } } + @Test + /** Check that a transaction type that does need approval, auto-approves if created by group admin */ + public void testAutoApprove() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount aliceAccount = Common.getTestAccount(repository, "alice"); + + int groupId = GroupUtils.createGroup(repository, "alice", "test", true, ApprovalThreshold.ONE, minBlockDelay, maxBlockDelay); + + Transaction transaction = buildIssueAssetTransaction(repository, "alice", groupId); + TransactionUtils.signAndForge(repository, transaction.getTransactionData(), aliceAccount); + + // Confirm transaction doesn't need approval + ApprovalStatus approvalStatus = GroupUtils.getApprovalStatus(repository, transaction.getTransactionData().getSignature()); + assertEquals("incorrect transaction approval status", ApprovalStatus.NOT_REQUIRED, approvalStatus); + } + } + @Test /** Check that a transaction, that requires approval, updates references and fees properly. */ public void testReferencesAndFees() throws DataException { - final int minBlockDelay = 5; - final int maxBlockDelay = 20; - try (final Repository repository = RepositoryManager.getRepository()) { PrivateKeyAccount aliceAccount = Common.getTestAccount(repository, "alice"); @@ -98,8 +115,7 @@ public class GroupApprovalTests extends Common { // Have Bob do a non-approval transaction to change his last-reference Transaction bobPaymentTransaction = buildPaymentTransaction(repository, "bob", "chloe", amount, Group.NO_GROUP); - TransactionUtils.signAsUnconfirmed(repository, bobPaymentTransaction.getTransactionData(), bobAccount); - BlockGenerator.generateTestingBlock(repository, aliceAccount); + TransactionUtils.signAndForge(repository, bobPaymentTransaction.getTransactionData(), bobAccount); byte[] bobPostPaymentReference = bobAccount.getLastReference(); assertFalse("reference should have changed", Arrays.equals(bobPostAssetReference, bobPostPaymentReference)); @@ -156,6 +172,252 @@ public class GroupApprovalTests extends Common { } } + @Test + /** Test generic approval. */ + public void testApproval() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount aliceAccount = Common.getTestAccount(repository, "alice"); + int groupId = GroupUtils.createGroup(repository, "alice", "test", true, ApprovalThreshold.ONE, minBlockDelay, maxBlockDelay); + + PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob"); + GroupUtils.joinGroup(repository, "bob", groupId); + + // Bob's issue-asset transaction needs group-approval + Transaction bobAssetTransaction = buildIssueAssetTransaction(repository, "bob", groupId); + TransactionUtils.signAndForge(repository, bobAssetTransaction.getTransactionData(), bobAccount); + + // Confirm transaction needs approval, and hasn't been approved + ApprovalStatus approvalStatus = GroupUtils.getApprovalStatus(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertEquals("incorrect transaction approval status", ApprovalStatus.PENDING, approvalStatus); + + // Confirm transaction has no group-approval decision height + Integer approvalHeight = GroupUtils.getApprovalHeight(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertNull("group-approval decision height should be null", approvalHeight); + + // Have Alice approve Bob's approval-needed transaction + GroupUtils.approveTransaction(repository, "alice", bobAssetTransaction.getTransactionData().getSignature(), true); + + // Now forge a few blocks so transaction is approved + for (int blockCount = 0; blockCount < minBlockDelay; ++blockCount) + BlockGenerator.generateTestingBlock(repository, aliceAccount); + + // Confirm transaction now approved + approvalStatus = GroupUtils.getApprovalStatus(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertEquals("incorrect transaction approval status", ApprovalStatus.APPROVED, approvalStatus); + + // Confirm transaction now has a group-approval decision height + approvalHeight = GroupUtils.getApprovalHeight(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertNotNull("group-approval decision height should not be null", approvalHeight); + + // Orphan blocks that decided approval + for (int blockCount = 0; blockCount < minBlockDelay; ++blockCount) + BlockUtils.orphanLastBlock(repository); + + // Confirm transaction no longer approved + approvalStatus = GroupUtils.getApprovalStatus(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertEquals("incorrect transaction approval status", ApprovalStatus.PENDING, approvalStatus); + + // Confirm transaction no longer has group-approval decision height + approvalHeight = GroupUtils.getApprovalHeight(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertNull("group-approval decision height should be null", approvalHeight); + + // Orphan block containing Alice's group-approval transaction + BlockUtils.orphanLastBlock(repository); + + // Confirm transaction no longer approved + approvalStatus = GroupUtils.getApprovalStatus(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertEquals("incorrect transaction approval status", ApprovalStatus.PENDING, approvalStatus); + + // Confirm transaction no longer has group-approval decision height + approvalHeight = GroupUtils.getApprovalHeight(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertNull("group-approval decision height should be null", approvalHeight); + } + } + + @Test + /** Test generic rejection. */ + public void testRejection() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount aliceAccount = Common.getTestAccount(repository, "alice"); + int groupId = GroupUtils.createGroup(repository, "alice", "test", true, ApprovalThreshold.ONE, minBlockDelay, maxBlockDelay); + + PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob"); + GroupUtils.joinGroup(repository, "bob", groupId); + + // Bob's issue-asset transaction needs group-approval + Transaction bobAssetTransaction = buildIssueAssetTransaction(repository, "bob", groupId); + TransactionUtils.signAndForge(repository, bobAssetTransaction.getTransactionData(), bobAccount); + + // Confirm transaction needs approval, and hasn't been approved + ApprovalStatus approvalStatus = GroupUtils.getApprovalStatus(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertEquals("incorrect transaction approval status", ApprovalStatus.PENDING, approvalStatus); + + // Confirm transaction has no group-approval decision height + Integer approvalHeight = GroupUtils.getApprovalHeight(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertNull("group-approval decision height should be null", approvalHeight); + + // Have Alice reject Bob's approval-needed transaction + GroupUtils.approveTransaction(repository, "alice", bobAssetTransaction.getTransactionData().getSignature(), false); + + // Now forge a few blocks so transaction is approved + for (int blockCount = 0; blockCount < minBlockDelay; ++blockCount) + BlockGenerator.generateTestingBlock(repository, aliceAccount); + + // Confirm transaction now rejected + approvalStatus = GroupUtils.getApprovalStatus(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertEquals("incorrect transaction approval status", ApprovalStatus.REJECTED, approvalStatus); + + // Confirm transaction now has a group-approval decision height + approvalHeight = GroupUtils.getApprovalHeight(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertNotNull("group-approval decision height should not be null", approvalHeight); + + // Orphan blocks that decided rejection + for (int blockCount = 0; blockCount < minBlockDelay; ++blockCount) + BlockUtils.orphanLastBlock(repository); + + // Confirm transaction no longer rejected + approvalStatus = GroupUtils.getApprovalStatus(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertEquals("incorrect transaction approval status", ApprovalStatus.PENDING, approvalStatus); + + // Confirm transaction no longer has group-approval decision height + approvalHeight = GroupUtils.getApprovalHeight(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertNull("group-approval decision height should be null", approvalHeight); + + // Orphan block containing Alice's group-approval transaction + BlockUtils.orphanLastBlock(repository); + + // Confirm transaction no longer rejected + approvalStatus = GroupUtils.getApprovalStatus(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertEquals("incorrect transaction approval status", ApprovalStatus.PENDING, approvalStatus); + + // Confirm transaction no longer has group-approval decision height + approvalHeight = GroupUtils.getApprovalHeight(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertNull("group-approval decision height should be null", approvalHeight); + } + } + + @Test + /** Test generic expiry. */ + public void testExpiry() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount aliceAccount = Common.getTestAccount(repository, "alice"); + int groupId = GroupUtils.createGroup(repository, "alice", "test", true, ApprovalThreshold.ONE, minBlockDelay, maxBlockDelay); + + PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob"); + GroupUtils.joinGroup(repository, "bob", groupId); + + // Bob's issue-asset transaction needs group-approval + Transaction bobAssetTransaction = buildIssueAssetTransaction(repository, "bob", groupId); + TransactionUtils.signAndForge(repository, bobAssetTransaction.getTransactionData(), bobAccount); + + // Confirm transaction needs approval, and hasn't been approved + ApprovalStatus approvalStatus = GroupUtils.getApprovalStatus(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertEquals("incorrect transaction approval status", ApprovalStatus.PENDING, approvalStatus); + + // Confirm transaction has no group-approval decision height + Integer approvalHeight = GroupUtils.getApprovalHeight(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertNull("group-approval decision height should be null", approvalHeight); + + // Now forge a few blocks so group-approval for transaction expires + for (int blockCount = 0; blockCount <= maxBlockDelay; ++blockCount) + BlockGenerator.generateTestingBlock(repository, aliceAccount); + + // Confirm transaction now expired + approvalStatus = GroupUtils.getApprovalStatus(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertEquals("incorrect transaction approval status", ApprovalStatus.EXPIRED, approvalStatus); + + // Confirm transaction now has a group-approval decision height + approvalHeight = GroupUtils.getApprovalHeight(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertNotNull("group-approval decision height should not be null", approvalHeight); + + // Orphan blocks that decided expiry + for (int blockCount = 0; blockCount <= maxBlockDelay; ++blockCount) + BlockUtils.orphanLastBlock(repository); + + // Confirm transaction no longer expired + approvalStatus = GroupUtils.getApprovalStatus(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertEquals("incorrect transaction approval status", ApprovalStatus.PENDING, approvalStatus); + + // Confirm transaction no longer has group-approval decision height + approvalHeight = GroupUtils.getApprovalHeight(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertNull("group-approval decision height should be null", approvalHeight); + } + } + + @Test + /** Test generic invalid. */ + public void testInvalid() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount aliceAccount = Common.getTestAccount(repository, "alice"); + int groupId = GroupUtils.createGroup(repository, "alice", "test", true, ApprovalThreshold.ONE, minBlockDelay, maxBlockDelay); + + PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob"); + GroupUtils.joinGroup(repository, "bob", groupId); + + // Bob's issue-asset transaction needs group-approval + Transaction bobAssetTransaction = buildIssueAssetTransaction(repository, "bob", groupId); + TransactionUtils.signAndForge(repository, bobAssetTransaction.getTransactionData(), bobAccount); + + // Confirm transaction needs approval, and hasn't been approved + ApprovalStatus approvalStatus = GroupUtils.getApprovalStatus(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertEquals("incorrect transaction approval status", ApprovalStatus.PENDING, approvalStatus); + + // Confirm transaction has no group-approval decision height + Integer approvalHeight = GroupUtils.getApprovalHeight(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertNull("group-approval decision height should be null", approvalHeight); + + // Have Alice approve Bob's approval-needed transaction + GroupUtils.approveTransaction(repository, "alice", bobAssetTransaction.getTransactionData().getSignature(), true); + + // But wait! Alice issues an asset with the same name before Bob's asset is issued! + // This transaction will be auto-approved as Alice is the group owner (and admin) + Transaction aliceAssetTransaction = buildIssueAssetTransaction(repository, "alice", groupId); + TransactionUtils.signAndForge(repository, aliceAssetTransaction.getTransactionData(), aliceAccount); + + // Confirm Alice's transaction auto-approved + approvalStatus = GroupUtils.getApprovalStatus(repository, aliceAssetTransaction.getTransactionData().getSignature()); + assertEquals("incorrect transaction approval status", ApprovalStatus.NOT_REQUIRED, approvalStatus); + + // Now forge a few blocks so transaction is approved + for (int blockCount = 0; blockCount < minBlockDelay; ++blockCount) + BlockGenerator.generateTestingBlock(repository, aliceAccount); + + // Confirm Bob's transaction now invalid + approvalStatus = GroupUtils.getApprovalStatus(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertEquals("incorrect transaction approval status", ApprovalStatus.INVALID, approvalStatus); + + // Confirm transaction now has a group-approval decision height + approvalHeight = GroupUtils.getApprovalHeight(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertNotNull("group-approval decision height should not be null", approvalHeight); + + // Orphan blocks that decided group-approval + for (int blockCount = 0; blockCount < minBlockDelay; ++blockCount) + BlockUtils.orphanLastBlock(repository); + + // Confirm transaction no longer invalid + approvalStatus = GroupUtils.getApprovalStatus(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertEquals("incorrect transaction approval status", ApprovalStatus.PENDING, approvalStatus); + + // Confirm transaction no longer has group-approval decision height + approvalHeight = GroupUtils.getApprovalHeight(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertNull("group-approval decision height should be null", approvalHeight); + + // Orphan block containing Alice's issue-asset transaction + BlockUtils.orphanLastBlock(repository); + + // Orphan block containing Alice's group-approval transaction + BlockUtils.orphanLastBlock(repository); + + // Confirm transaction no longer approved + approvalStatus = GroupUtils.getApprovalStatus(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertEquals("incorrect transaction approval status", ApprovalStatus.PENDING, approvalStatus); + + // Confirm transaction no longer has group-approval decision height + approvalHeight = GroupUtils.getApprovalHeight(repository, bobAssetTransaction.getTransactionData().getSignature()); + assertNull("group-approval decision height should be null", approvalHeight); + } + } + private Transaction buildPaymentTransaction(Repository repository, String sender, String recipient, BigDecimal amount, int txGroupId) throws DataException { PrivateKeyAccount sendingAccount = Common.getTestAccount(repository, sender); PrivateKeyAccount recipientAccount = Common.getTestAccount(repository, recipient);