group approval tests and fixes

This commit is contained in:
catbref 2019-06-02 17:07:07 +01:00
parent 8af761c1c3
commit c9f226cf88
9 changed files with 296 additions and 114 deletions

View File

@ -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?

View File

@ -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();

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}