Allow MESSAGE transactions to have no recipient.

This allows on-chain messages to a group, including NO_GROUP / groupID zero.

No-recipient messages cannot have an amount - where would it go?

Changed MESSAGE serialization layout to add boolean indicating
whether recipient is present.

Changed MESSAGE serialization layout so assetID is after amount,
and only present if amount is non-zero.

Changed DB table structures to cover above.

Added unit tests to cover above.
This commit is contained in:
catbref
2020-05-18 09:09:35 +01:00
parent f6ed3388a4
commit 24eb7c6933
10 changed files with 273 additions and 47 deletions

View File

@@ -0,0 +1,154 @@
package org.qortal.test;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qortal.asset.Asset;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.group.Group.ApprovalThreshold;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.GroupUtils;
import org.qortal.test.common.TestAccount;
import org.qortal.test.common.TransactionUtils;
import org.qortal.test.common.transaction.TestTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.MessageTransactionTransformer;
import org.qortal.transform.transaction.TransactionTransformer;
import static org.junit.Assert.*;
public class MessageTests extends Common {
private static final int version = 3;
private static final String recipient = Common.getTestAccount(null, "bob").getAddress();
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@After
public void afterTest() throws DataException {
Common.orphanCheck();
}
@Test
public void validityTests() throws DataException {
// with recipient, with amount
assertTrue(isValid(Group.NO_GROUP, recipient, 123L, Asset.QORT));
// with recipient, no amount
assertTrue(isValid(Group.NO_GROUP, recipient, 0L, null));
// no recipient (message to group), no amount
assertTrue(isValid(Group.NO_GROUP, null, 0L, null));
// can't have amount if no recipient!
assertFalse(isValid(Group.NO_GROUP, null, 123L, Asset.QORT));
// Alice is part of group 1
assertTrue(isValid(1, null, 0L, null));
int newGroupId;
try (final Repository repository = RepositoryManager.getRepository()) {
newGroupId = GroupUtils.createGroup(repository, "chloe", "non-alice-group", false, ApprovalThreshold.ONE, 10, 1440);
}
// Alice is not part of new group
assertFalse(isValid(newGroupId, null, 0L, null));
}
@Test
public void withRecipentNoAmount() throws DataException {
testMessage(Group.NO_GROUP, recipient, 0L, null);
}
@Test
public void withRecipentWithAmount() throws DataException {
testMessage(Group.NO_GROUP, recipient, 123L, Asset.QORT);
}
@Test
public void noRecipentNoAmount() throws DataException {
testMessage(Group.NO_GROUP, null, 0L, null);
}
@Test
public void noRecipentNoAmountWithGroup() throws DataException {
testMessage(1, null, 0L, null);
}
@Test
public void serializationTests() throws DataException, TransformationException {
// with recipient, with amount
testSerialization(recipient, 123L, Asset.QORT);
// with recipient, no amount
testSerialization(recipient, 0L, null);
// no recipient (message to group), no amount
testSerialization(null, 0L, null);
}
private boolean isValid(int txGroupId, String recipient, long amount, Long assetId) throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
TestAccount alice = Common.getTestAccount(repository, "alice");
MessageTransactionData transactionData = new MessageTransactionData(TestTransaction.generateBase(alice, txGroupId),
version, recipient, amount, assetId, new byte[1], false, false);
Transaction transaction = new MessageTransaction(repository, transactionData);
return transaction.isValidUnconfirmed() == ValidationResult.OK;
}
}
private void testMessage(int txGroupId, String recipient, long amount, Long assetId) throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
TestAccount alice = Common.getTestAccount(repository, "alice");
MessageTransactionData transactionData = new MessageTransactionData(TestTransaction.generateBase(alice, txGroupId),
version, recipient, amount, assetId, new byte[1], false, false);
TransactionUtils.signAndMint(repository, transactionData, alice);
BlockUtils.orphanLastBlock(repository);
}
}
private void testSerialization(String recipient, long amount, Long assetId) throws DataException, TransformationException {
try (final Repository repository = RepositoryManager.getRepository()) {
TestAccount alice = Common.getTestAccount(repository, "alice");
MessageTransactionData expectedTransactionData = new MessageTransactionData(TestTransaction.generateBase(alice),
version, recipient, amount, assetId, new byte[1], false, false);
Transaction transaction = new MessageTransaction(repository, expectedTransactionData);
transaction.sign(alice);
MessageTransactionTransformer.getDataLength(expectedTransactionData);
byte[] transactionBytes = MessageTransactionTransformer.toBytes(expectedTransactionData);
TransactionData transactionData = TransactionTransformer.fromBytes(transactionBytes);
assertEquals(TransactionType.MESSAGE, transactionData.getType());
MessageTransactionData actualTransactionData = (MessageTransactionData) transactionData;
assertEquals(expectedTransactionData.getRecipient(), actualTransactionData.getRecipient());
assertEquals(expectedTransactionData.getAmount(), actualTransactionData.getAmount());
assertEquals(expectedTransactionData.getAssetId(), actualTransactionData.getAssetId());
}
}
}

View File

@@ -474,9 +474,10 @@ public class AtTests extends Common {
Long fee = null;
long amount = 0;
Long assetId = null; // because amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, 4, recipient, Asset.QORT, amount, data, false, false);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, 4, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);

View File

@@ -19,7 +19,7 @@ public class MessageTestTransaction extends TestTransaction {
final boolean isText = true;
final boolean isEncrypted = false;
return new MessageTransactionData(generateBase(account), version, recipient, assetId, amount, data, isText, isEncrypted);
return new MessageTransactionData(generateBase(account), version, recipient, amount, assetId, data, isText, isEncrypted);
}
}

View File

@@ -12,8 +12,12 @@ public abstract class TestTransaction {
protected static final Random random = new Random();
public static BaseTransactionData generateBase(PrivateKeyAccount account, int txGroupId) throws DataException {
return new BaseTransactionData(System.currentTimeMillis(), txGroupId, account.getLastReference(), account.getPublicKey(), BlockChain.getInstance().getUnitFee(), null);
}
public static BaseTransactionData generateBase(PrivateKeyAccount account) throws DataException {
return new BaseTransactionData(System.currentTimeMillis(), Group.NO_GROUP, account.getLastReference(), account.getPublicKey(), BlockChain.getInstance().getUnitFee(), null);
return generateBase(account, Group.NO_GROUP);
}
}