From c9f226cf88c6e0027368fe4496d5dc1352c7f133 Mon Sep 17 00:00:00 2001
From: catbref <misc-github@talk2dom.com>
Date: Sun, 2 Jun 2019 17:07:07 +0100
Subject: [PATCH] group approval tests and fixes

---
 src/main/java/org/qora/block/Block.java       |   6 +-
 .../transaction/GroupApprovalTransaction.java |  10 +-
 .../org/qora/transaction/Transaction.java     |   2 +
 .../org/qora/test/GroupApprovalTests.java     | 101 ----------
 .../java/org/qora/test/common/BlockUtils.java |  25 +++
 .../java/org/qora/test/common/GroupUtils.java |  66 +++++++
 .../org/qora/test/common/TestAccount.java     |   2 +
 .../org/qora/test/forging/RewardTests.java    |  14 +-
 .../qora/test/group/GroupApprovalTests.java   | 184 ++++++++++++++++++
 9 files changed, 296 insertions(+), 114 deletions(-)
 delete mode 100644 src/test/java/org/qora/test/GroupApprovalTests.java
 create mode 100644 src/test/java/org/qora/test/common/BlockUtils.java
 create mode 100644 src/test/java/org/qora/test/common/GroupUtils.java
 create mode 100644 src/test/java/org/qora/test/group/GroupApprovalTests.java

diff --git a/src/main/java/org/qora/block/Block.java b/src/main/java/org/qora/block/Block.java
index 7194dc69..cafa44c8 100644
--- a/src/main/java/org/qora/block/Block.java
+++ b/src/main/java/org/qora/block/Block.java
@@ -1090,7 +1090,7 @@ public class Block {
 	}
 
 	protected void processBlockRewards() throws DataException {
-		BigDecimal reward = getRewardAtHeight(this.blockData.getHeight());
+		BigDecimal reward = Block.getRewardAtHeight(this.blockData.getHeight());
 
 		// No reward for our height?
 		if (reward == null)
@@ -1335,7 +1335,7 @@ public class Block {
 	}
 
 	protected void orphanBlockRewards() throws DataException {
-		BigDecimal reward = getRewardAtHeight(this.blockData.getHeight());
+		BigDecimal reward = Block.getRewardAtHeight(this.blockData.getHeight());
 
 		// No reward for our height?
 		if (reward == null)
@@ -1397,7 +1397,7 @@ public class Block {
 		atRepository.deleteATStates(this.blockData.getHeight());
 	}
 
-	protected BigDecimal getRewardAtHeight(int ourHeight) {
+	public static BigDecimal getRewardAtHeight(int ourHeight) {
 		List<RewardByHeight> rewardsByHeight = BlockChain.getInstance().getBlockRewardsByHeight();
 
 		// No rewards configured?
diff --git a/src/main/java/org/qora/transaction/GroupApprovalTransaction.java b/src/main/java/org/qora/transaction/GroupApprovalTransaction.java
index e3220d53..59ef97a9 100644
--- a/src/main/java/org/qora/transaction/GroupApprovalTransaction.java
+++ b/src/main/java/org/qora/transaction/GroupApprovalTransaction.java
@@ -68,9 +68,13 @@ public class GroupApprovalTransaction extends Transaction {
 		if (pendingTransactionData == null)
 			return ValidationResult.TRANSACTION_UNKNOWN;
 
-		// Check pending transaction is not already in a block
-		if (this.repository.getTransactionRepository().getHeightFromSignature(groupApprovalTransactionData.getPendingSignature()) != 0)
-			return ValidationResult.TRANSACTION_ALREADY_CONFIRMED;
+		// Check pending transaction is actually needs group approval
+		if (pendingTransactionData.getApprovalStatus() == ApprovalStatus.NOT_REQUIRED)
+			return ValidationResult.GROUP_APPROVAL_NOT_REQUIRED;
+
+		// Check pending transaction is actually pending
+		if (pendingTransactionData.getApprovalStatus() != ApprovalStatus.PENDING)
+			return ValidationResult.GROUP_APPROVAL_DECIDED;
 
 		Account admin = getAdmin();
 
diff --git a/src/main/java/org/qora/transaction/Transaction.java b/src/main/java/org/qora/transaction/Transaction.java
index 8e3f6b93..5962ebd9 100644
--- a/src/main/java/org/qora/transaction/Transaction.java
+++ b/src/main/java/org/qora/transaction/Transaction.java
@@ -230,6 +230,8 @@ public abstract class Transaction {
 		INVALID_PUBLIC_KEY(79),
 		AT_UNKNOWN(80),
 		AT_ALREADY_EXISTS(81),
+		GROUP_APPROVAL_NOT_REQUIRED(82),
+		GROUP_APPROVAL_DECIDED(83),
 		NOT_YET_RELEASED(1000);
 
 		public final int value;
diff --git a/src/test/java/org/qora/test/GroupApprovalTests.java b/src/test/java/org/qora/test/GroupApprovalTests.java
deleted file mode 100644
index 5144d171..00000000
--- a/src/test/java/org/qora/test/GroupApprovalTests.java
+++ /dev/null
@@ -1,101 +0,0 @@
-package org.qora.test;
-
-import org.junit.Test;
-import org.qora.account.PrivateKeyAccount;
-import org.qora.block.BlockChain;
-import org.qora.block.BlockGenerator;
-import org.qora.data.transaction.BaseTransactionData;
-import org.qora.data.transaction.CreateGroupTransactionData;
-import org.qora.data.transaction.PaymentTransactionData;
-import org.qora.data.transaction.TransactionData;
-import org.qora.group.Group;
-import org.qora.group.Group.ApprovalThreshold;
-import org.qora.repository.DataException;
-import org.qora.repository.Repository;
-import org.qora.repository.RepositoryManager;
-import org.qora.test.common.Common;
-import org.qora.transaction.CreateGroupTransaction;
-import org.qora.transaction.PaymentTransaction;
-import org.qora.transaction.Transaction;
-import org.qora.transaction.Transaction.ValidationResult;
-
-import static org.junit.Assert.*;
-
-import java.math.BigDecimal;
-
-public class GroupApprovalTests extends Common {
-
-	/** Check that a tx type that doesn't need approval doesn't accept txGroupId apart from NO_GROUP */
-	@Test
-	public void testNonApprovalTxGroupId() throws DataException {
-		try (final Repository repository = RepositoryManager.getRepository()) {
-			BlockChain.validate();
-
-			TransactionData transactionData = buildPayment(repository, Group.NO_GROUP);
-			Transaction transaction = new PaymentTransaction(repository, transactionData);
-			assertEquals(ValidationResult.OK, transaction.isValidUnconfirmed());
-
-			int groupId = createGroup(repository);
-
-			transactionData = buildPayment(repository, groupId);
-			transaction = new PaymentTransaction(repository, transactionData);
-			assertEquals(ValidationResult.INVALID_TX_GROUP_ID, transaction.isValidUnconfirmed());
-		}
-	}
-
-	private PaymentTransactionData buildPayment(Repository repository, int txGroupId) throws DataException {
-		long timestamp = System.currentTimeMillis() - 1000L;
-		byte[] reference = repository.getAccountRepository().getLastReference(v2testAddress);
-		byte[] senderPublicKey = v2testPublicKey;
-		String recipient = v2testAddress;
-		BigDecimal amount = BigDecimal.ONE.setScale(8);
-		BigDecimal fee = BigDecimal.ONE.setScale(8);
-
-		BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, senderPublicKey, fee, null);
-		return new PaymentTransactionData(baseTransactionData, recipient, amount);
-	}
-
-	private int createGroup(Repository repository) throws DataException {
-		long timestamp = System.currentTimeMillis() - 1000L;
-		int txGroupId = Group.NO_GROUP;
-		byte[] reference = repository.getAccountRepository().getLastReference(v2testAddress);
-		byte[] creatorPublicKey = v2testPublicKey;
-		String owner = v2testAddress;
-		String groupName = "test-group";
-		String description = "test group description";
-		boolean isOpen = false;
-		ApprovalThreshold approvalThreshold = ApprovalThreshold.ONE;
-		int minimumBlockDelay = 0;
-		int maximumBlockDelay = 1440;
-		BigDecimal fee = BigDecimal.ONE.setScale(8);
-
-		BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, creatorPublicKey, fee, null);
-		TransactionData transactionData = new CreateGroupTransactionData(baseTransactionData, owner, groupName, description,
-				isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay);
-		Transaction transaction = new CreateGroupTransaction(repository, transactionData);
-
-		// Sign transaction
-		PrivateKeyAccount signer = new PrivateKeyAccount(repository, v2testPrivateKey);
-		transaction.sign(signer);
-
-		// Add to unconfirmed
-		if (!transaction.isSignatureValid())
-			throw new RuntimeException("CREATE_GROUP transaction's signature invalid");
-
-		ValidationResult result = transaction.isValidUnconfirmed();
-		if (result != ValidationResult.OK)
-			throw new RuntimeException(String.format("CREATE_GROUP transaction invalid: %s", result.name()));
-
-		repository.getTransactionRepository().save(transactionData);
-		repository.getTransactionRepository().unconfirmTransaction(transactionData);
-		repository.saveChanges();
-
-		// Generate block
-		BlockGenerator.generateTestingBlock(repository, signer);
-
-		// Return assigned groupId
-		transactionData = repository.getTransactionRepository().fromSignature(transactionData.getSignature());
-		return ((CreateGroupTransactionData) transactionData).getGroupId();
-	}
-
-}
diff --git a/src/test/java/org/qora/test/common/BlockUtils.java b/src/test/java/org/qora/test/common/BlockUtils.java
new file mode 100644
index 00000000..2364f618
--- /dev/null
+++ b/src/test/java/org/qora/test/common/BlockUtils.java
@@ -0,0 +1,25 @@
+package org.qora.test.common;
+
+import java.math.BigDecimal;
+
+import org.qora.block.Block;
+import org.qora.data.block.BlockData;
+import org.qora.repository.DataException;
+import org.qora.repository.Repository;
+
+public class BlockUtils {
+
+	public static BigDecimal getNextBlockReward(Repository repository) throws DataException {
+		int currentHeight = repository.getBlockRepository().getBlockchainHeight();
+
+		return Block.getRewardAtHeight(currentHeight + 1);
+	}
+
+	public static void orphanLastBlock(Repository repository) throws DataException {
+		BlockData blockData = repository.getBlockRepository().getLastBlock();
+		Block block = new Block(repository, blockData);
+		block.orphan();
+		repository.saveChanges();
+	}
+
+}
diff --git a/src/test/java/org/qora/test/common/GroupUtils.java b/src/test/java/org/qora/test/common/GroupUtils.java
new file mode 100644
index 00000000..ab46d69f
--- /dev/null
+++ b/src/test/java/org/qora/test/common/GroupUtils.java
@@ -0,0 +1,66 @@
+package org.qora.test.common;
+
+import java.math.BigDecimal;
+
+import org.qora.account.PrivateKeyAccount;
+import org.qora.data.transaction.BaseTransactionData;
+import org.qora.data.transaction.CreateGroupTransactionData;
+import org.qora.data.transaction.GroupApprovalTransactionData;
+import org.qora.data.transaction.JoinGroupTransactionData;
+import org.qora.data.transaction.TransactionData;
+import org.qora.group.Group;
+import org.qora.group.Group.ApprovalThreshold;
+import org.qora.repository.DataException;
+import org.qora.repository.Repository;
+import org.qora.transaction.Transaction.ApprovalStatus;
+
+public class GroupUtils {
+
+	public static final int txGroupId = Group.NO_GROUP;
+	public static final BigDecimal fee = BigDecimal.ONE.setScale(8);
+
+	public static int createGroup(Repository repository, String creatorAccountName, String groupName, boolean isOpen, ApprovalThreshold approvalThreshold,
+				int minimumBlockDelay, int maximumBlockDelay) throws DataException {
+		PrivateKeyAccount account = Common.getTestAccount(repository, creatorAccountName);
+
+		byte[] reference = account.getLastReference();
+		long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1;
+		String groupDescription = groupName + " (test group)";
+
+		BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, account.getPublicKey(), GroupUtils.fee, null);
+		TransactionData transactionData = new CreateGroupTransactionData(baseTransactionData, account.getAddress(), groupName, groupDescription, isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay);
+
+		TransactionUtils.signAndForge(repository, transactionData, account);
+
+		return repository.getGroupRepository().fromGroupName(groupName).getGroupId();
+	}
+
+	public static void joinGroup(Repository repository, String joinerAccountName, int groupId) throws DataException {
+		PrivateKeyAccount account = Common.getTestAccount(repository, joinerAccountName);
+
+		byte[] reference = account.getLastReference();
+		long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1;
+
+		BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, account.getPublicKey(), GroupUtils.fee, null);
+		TransactionData transactionData = new JoinGroupTransactionData(baseTransactionData, groupId);
+
+		TransactionUtils.signAndForge(repository, transactionData, account);
+	}
+
+	public static void approveTransaction(Repository repository, String accountName, byte[] pendingSignature, boolean decision) throws DataException {
+		PrivateKeyAccount account = Common.getTestAccount(repository, accountName);
+
+		byte[] reference = account.getLastReference();
+		long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1;
+
+		BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, account.getPublicKey(), GroupUtils.fee, null);
+		TransactionData transactionData = new GroupApprovalTransactionData(baseTransactionData, pendingSignature, decision);
+
+		TransactionUtils.signAndForge(repository, transactionData, account);
+	}
+
+	public static ApprovalStatus getApprovalStatus(Repository repository, byte[] signature) throws DataException {
+		return repository.getTransactionRepository().fromSignature(signature).getApprovalStatus();
+	}
+
+}
diff --git a/src/test/java/org/qora/test/common/TestAccount.java b/src/test/java/org/qora/test/common/TestAccount.java
index 48269ce2..26a9d570 100644
--- a/src/test/java/org/qora/test/common/TestAccount.java
+++ b/src/test/java/org/qora/test/common/TestAccount.java
@@ -5,6 +5,7 @@ import org.qora.repository.Repository;
 import org.qora.utils.Base58;
 
 public class TestAccount extends PrivateKeyAccount {
+
 	public final String accountName;
 
 	public TestAccount(Repository repository, String accountName, byte[] privateKey) {
@@ -16,4 +17,5 @@ public class TestAccount extends PrivateKeyAccount {
 	public TestAccount(Repository repository, String accountName, String privateKey) {
 		this(repository, accountName, Base58.decode(privateKey));
 	}
+
 }
diff --git a/src/test/java/org/qora/test/forging/RewardTests.java b/src/test/java/org/qora/test/forging/RewardTests.java
index c908496a..c9704307 100644
--- a/src/test/java/org/qora/test/forging/RewardTests.java
+++ b/src/test/java/org/qora/test/forging/RewardTests.java
@@ -17,6 +17,7 @@ import org.qora.repository.DataException;
 import org.qora.repository.Repository;
 import org.qora.repository.RepositoryManager;
 import org.qora.test.common.AccountUtils;
+import org.qora.test.common.BlockUtils;
 import org.qora.test.common.Common;
 
 public class RewardTests extends Common {
@@ -38,11 +39,11 @@ public class RewardTests extends Common {
 
 			PrivateKeyAccount forgingAccount = Common.getTestAccount(repository, "alice");
 
-			BigDecimal firstReward = BlockChain.getInstance().getBlockRewardsByHeight().get(0).reward;
+			BigDecimal blockReward = BlockUtils.getNextBlockReward(repository);
 
 			BlockGenerator.generateTestingBlock(repository, forgingAccount);
 
-			BigDecimal expectedBalance = initialBalances.get("alice").get(Asset.QORA).add(firstReward);
+			BigDecimal expectedBalance = initialBalances.get("alice").get(Asset.QORA).add(blockReward);
 			AccountUtils.assertBalance(repository, "alice", Asset.QORA, expectedBalance);
 		}
 	}
@@ -84,16 +85,15 @@ public class RewardTests extends Common {
 			PrivateKeyAccount proxyAccount = new PrivateKeyAccount(repository, proxyPrivateKey);
 
 			Map<String, Map<Long, BigDecimal>> initialBalances = AccountUtils.getBalances(repository, Asset.QORA);
+			BigDecimal blockReward = BlockUtils.getNextBlockReward(repository);
 			BlockGenerator.generateTestingBlock(repository, proxyAccount);
 
-			// We're expected reward * 12.8% to Bob, the rest to Alice
-			// (first reward is good for first 10 blocks)
-			BigDecimal firstReward = BlockChain.getInstance().getBlockRewardsByHeight().get(0).reward;
+			// We're expecting reward * 12.8% to Bob, the rest to Alice
 
-			BigDecimal bobShare = firstReward.multiply(share.movePointLeft(2)).setScale(8, RoundingMode.DOWN);
+			BigDecimal bobShare = blockReward.multiply(share.movePointLeft(2)).setScale(8, RoundingMode.DOWN);
 			AccountUtils.assertBalance(repository, "bob", Asset.QORA, initialBalances.get("bob").get(Asset.QORA).add(bobShare));
 
-			BigDecimal aliceShare = firstReward.subtract(bobShare);
+			BigDecimal aliceShare = blockReward.subtract(bobShare);
 			AccountUtils.assertBalance(repository, "alice", Asset.QORA, initialBalances.get("alice").get(Asset.QORA).add(aliceShare));
 		}
 	}
diff --git a/src/test/java/org/qora/test/group/GroupApprovalTests.java b/src/test/java/org/qora/test/group/GroupApprovalTests.java
new file mode 100644
index 00000000..eac1aa97
--- /dev/null
+++ b/src/test/java/org/qora/test/group/GroupApprovalTests.java
@@ -0,0 +1,184 @@
+package org.qora.test.group;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.qora.account.PrivateKeyAccount;
+import org.qora.asset.Asset;
+import org.qora.block.BlockGenerator;
+import org.qora.data.transaction.BaseTransactionData;
+import org.qora.data.transaction.IssueAssetTransactionData;
+import org.qora.data.transaction.PaymentTransactionData;
+import org.qora.data.transaction.TransactionData;
+import org.qora.group.Group;
+import org.qora.group.Group.ApprovalThreshold;
+import org.qora.repository.DataException;
+import org.qora.repository.Repository;
+import org.qora.repository.RepositoryManager;
+import org.qora.test.common.BlockUtils;
+import org.qora.test.common.Common;
+import org.qora.test.common.GroupUtils;
+import org.qora.test.common.TransactionUtils;
+import org.qora.transaction.Transaction;
+import org.qora.transaction.Transaction.ApprovalStatus;
+import org.qora.transaction.Transaction.ValidationResult;
+
+import static org.junit.Assert.*;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+
+public class GroupApprovalTests extends Common {
+
+	private static final BigDecimal amount = BigDecimal.valueOf(5000L).setScale(8);
+	private static final BigDecimal fee = BigDecimal.ONE.setScale(8);
+
+	@Before
+	public void beforeTest() throws DataException {
+		Common.useDefaultSettings();
+	}
+
+	@After
+	public void afterTest() throws DataException {
+		Common.orphanCheck();
+	}
+
+	@Test
+	/** Check that a transaction type that doesn't need approval doesn't accept txGroupId apart from NO_GROUP */
+	public void testNonApprovalTxGroupId() throws DataException {
+		try (final Repository repository = RepositoryManager.getRepository()) {
+			Transaction transaction = buildPaymentTransaction(repository, "alice", "bob", amount, Group.NO_GROUP);
+			assertEquals(ValidationResult.OK, transaction.isValidUnconfirmed());
+
+			int groupId = GroupUtils.createGroup(repository, "alice", "test", true, ApprovalThreshold.NONE, 0, 10);
+
+			transaction = buildPaymentTransaction(repository, "alice", "bob", amount, groupId);
+			assertEquals(ValidationResult.INVALID_TX_GROUP_ID, transaction.isValidUnconfirmed());
+		}
+	}
+
+	@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");
+
+			int groupId = GroupUtils.createGroup(repository, "alice", "test", true, ApprovalThreshold.ONE, minBlockDelay, maxBlockDelay);
+
+			GroupUtils.joinGroup(repository, "bob", groupId);
+
+			PrivateKeyAccount bobAccount = Common.getTestAccount(repository, "bob");
+			byte[] bobOriginalReference = bobAccount.getLastReference();
+
+			BigDecimal aliceOriginalBalance = aliceAccount.getConfirmedBalance(Asset.QORA);
+			BigDecimal bobOriginalBalance = bobAccount.getConfirmedBalance(Asset.QORA);
+
+			BigDecimal blockReward = BlockUtils.getNextBlockReward(repository);
+			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);
+
+			// Bob's last-reference should have changed, even though the transaction itself hasn't been approved yet
+			byte[] bobPostAssetReference = bobAccount.getLastReference();
+			assertFalse("reference should have changed", Arrays.equals(bobOriginalReference, bobPostAssetReference));
+
+			// Bob's balance should have the fee removed, even though the transaction itself hasn't been approved yet
+			BigDecimal bobPostAssetBalance = bobAccount.getConfirmedBalance(Asset.QORA);
+			Common.assertEqualBigDecimals("approval-pending transaction creator's balance incorrect", bobOriginalBalance.subtract(fee), bobPostAssetBalance);
+
+			// Transaction fee should have ended up in forging account
+			BigDecimal alicePostAssetBalance = aliceAccount.getConfirmedBalance(Asset.QORA);
+			Common.assertEqualBigDecimals("block forger's balance incorrect", aliceOriginalBalance.add(blockReward).add(fee), alicePostAssetBalance);
+
+			// 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);
+
+			byte[] bobPostPaymentReference = bobAccount.getLastReference();
+			assertFalse("reference should have changed", Arrays.equals(bobPostAssetReference, bobPostPaymentReference));
+
+			// 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);
+
+			// Check Bob's last reference hasn't been changed by transaction approval
+			byte[] bobPostApprovalReference = bobAccount.getLastReference();
+			assertTrue("reference should be unchanged", Arrays.equals(bobPostPaymentReference, bobPostApprovalReference));
+
+			// Ok, now unwind/orphan all the above to double-check
+
+			// Orphan blocks that decided transaction approval
+			for (int blockCount = 0; blockCount < minBlockDelay; ++blockCount)
+				BlockUtils.orphanLastBlock(repository);
+
+			// Check Bob's last reference is still correct
+			byte[] bobReference = bobAccount.getLastReference();
+			assertTrue("reference should be unchanged", Arrays.equals(bobPostPaymentReference, bobReference));
+
+			// Orphan block containing Alice's group-approval transaction
+			BlockUtils.orphanLastBlock(repository);
+
+			// Check Bob's last reference is still correct
+			bobReference = bobAccount.getLastReference();
+			assertTrue("reference should be unchanged", Arrays.equals(bobPostPaymentReference, bobReference));
+
+			// Orphan block containing Bob's non-approval payment transaction
+			BlockUtils.orphanLastBlock(repository);
+
+			// Check Bob's last reference has reverted to pre-payment value
+			bobReference = bobAccount.getLastReference();
+			assertTrue("reference should be pre-payment", Arrays.equals(bobPostAssetReference, bobReference));
+
+			// Orphan block containing Bob's issue-asset approval-needed transaction
+			BlockUtils.orphanLastBlock(repository);
+
+			// Check Bob's last reference has reverted to original value
+			bobReference = bobAccount.getLastReference();
+			assertTrue("reference should be pre-payment", Arrays.equals(bobOriginalReference, bobReference));
+
+			// Also check Bob's balance is back to original value
+			BigDecimal bobBalance = bobAccount.getConfirmedBalance(Asset.QORA);
+			Common.assertEqualBigDecimals("reverted balance doesn't match original", bobOriginalBalance, bobBalance);
+		}
+	}
+
+	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);
+
+		byte[] reference = sendingAccount.getLastReference();
+		long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1;
+
+		BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, sendingAccount.getPublicKey(), fee, null);
+		PaymentTransactionData transactionData = new PaymentTransactionData(baseTransactionData, recipientAccount.getAddress(), amount);
+
+		return Transaction.fromData(repository, transactionData);
+	}
+
+	private Transaction buildIssueAssetTransaction(Repository repository, String testAccountName, int txGroupId) throws DataException {
+		PrivateKeyAccount account = Common.getTestAccount(repository, testAccountName);
+
+		byte[] reference = account.getLastReference();
+		long timestamp = repository.getTransactionRepository().fromSignature(reference).getTimestamp() + 1;
+
+		BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, account.getPublicKey(), fee, null);
+		TransactionData transactionData = new IssueAssetTransactionData(baseTransactionData, account.getAddress(), "test asset", "test asset desc", 1000L, true, "{}");
+
+		return Transaction.fromData(repository, transactionData);
+	}
+
+}