Added PoW to MESSAGE (for zero fee). DB and tx layout changes.

This commit is contained in:
catbref 2020-06-04 10:20:02 +01:00
parent 50b912e229
commit d0b4a1f12f
10 changed files with 238 additions and 26 deletions

View File

@ -807,12 +807,14 @@ public class CrossChainResource {
if (lastReference == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_REFERENCE);
Long fee = null;
int version = 4;
int nonce = 0;
long amount = 0L;
Long assetId = null; // no assetId as amount is zero
Long fee = null;
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, senderPublicKey, fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, 4, atAddress, amount, assetId, messageData, false, false);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, atAddress, amount, assetId, messageData, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);

View File

@ -8,6 +8,7 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
@ -20,6 +21,9 @@ public class MessageTransactionData extends TransactionData {
private int version;
@Schema(accessMode = AccessMode.READ_ONLY)
private int nonce;
// Not always present
private String recipient;
@ -47,11 +51,12 @@ public class MessageTransactionData extends TransactionData {
}
public MessageTransactionData(BaseTransactionData baseTransactionData,
int version, String recipient, long amount, Long assetId, byte[] data, boolean isText, boolean isEncrypted) {
int version, int nonce, String recipient, long amount, Long assetId, byte[] data, boolean isText, boolean isEncrypted) {
super(TransactionType.MESSAGE, baseTransactionData);
this.senderPublicKey = baseTransactionData.creatorPublicKey;
this.version = version;
this.nonce = nonce;
this.recipient = recipient;
this.amount = amount;
this.assetId = assetId;
@ -70,6 +75,14 @@ public class MessageTransactionData extends TransactionData {
return this.version;
}
public int getNonce() {
return this.nonce;
}
public void setNonce(int nonce) {
this.nonce = nonce;
}
public String getRecipient() {
return this.recipient;
}

View File

@ -268,7 +268,7 @@ public class HSQLDBDatabaseUpdates {
case 6:
// Message Transactions
stmt.execute("CREATE TABLE MessageTransactions (signature Signature, version TINYINT NOT NULL, "
stmt.execute("CREATE TABLE MessageTransactions (signature Signature, version TINYINT NOT NULL, nonce INT NOT NULL, "
+ "sender QortalPublicKey NOT NULL, recipient QortalAddress, amount QortalAmount NOT NULL, asset_id AssetID, "
+ "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, data MessageData NOT NULL, "
+ TRANSACTION_KEYS + ")");

View File

@ -17,26 +17,27 @@ public class HSQLDBMessageTransactionRepository extends HSQLDBTransactionReposit
}
TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException {
String sql = "SELECT version, recipient, is_text, is_encrypted, amount, asset_id, data FROM MessageTransactions WHERE signature = ?";
String sql = "SELECT version, nonce, recipient, is_text, is_encrypted, amount, asset_id, data FROM MessageTransactions WHERE signature = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) {
if (resultSet == null)
return null;
int version = resultSet.getInt(1);
String recipient = resultSet.getString(2);
boolean isText = resultSet.getBoolean(3);
boolean isEncrypted = resultSet.getBoolean(4);
long amount = resultSet.getLong(5);
int nonce = resultSet.getInt(2);
String recipient = resultSet.getString(3);
boolean isText = resultSet.getBoolean(4);
boolean isEncrypted = resultSet.getBoolean(5);
long amount = resultSet.getLong(6);
// Special null-checking for asset ID
Long assetId = resultSet.getLong(6);
Long assetId = resultSet.getLong(7);
if (assetId == 0 && resultSet.wasNull())
assetId = null;
byte[] data = resultSet.getBytes(7);
byte[] data = resultSet.getBytes(8);
return new MessageTransactionData(baseTransactionData, version, recipient, amount, assetId, data, isText, isEncrypted);
return new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, isText, isEncrypted);
} catch (SQLException e) {
throw new DataException("Unable to fetch message transaction from repository", e);
}
@ -52,7 +53,7 @@ public class HSQLDBMessageTransactionRepository extends HSQLDBTransactionReposit
.bind("sender", messageTransactionData.getSenderPublicKey()).bind("recipient", messageTransactionData.getRecipient())
.bind("is_text", messageTransactionData.isText()).bind("is_encrypted", messageTransactionData.isEncrypted())
.bind("amount", messageTransactionData.getAmount()).bind("asset_id", messageTransactionData.getAssetId())
.bind("data", messageTransactionData.getData());
.bind("nonce", messageTransactionData.getNonce()).bind("data", messageTransactionData.getData());
try {
saveHelper.execute(this.repository);

View File

@ -136,8 +136,10 @@ public class ChatTransaction extends Transaction {
@Override
public ValidationResult isValid() throws DataException {
// Nonce checking is done via isSignatureValid() as that method is only called once per import
// If we exist in the repository then we've been imported as unconfirmed,
// but we don't want to make it into a block, so return false.
// but we don't want to make it into a block, so return fake non-OK result.
if (this.repository.getTransactionRepository().exists(this.chatTransactionData.getSignature()))
return ValidationResult.CHAT;
@ -150,8 +152,6 @@ public class ChatTransaction extends Transaction {
if (chatTransactionData.getData().length < 1 || chatTransactionData.getData().length > MAX_DATA_SIZE)
return ValidationResult.INVALID_DATA_LENGTH;
// Nonce checking is done via isSignatureValid() as that method is only called once per import
return ValidationResult.OK;
}

View File

@ -4,19 +4,30 @@ import java.util.Collections;
import java.util.List;
import org.qortal.account.Account;
import org.qortal.account.PublicKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.crypto.Crypto;
import org.qortal.crypto.MemoryPoW;
import org.qortal.data.PaymentData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.payment.Payment;
import org.qortal.repository.DataException;
import org.qortal.repository.GroupRepository;
import org.qortal.repository.Repository;
import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.ChatTransactionTransformer;
import org.qortal.transform.transaction.MessageTransactionTransformer;
import org.qortal.transform.transaction.TransactionTransformer;
public class MessageTransaction extends Transaction {
// Useful constants
public static final int MAX_DATA_SIZE = 4000;
public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
public static final int POW_DIFFICULTY = 14; // leading zero bits
// Properties
@ -63,8 +74,76 @@ public class MessageTransaction extends Transaction {
return this.paymentData;
}
public void computeNonce() throws DataException {
byte[] transactionBytes;
try {
transactionBytes = TransactionTransformer.toBytesForSigning(this.transactionData);
} catch (TransformationException e) {
throw new RuntimeException("Unable to transform transaction to byte array for verification", e);
}
// Clear nonce from transactionBytes
MessageTransactionTransformer.clearNonce(transactionBytes);
// Calculate nonce
this.messageTransactionData.setNonce(MemoryPoW.compute2(transactionBytes, POW_BUFFER_SIZE, POW_DIFFICULTY));
}
/**
* Returns whether MESSAGE transaction has valid txGroupId.
* <p>
* For MESSAGE transactions, a non-NO_GROUP txGroupId represents
* sending to a group, rather than to everyone.
* <p>
* If txGroupId is not NO_GROUP, then the sender needs to be
* a member of that group. The recipient, if supplied, also
* needs to be a member of that group.
*/
@Override
protected boolean isValidTxGroupId() throws DataException {
int txGroupId = this.transactionData.getTxGroupId();
// txGroupId represents recipient group, unless NO_GROUP
// Anyone can use NO_GROUP
if (txGroupId == Group.NO_GROUP)
return true;
// Group even exist?
if (!this.repository.getGroupRepository().groupExists(txGroupId))
return false;
GroupRepository groupRepository = this.repository.getGroupRepository();
// Is transaction's creator is group member?
PublicKeyAccount creator = this.getCreator();
if (!groupRepository.memberExists(txGroupId, creator.getAddress()))
return false;
// If recipient address present, check they belong to group too.
String recipient = this.messageTransactionData.getRecipient();
if (recipient != null && !groupRepository.memberExists(txGroupId, recipient))
return false;
return true;
}
@Override
public ValidationResult isFeeValid() throws DataException {
// Allow zero or positive fee.
// Actual enforcement of fee vs nonce is done in isSignatureValid().
if (this.transactionData.getFee() < 0)
return ValidationResult.NEGATIVE_FEE;
return ValidationResult.OK;
}
@Override
public ValidationResult isValid() throws DataException {
// Nonce checking is done via isSignatureValid() as that method is only called once per import
// Check data length
if (this.messageTransactionData.getData().length < 1 || this.messageTransactionData.getData().length > MAX_DATA_SIZE)
return ValidationResult.INVALID_DATA_LENGTH;
@ -86,6 +165,36 @@ public class MessageTransaction extends Transaction {
this.messageTransactionData.getFee(), true);
}
@Override
public boolean isSignatureValid() {
byte[] signature = this.transactionData.getSignature();
if (signature == null)
return false;
byte[] transactionBytes;
try {
transactionBytes = ChatTransactionTransformer.toBytesForSigning(this.transactionData);
} catch (TransformationException e) {
throw new RuntimeException("Unable to transform transaction to byte array for verification", e);
}
if (!Crypto.verify(this.transactionData.getCreatorPublicKey(), signature, transactionBytes))
return false;
// If feee is non-zero then we don't check nonce
if (this.messageTransactionData.getFee() > 0)
return true;
int nonce = this.messageTransactionData.getNonce();
// Clear nonce from transactionBytes
MessageTransactionTransformer.clearNonce(transactionBytes);
// Check nonce
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, POW_DIFFICULTY, nonce);
}
@Override
public ValidationResult isProcessable() throws DataException {
// If we have no amount then we can always process

View File

@ -19,13 +19,14 @@ import com.google.common.primitives.Longs;
public class MessageTransactionTransformer extends TransactionTransformer {
// Property lengths
private static final int NONCE_LENGTH = INT_LENGTH;
private static final int HAS_RECIPIENT_LENGTH = BOOLEAN_LENGTH;
private static final int RECIPIENT_LENGTH = ADDRESS_LENGTH;
private static final int DATA_SIZE_LENGTH = INT_LENGTH;
private static final int IS_TEXT_LENGTH = BOOLEAN_LENGTH;
private static final int IS_ENCRYPTED_LENGTH = BOOLEAN_LENGTH;
private static final int EXTRAS_LENGTH = HAS_RECIPIENT_LENGTH + AMOUNT_LENGTH + DATA_SIZE_LENGTH + IS_ENCRYPTED_LENGTH + IS_TEXT_LENGTH;
private static final int EXTRAS_LENGTH = NONCE_LENGTH + HAS_RECIPIENT_LENGTH + AMOUNT_LENGTH + DATA_SIZE_LENGTH + IS_ENCRYPTED_LENGTH + IS_TEXT_LENGTH;
protected static final TransactionLayout layout;
@ -36,10 +37,11 @@ public class MessageTransactionTransformer extends TransactionTransformer {
layout.add("transaction's groupID", TransformationType.INT);
layout.add("reference", TransformationType.SIGNATURE);
layout.add("sender's public key", TransformationType.PUBLIC_KEY);
layout.add("proof-of-work nonce (zero if fee not zero)", TransformationType.INT);
layout.add("has recipient?", TransformationType.BOOLEAN);
layout.add("? recipient", TransformationType.ADDRESS);
layout.add("payment (can be zero)", TransformationType.AMOUNT);
layout.add("asset ID of payment (if payment not zero)", TransformationType.LONG);
layout.add("? asset ID of payment (if payment not zero)", TransformationType.LONG);
layout.add("message length", TransformationType.INT);
layout.add("message", TransformationType.DATA);
layout.add("is message encrypted?", TransformationType.BOOLEAN);
@ -60,6 +62,8 @@ public class MessageTransactionTransformer extends TransactionTransformer {
byte[] senderPublicKey = Serialization.deserializePublicKey(byteBuffer);
int nonce = byteBuffer.getInt();
boolean hasRecipient = byteBuffer.get() != 0;
String recipient = hasRecipient ? Serialization.deserializeAddress(byteBuffer) : null;
@ -86,7 +90,7 @@ public class MessageTransactionTransformer extends TransactionTransformer {
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, senderPublicKey, fee, signature);
return new MessageTransactionData(baseTransactionData, version, recipient, amount, assetId, data, isText, isEncrypted);
return new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, isText, isEncrypted);
}
public static int getDataLength(TransactionData transactionData) throws TransformationException {
@ -111,6 +115,8 @@ public class MessageTransactionTransformer extends TransactionTransformer {
transformCommonBytes(transactionData, bytes);
bytes.write(Ints.toByteArray(messageTransactionData.getNonce()));
if (messageTransactionData.getRecipient() != null) {
bytes.write((byte) 1);
Serialization.serializeAddress(bytes, messageTransactionData.getRecipient());
@ -142,4 +148,13 @@ public class MessageTransactionTransformer extends TransactionTransformer {
}
}
public static void clearNonce(byte[] transactionBytes) {
int nonceIndex = TYPE_LENGTH + TIMESTAMP_LENGTH + GROUPID_LENGTH + REFERENCE_LENGTH + PUBLIC_KEY_LENGTH;
transactionBytes[nonceIndex++] = (byte) 0;
transactionBytes[nonceIndex++] = (byte) 0;
transactionBytes[nonceIndex++] = (byte) 0;
transactionBytes[nonceIndex++] = (byte) 0;
}
}

View File

@ -29,7 +29,7 @@ import static org.junit.Assert.*;
public class MessageTests extends Common {
private static final int version = 3;
private static final int version = 4;
private static final String recipient = Common.getTestAccount(null, "bob").getAddress();
@ -69,6 +69,26 @@ public class MessageTests extends Common {
assertFalse(isValid(newGroupId, null, 0L, null));
}
@Test
public void noFeeNoNonce() throws DataException {
testFeeNonce(false, false, false);
}
@Test
public void withFeeNoNonce() throws DataException {
testFeeNonce(true, false, true);
}
@Test
public void noFeeWithNonce() throws DataException {
testFeeNonce(false, true, true);
}
@Test
public void withFeeWithNonce() throws DataException {
testFeeNonce(true, true, true);
}
@Test
public void withRecipentNoAmount() throws DataException {
testMessage(Group.NO_GROUP, recipient, 0L, null);
@ -105,8 +125,13 @@ public class MessageTests extends Common {
try (final Repository repository = RepositoryManager.getRepository()) {
TestAccount alice = Common.getTestAccount(repository, "alice");
int nonce = 0;
byte[] data = new byte[1];
boolean isText = false;
boolean isEncrypted = false;
MessageTransactionData transactionData = new MessageTransactionData(TestTransaction.generateBase(alice, txGroupId),
version, recipient, amount, assetId, new byte[1], false, false);
version, nonce, recipient, amount, assetId, data, isText, isEncrypted);
Transaction transaction = new MessageTransaction(repository, transactionData);
@ -114,12 +139,51 @@ public class MessageTests extends Common {
}
}
private void testFeeNonce(boolean withFee, boolean withNonce, boolean isValid) throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
TestAccount alice = Common.getTestAccount(repository, "alice");
int txGroupId = 0;
int nonce = 0;
long amount = 0;
long assetId = Asset.QORT;
byte[] data = new byte[1];
boolean isText = false;
boolean isEncrypted = false;
MessageTransactionData transactionData = new MessageTransactionData(TestTransaction.generateBase(alice, txGroupId),
version, nonce, recipient, amount, assetId, data, isText, isEncrypted);
MessageTransaction transaction = new MessageTransaction(repository, transactionData);
if (withFee)
transactionData.setFee(transaction.calcRecommendedFee());
else
transactionData.setFee(0L);
if (withNonce) {
transaction.computeNonce();
} else {
transactionData.setNonce(-1);
}
transaction.sign(alice);
assertEquals(isValid, transaction.isSignatureValid());
}
}
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");
int nonce = 0;
byte[] data = new byte[1];
boolean isText = false;
boolean isEncrypted = false;
MessageTransactionData transactionData = new MessageTransactionData(TestTransaction.generateBase(alice, txGroupId),
version, recipient, amount, assetId, new byte[1], false, false);
version, nonce, recipient, amount, assetId, data, isText, isEncrypted);
TransactionUtils.signAndMint(repository, transactionData, alice);
@ -131,8 +195,13 @@ public class MessageTests extends Common {
try (final Repository repository = RepositoryManager.getRepository()) {
TestAccount alice = Common.getTestAccount(repository, "alice");
int nonce = 0;
byte[] data = new byte[1];
boolean isText = false;
boolean isEncrypted = false;
MessageTransactionData expectedTransactionData = new MessageTransactionData(TestTransaction.generateBase(alice),
version, recipient, amount, assetId, new byte[1], false, false);
version, nonce, recipient, amount, assetId, data, isText, isEncrypted);
Transaction transaction = new MessageTransaction(repository, expectedTransactionData);
transaction.sign(alice);

View File

@ -473,11 +473,13 @@ public class AtTests extends Common {
}
Long fee = null;
int version = 4;
int nonce = 0;
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, amount, assetId, data, false, false);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);

View File

@ -11,7 +11,8 @@ import org.qortal.utils.Amounts;
public class MessageTestTransaction extends TestTransaction {
public static TransactionData randomTransaction(Repository repository, PrivateKeyAccount account, boolean wantValid) throws DataException {
final int version = 3;
final int version = 4;
final int nonce = 0;
String recipient = account.getAddress();
final long assetId = Asset.QORT;
long amount = 123L * Amounts.MULTIPLIER;
@ -19,7 +20,7 @@ public class MessageTestTransaction extends TestTransaction {
final boolean isText = true;
final boolean isEncrypted = false;
return new MessageTransactionData(generateBase(account), version, recipient, amount, assetId, data, isText, isEncrypted);
return new MessageTransactionData(generateBase(account), version, nonce, recipient, amount, assetId, data, isText, isEncrypted);
}
}