From 15d25649b21ebc980fa340a4af8a5a732118a7f9 Mon Sep 17 00:00:00 2001 From: catbref Date: Thu, 26 Nov 2020 16:11:11 +0000 Subject: [PATCH] WIP: PRESENCE transactions - repository support, removing older versions, tests --- .../transaction/PresenceTransactionData.java | 3 - .../hsqldb/HSQLDBDatabaseUpdates.java | 7 +++ .../HSQLDBPresenceTransactionRepository.java | 57 +++++++++++++++++++ .../transaction/PresenceTransaction.java | 42 +++++++++----- .../java/org/qortal/test/PresenceTests.java | 32 ++++++++++- 5 files changed, 120 insertions(+), 21 deletions(-) create mode 100644 src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBPresenceTransactionRepository.java diff --git a/src/main/java/org/qortal/data/transaction/PresenceTransactionData.java b/src/main/java/org/qortal/data/transaction/PresenceTransactionData.java index 372c31b2..2276f6ea 100644 --- a/src/main/java/org/qortal/data/transaction/PresenceTransactionData.java +++ b/src/main/java/org/qortal/data/transaction/PresenceTransactionData.java @@ -23,9 +23,6 @@ public class PresenceTransactionData extends TransactionData { @Schema(description = "sender's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") private byte[] senderPublicKey; - @Schema(accessMode = AccessMode.READ_ONLY) - private String sender; - @Schema(accessMode = AccessMode.READ_ONLY) private int nonce; diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index e60616d6..692ae26d 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -771,6 +771,13 @@ public class HSQLDBDatabaseUpdates { stmt.execute("CHECKPOINT"); break; + case 31: + // PRESENCE transactions + stmt.execute("CREATE TABLE IF NOT EXISTS PresenceTransactions (" + + "signature Signature, nonce INT NOT NULL, presence_type INT NOT NULL, " + + "timestamp_signature Signature NOT NULL, " + TRANSACTION_KEYS + ")"); + break; + default: // nothing to do return false; diff --git a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBPresenceTransactionRepository.java b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBPresenceTransactionRepository.java new file mode 100644 index 00000000..cb2b3638 --- /dev/null +++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBPresenceTransactionRepository.java @@ -0,0 +1,57 @@ +package org.qortal.repository.hsqldb.transaction; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.qortal.data.transaction.BaseTransactionData; +import org.qortal.data.transaction.PresenceTransactionData; +import org.qortal.data.transaction.PresenceTransactionData.PresenceType; +import org.qortal.data.transaction.TransactionData; +import org.qortal.repository.DataException; +import org.qortal.repository.hsqldb.HSQLDBRepository; +import org.qortal.repository.hsqldb.HSQLDBSaver; + +public class HSQLDBPresenceTransactionRepository extends HSQLDBTransactionRepository { + + public HSQLDBPresenceTransactionRepository(HSQLDBRepository repository) { + this.repository = repository; + } + + TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException { + String sql = "SELECT nonce, presence_type, timestamp_signature FROM PresenceTransactions WHERE signature = ?"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) { + if (resultSet == null) + return null; + + int nonce = resultSet.getInt(1); + int presenceTypeValue = resultSet.getInt(2); + PresenceType presenceType = PresenceType.valueOf(presenceTypeValue); + + byte[] timestampSignature = resultSet.getBytes(3); + + return new PresenceTransactionData(baseTransactionData, nonce, presenceType, timestampSignature); + } catch (SQLException e) { + throw new DataException("Unable to fetch presence transaction from repository", e); + } + } + + @Override + public void save(TransactionData transactionData) throws DataException { + PresenceTransactionData presenceTransactionData = (PresenceTransactionData) transactionData; + + HSQLDBSaver saveHelper = new HSQLDBSaver("PresenceTransactions"); + + saveHelper.bind("signature", presenceTransactionData.getSignature()) + .bind("nonce", presenceTransactionData.getNonce()) + .bind("presence_type", presenceTransactionData.getPresenceType().value) + .bind("timestamp_signature", presenceTransactionData.getTimestampSignature()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save chat transaction into repository", e); + } + } + +} diff --git a/src/main/java/org/qortal/transaction/PresenceTransaction.java b/src/main/java/org/qortal/transaction/PresenceTransaction.java index e300d290..755c31a5 100644 --- a/src/main/java/org/qortal/transaction/PresenceTransaction.java +++ b/src/main/java/org/qortal/transaction/PresenceTransaction.java @@ -3,8 +3,9 @@ package org.qortal.transaction; import java.util.Collections; import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.qortal.account.Account; -import org.qortal.asset.Asset; import org.qortal.crypto.Crypto; import org.qortal.crypto.MemoryPoW; import org.qortal.data.transaction.PresenceTransactionData; @@ -15,19 +16,20 @@ import org.qortal.repository.Repository; import org.qortal.transform.TransformationException; import org.qortal.transform.transaction.PresenceTransactionTransformer; import org.qortal.transform.transaction.TransactionTransformer; +import org.qortal.utils.Base58; import com.google.common.primitives.Longs; public class PresenceTransaction extends Transaction { + private static final Logger LOGGER = LogManager.getLogger(PresenceTransaction.class); + // Properties private PresenceTransactionData presenceTransactionData; // Other useful constants - public static final int MAX_DATA_SIZE = 256; public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes - public static final int POW_DIFFICULTY_WITH_QORT = 8; // leading zero bits - public static final int POW_DIFFICULTY_NO_QORT = 14; // leading zero bits + public static final int POW_DIFFICULTY = 8; // leading zero bits // Constructors @@ -64,10 +66,8 @@ public class PresenceTransaction extends Transaction { // Clear nonce from transactionBytes PresenceTransactionTransformer.clearNonce(transactionBytes); - int difficulty = this.getSender().getConfirmedBalance(Asset.QORT) > 0 ? POW_DIFFICULTY_WITH_QORT : POW_DIFFICULTY_NO_QORT; - // Calculate nonce - this.presenceTransactionData.setNonce(MemoryPoW.compute2(transactionBytes, POW_BUFFER_SIZE, difficulty)); + this.presenceTransactionData.setNonce(MemoryPoW.compute2(transactionBytes, POW_BUFFER_SIZE, POW_DIFFICULTY)); } /** @@ -135,15 +135,27 @@ public class PresenceTransaction extends Transaction { // Clear nonce from transactionBytes PresenceTransactionTransformer.clearNonce(transactionBytes); - int difficulty; - try { - difficulty = this.getSender().getConfirmedBalance(Asset.QORT) > 0 ? POW_DIFFICULTY_WITH_QORT : POW_DIFFICULTY_NO_QORT; - } catch (DataException e) { - return false; - } - // Check nonce - return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, difficulty, nonce); + return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, POW_DIFFICULTY, nonce); + } + + /** + * Remove any PRESENCE transactions by the same signer that have older timestamps. + */ + @Override + protected void onImportAsUnconfirmed() throws DataException { + byte[] creatorPublicKey = this.transactionData.getCreatorPublicKey(); + List creatorsPresenceTransactions = this.repository.getTransactionRepository().getUnconfirmedTransactions(TransactionType.PRESENCE, creatorPublicKey); + + if (creatorsPresenceTransactions.isEmpty()) + return; + + // List should contain oldest transaction first, so remove all but last from repository. + creatorsPresenceTransactions.remove(creatorsPresenceTransactions.size() - 1); + for (TransactionData transactionData : creatorsPresenceTransactions) { + LOGGER.info(() -> String.format("Deleting older PRESENCE transaction %s", Base58.encode(transactionData.getSignature()))); + this.repository.getTransactionRepository().delete(transactionData); + } } @Override diff --git a/src/test/java/org/qortal/test/PresenceTests.java b/src/test/java/org/qortal/test/PresenceTests.java index a0f4f4da..96cdc98c 100644 --- a/src/test/java/org/qortal/test/PresenceTests.java +++ b/src/test/java/org/qortal/test/PresenceTests.java @@ -12,6 +12,7 @@ import org.qortal.repository.DataException; import org.qortal.repository.Repository; import org.qortal.repository.RepositoryManager; import org.qortal.test.common.Common; +import org.qortal.test.common.TransactionUtils; import org.qortal.transaction.PresenceTransaction; import org.qortal.transaction.Transaction; import org.qortal.transaction.Transaction.ValidationResult; @@ -51,19 +52,44 @@ public class PresenceTests extends Common { assertTrue(isValid(Group.NO_GROUP, this.signer, timestamp, timestampSignature)); } + @Test + public void newestOnlyTests() throws DataException { + long OLDER_TIMESTAMP = System.currentTimeMillis() - 2000L; + long NEWER_TIMESTAMP = OLDER_TIMESTAMP + 1000L; + + PresenceTransaction older = buildPresenceTransaction(Group.NO_GROUP, this.signer, OLDER_TIMESTAMP, null); + older.computeNonce(); + TransactionUtils.signAndImportValid(repository, older.getTransactionData(), this.signer); + + assertTrue(this.repository.getTransactionRepository().exists(older.getTransactionData().getSignature())); + + PresenceTransaction newer = buildPresenceTransaction(Group.NO_GROUP, this.signer, NEWER_TIMESTAMP, null); + newer.computeNonce(); + TransactionUtils.signAndImportValid(repository, newer.getTransactionData(), this.signer); + + assertTrue(this.repository.getTransactionRepository().exists(newer.getTransactionData().getSignature())); + assertFalse(this.repository.getTransactionRepository().exists(older.getTransactionData().getSignature())); + } + private boolean isValid(int txGroupId, PrivateKeyAccount signer, long timestamp, byte[] timestampSignature) throws DataException { + Transaction transaction = buildPresenceTransaction(txGroupId, signer, timestamp, timestampSignature); + return transaction.isValidUnconfirmed() == ValidationResult.OK; + } + + private PresenceTransaction buildPresenceTransaction(int txGroupId, PrivateKeyAccount signer, long timestamp, byte[] timestampSignature) throws DataException { int nonce = 0; byte[] reference = signer.getLastReference(); byte[] creatorPublicKey = signer.getPublicKey(); long fee = 0L; + if (timestampSignature == null) + timestampSignature = this.signer.sign(Longs.toByteArray(timestamp)); + BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, creatorPublicKey, fee, null); PresenceTransactionData transactionData = new PresenceTransactionData(baseTransactionData, nonce, PresenceType.REWARD_SHARE, timestampSignature); - Transaction transaction = new PresenceTransaction(this.repository, transactionData); - - return transaction.isValidUnconfirmed() == ValidationResult.OK; + return new PresenceTransaction(this.repository, transactionData); } }