From 22c87a6e085e4a90cb77954c87f197058076d06b Mon Sep 17 00:00:00 2001 From: catbref Date: Wed, 9 Jan 2019 14:41:49 +0000 Subject: [PATCH] API + fix for names in HSQLDB Added POST /names/update for building an UPDATE-NAME transaction. BlockGenerator now tries to validate new block after adding each unconfirmed transaction in turn. If block becomes invalid then that transaction is removed/skipped. This should further prevent block jams. Skipped transactions might be deleted as the next block is forged when unconfirmed transactions are collated/filtered/expired. Add Block.deleteTransaction() for use during block generation above. Block.addTransaction() and Block.deleteTransaction() use transaction signatures to test for presence in Block's existing transactions. Names shouldn't have stored registrant's public key! "registrantPublicKey" removed from NameData Java object/bean. Corresponding column removed from HSQLDB using ALTER TABLE but also from the original CREATE TABLE definition. Remove the ALTER TABLE statement just prior to rebuilding database! (This needs to be applied to Polls too as some point). Also, UpdateNameTransactions and BuyNameTransactions tables now allow name_reference to be NULL as this column value isn't set until the corresponding transactions are processed/added to a block. (name_reference is a link to previous name-related transaction that altered Name data like "owner" or "data" so that name-related transactions can be orphaned/undone). --- .../org/qora/api/resource/NamesResource.java | 47 +++++++++++++++- src/main/java/org/qora/block/Block.java | 47 ++++++++++++++-- .../java/org/qora/block/BlockGenerator.java | 14 ++++- .../java/org/qora/data/naming/NameData.java | 12 ++--- .../UpdateNameTransactionData.java | 9 ++++ src/main/java/org/qora/naming/Name.java | 2 +- .../hsqldb/HSQLDBDatabaseUpdates.java | 15 ++++-- .../hsqldb/HSQLDBNameRepository.java | 53 +++++++++---------- .../org/qora/transaction/Transaction.java | 2 +- .../transaction/UpdateNameTransaction.java | 4 ++ 10 files changed, 158 insertions(+), 47 deletions(-) diff --git a/src/main/java/org/qora/api/resource/NamesResource.java b/src/main/java/org/qora/api/resource/NamesResource.java index 47aff78d..fde7429a 100644 --- a/src/main/java/org/qora/api/resource/NamesResource.java +++ b/src/main/java/org/qora/api/resource/NamesResource.java @@ -29,6 +29,7 @@ import org.qora.api.model.NameSummary; import org.qora.crypto.Crypto; import org.qora.data.naming.NameData; import org.qora.data.transaction.RegisterNameTransactionData; +import org.qora.data.transaction.UpdateNameTransactionData; import org.qora.repository.DataException; import org.qora.repository.Repository; import org.qora.repository.RepositoryManager; @@ -36,6 +37,7 @@ import org.qora.transaction.Transaction; import org.qora.transaction.Transaction.ValidationResult; import org.qora.transform.TransformationException; import org.qora.transform.transaction.RegisterNameTransactionTransformer; +import org.qora.transform.transaction.UpdateNameTransactionTransformer; import org.qora.utils.Base58; @Path("/names") @@ -153,7 +155,7 @@ public class NamesResource { } ) @ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) - public String buildTransaction(RegisterNameTransactionData transactionData) { + public String registerName(RegisterNameTransactionData transactionData) { try (final Repository repository = RepositoryManager.getRepository()) { Transaction transaction = Transaction.fromData(repository, transactionData); @@ -170,4 +172,47 @@ public class NamesResource { } } + @POST + @Path("/update") + @Operation( + summary = "Build raw, unsigned, UPDATE_NAME transaction", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = UpdateNameTransactionData.class + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, unsigned, UPDATE_NAME transaction encoded in Base58", + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string" + ) + ) + ) + } + ) + @ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) + public String updateName(UpdateNameTransactionData transactionData) { + try (final Repository repository = RepositoryManager.getRepository()) { + Transaction transaction = Transaction.fromData(repository, transactionData); + + ValidationResult result = transaction.isValidUnconfirmed(); + if (result != ValidationResult.OK) + throw TransactionsResource.createTransactionInvalidException(request, result); + + byte[] bytes = UpdateNameTransactionTransformer.toBytes(transactionData); + return Base58.encode(bytes); + } catch (TransformationException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + } \ No newline at end of file diff --git a/src/main/java/org/qora/block/Block.java b/src/main/java/org/qora/block/Block.java index daccd873..f08d67d6 100644 --- a/src/main/java/org/qora/block/Block.java +++ b/src/main/java/org/qora/block/Block.java @@ -543,8 +543,8 @@ public class Block { if (this.blockData.getGeneratorSignature() == null) throw new IllegalStateException("Cannot calculate transactions signature as block has no generator signature"); - // Already added? - if (this.transactions.contains(transactionData)) + // Already added? (Check using signature) + if (this.transactions.stream().anyMatch(transaction -> Arrays.equals(transaction.getTransactionData().getSignature(), transactionData.getSignature()))) return true; // Check there is space in block @@ -573,6 +573,47 @@ public class Block { return true; } + /** + * Remove a transaction from the block. + *

+ * Used when constructing a new block during forging. + *

+ * Requires block's {@code generator} being a {@code PrivateKeyAccount} so block's transactions signature can be recalculated. + * + * @param transactionData + * @throws IllegalStateException + * if block's {@code generator} is not a {@code PrivateKeyAccount}. + */ + public void deleteTransaction(TransactionData transactionData) { + // Can't add to transactions if we haven't loaded existing ones yet + if (this.transactions == null) + throw new IllegalStateException("Attempted to add transaction to partially loaded database Block"); + + if (!(this.generator instanceof PrivateKeyAccount)) + throw new IllegalStateException("Block's generator has no private key"); + + if (this.blockData.getGeneratorSignature() == null) + throw new IllegalStateException("Cannot calculate transactions signature as block has no generator signature"); + + // Attempt to remove from block (Check using signature) + boolean wasElementRemoved = this.transactions.removeIf(transaction -> Arrays.equals(transaction.getTransactionData().getSignature(), transactionData.getSignature())); + if (!wasElementRemoved) + // Wasn't there - nothing more to do + return; + + // Re-sort + this.transactions.sort(Transaction.getComparator()); + + // Update transaction count + this.blockData.setTransactionCount(this.blockData.getTransactionCount() - 1); + + // Update totalFees + this.blockData.setTotalFees(this.blockData.getTotalFees().subtract(transactionData.getFee())); + + // We've removed a transaction, so recalculate transactions signature + calcTransactionsSignature(); + } + /** * Recalculate block's generator signature. *

@@ -787,7 +828,7 @@ public class Block { // NOTE: in Gen1 there was an extra block height passed to DeployATTransaction.isValid Transaction.ValidationResult validationResult = transaction.isValid(); if (validationResult != Transaction.ValidationResult.OK) { - LOGGER.error("Error during transaction validation, tx " + Base58.encode(transaction.getTransactionData().getSignature()) + ": " + LOGGER.debug("Error during transaction validation, tx " + Base58.encode(transaction.getTransactionData().getSignature()) + ": " + validationResult.name()); return ValidationResult.TRANSACTION_INVALID; } diff --git a/src/main/java/org/qora/block/BlockGenerator.java b/src/main/java/org/qora/block/BlockGenerator.java index 4b71120d..54b757ba 100644 --- a/src/main/java/org/qora/block/BlockGenerator.java +++ b/src/main/java/org/qora/block/BlockGenerator.java @@ -147,9 +147,21 @@ public class BlockGenerator extends Thread { repository.discardChanges(); // Attempt to add transactions until block is full, or we run out - for (TransactionData transactionData : unconfirmedTransactions) + // If a transaction makes the block invalid then skip it and it'll either expire or be in next block. + for (TransactionData transactionData : unconfirmedTransactions) { if (!newBlock.addTransaction(transactionData)) break; + + // Sign to create block's signature + newBlock.sign(); + + // If newBlock is no longer valid then we can't use transaction + ValidationResult validationResult = newBlock.isValid(); + if (validationResult != ValidationResult.OK) { + LOGGER.debug("Skipping invalid transaction " + Base58.encode(transactionData.getSignature()) + " during block generation"); + newBlock.deleteTransaction(transactionData); + } + } } public void shutdown() { diff --git a/src/main/java/org/qora/data/naming/NameData.java b/src/main/java/org/qora/data/naming/NameData.java index c8dedf11..8bf3b075 100644 --- a/src/main/java/org/qora/data/naming/NameData.java +++ b/src/main/java/org/qora/data/naming/NameData.java @@ -10,7 +10,6 @@ import javax.xml.bind.annotation.XmlAccessorType; public class NameData { // Properties - private byte[] registrantPublicKey; private String owner; private String name; private String data; @@ -26,9 +25,8 @@ public class NameData { protected NameData() { } - public NameData(byte[] registrantPublicKey, String owner, String name, String data, long registered, Long updated, byte[] reference, boolean isForSale, + public NameData(String owner, String name, String data, long registered, Long updated, byte[] reference, boolean isForSale, BigDecimal salePrice) { - this.registrantPublicKey = registrantPublicKey; this.owner = owner; this.name = name; this.data = data; @@ -39,16 +37,12 @@ public class NameData { this.salePrice = salePrice; } - public NameData(byte[] registrantPublicKey, String owner, String name, String data, long registered, byte[] reference) { - this(registrantPublicKey, owner, name, data, registered, null, reference, false, null); + public NameData(String owner, String name, String data, long registered, byte[] reference) { + this(owner, name, data, registered, null, reference, false, null); } // Getters / setters - public byte[] getRegistrantPublicKey() { - return this.registrantPublicKey; - } - public String getOwner() { return this.owner; } diff --git a/src/main/java/org/qora/data/transaction/UpdateNameTransactionData.java b/src/main/java/org/qora/data/transaction/UpdateNameTransactionData.java index 815a8bfa..f4bb0e06 100644 --- a/src/main/java/org/qora/data/transaction/UpdateNameTransactionData.java +++ b/src/main/java/org/qora/data/transaction/UpdateNameTransactionData.java @@ -4,6 +4,7 @@ import java.math.BigDecimal; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlTransient; import org.qora.transaction.Transaction.TransactionType; @@ -15,16 +16,24 @@ import io.swagger.v3.oas.annotations.media.Schema; public class UpdateNameTransactionData extends TransactionData { // Properties + @Schema(description = "owner's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") private byte[] ownerPublicKey; + @Schema(description = "new owner's address", example = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v") private String newOwner; + @Schema(description = "which name to update", example = "my-name") private String name; + @Schema(description = "replacement simple name-related info in JSON format", example = "{ \"age\": 30 }") private String newData; + // For internal use when orphaning + @XmlTransient + @Schema(hidden = true) private byte[] nameReference; // Constructors // For JAX-RS protected UpdateNameTransactionData() { + super(TransactionType.UPDATE_NAME); } public UpdateNameTransactionData(byte[] ownerPublicKey, String newOwner, String name, String newData, byte[] nameReference, BigDecimal fee, long timestamp, diff --git a/src/main/java/org/qora/naming/Name.java b/src/main/java/org/qora/naming/Name.java index 11903089..fb419a3a 100644 --- a/src/main/java/org/qora/naming/Name.java +++ b/src/main/java/org/qora/naming/Name.java @@ -33,7 +33,7 @@ public class Name { */ public Name(Repository repository, RegisterNameTransactionData registerNameTransactionData) { this.repository = repository; - this.nameData = new NameData(registerNameTransactionData.getRegistrantPublicKey(), registerNameTransactionData.getOwner(), + this.nameData = new NameData(registerNameTransactionData.getOwner(), registerNameTransactionData.getName(), registerNameTransactionData.getData(), registerNameTransactionData.getTimestamp(), registerNameTransactionData.getSignature()); } diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java index b13f08d1..b9dd6401 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -184,7 +184,7 @@ public class HSQLDBDatabaseUpdates { case 6: // Update Name Transactions stmt.execute("CREATE TABLE UpdateNameTransactions (signature Signature, owner QoraPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "new_owner QoraAddress NOT NULL, new_data NameData NOT NULL, name_reference Signature NOT NULL, " + + "new_owner QoraAddress NOT NULL, new_data NameData NOT NULL, name_reference Signature, " + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); break; @@ -203,7 +203,7 @@ public class HSQLDBDatabaseUpdates { case 9: // Buy Name Transactions stmt.execute("CREATE TABLE BuyNameTransactions (signature Signature, buyer QoraPublicKey NOT NULL, name RegisteredName NOT NULL, " - + "seller QoraAddress NOT NULL, amount QoraAmount NOT NULL, name_reference Signature NOT NULL, " + + "seller QoraAddress NOT NULL, amount QoraAmount NOT NULL, name_reference Signature, " + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); break; @@ -357,7 +357,7 @@ public class HSQLDBDatabaseUpdates { case 26: // Registered Names stmt.execute( - "CREATE TABLE Names (name RegisteredName, data VARCHAR(4000) NOT NULL, registrant QoraPublicKey NOT NULL, owner QoraAddress NOT NULL, " + "CREATE TABLE Names (name RegisteredName, data VARCHAR(4000) NOT NULL, owner QoraAddress NOT NULL, " + "registered TIMESTAMP WITH TIME ZONE NOT NULL, updated TIMESTAMP WITH TIME ZONE, reference Signature, is_for_sale BOOLEAN NOT NULL, sale_price QoraAmount, " + "PRIMARY KEY (name))"); break; @@ -389,6 +389,15 @@ public class HSQLDBDatabaseUpdates { stmt.execute("CREATE INDEX ATTransactionsIndex on ATTransactions (AT_address)"); break; + case 28: + // XXX TEMP fix until database rebuild + // Allow name_reference to be NULL while transaction is unconfirmed + stmt.execute("ALTER TABLE UpdateNameTransactions ALTER COLUMN name_reference SET NULL"); + stmt.execute("ALTER TABLE BuyNameTransactions ALTER COLUMN name_reference SET NULL"); + // Names.registrant shouldn't be there + stmt.execute("ALTER TABLE Names DROP COLUMN registrant"); + break; + default: // nothing to do return false; diff --git a/src/main/java/org/qora/repository/hsqldb/HSQLDBNameRepository.java b/src/main/java/org/qora/repository/hsqldb/HSQLDBNameRepository.java index bf183d49..b5dbb2e5 100644 --- a/src/main/java/org/qora/repository/hsqldb/HSQLDBNameRepository.java +++ b/src/main/java/org/qora/repository/hsqldb/HSQLDBNameRepository.java @@ -23,24 +23,23 @@ public class HSQLDBNameRepository implements NameRepository { @Override public NameData fromName(String name) throws DataException { try (ResultSet resultSet = this.repository - .checkedExecute("SELECT registrant, owner, data, registered, updated, reference, is_for_sale, sale_price FROM Names WHERE name = ?", name)) { + .checkedExecute("SELECT owner, data, registered, updated, reference, is_for_sale, sale_price FROM Names WHERE name = ?", name)) { if (resultSet == null) return null; - byte[] registrantPublicKey = resultSet.getBytes(1); - String owner = resultSet.getString(2); - String data = resultSet.getString(3); - long registered = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + String owner = resultSet.getString(1); + String data = resultSet.getString(2); + long registered = resultSet.getTimestamp(3, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); // Special handling for possibly-NULL "updated" column - Timestamp updatedTimestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)); + Timestamp updatedTimestamp = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)); Long updated = updatedTimestamp == null ? null : updatedTimestamp.getTime(); - byte[] reference = resultSet.getBytes(6); - boolean isForSale = resultSet.getBoolean(7); - BigDecimal salePrice = resultSet.getBigDecimal(8); + byte[] reference = resultSet.getBytes(5); + boolean isForSale = resultSet.getBoolean(6); + BigDecimal salePrice = resultSet.getBigDecimal(7); - return new NameData(registrantPublicKey, owner, name, data, registered, updated, reference, isForSale, salePrice); + return new NameData(owner, name, data, registered, updated, reference, isForSale, salePrice); } catch (SQLException e) { throw new DataException("Unable to fetch name info from repository", e); } @@ -60,26 +59,25 @@ public class HSQLDBNameRepository implements NameRepository { List names = new ArrayList<>(); try (ResultSet resultSet = this.repository - .checkedExecute("SELECT name, data, registrant, owner, registered, updated, reference, is_for_sale, sale_price FROM Names")) { + .checkedExecute("SELECT name, data, owner, registered, updated, reference, is_for_sale, sale_price FROM Names")) { if (resultSet == null) return names; do { String name = resultSet.getString(1); String data = resultSet.getString(2); - byte[] registrantPublicKey = resultSet.getBytes(3); - String owner = resultSet.getString(4); - long registered = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + String owner = resultSet.getString(3); + long registered = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); // Special handling for possibly-NULL "updated" column - Timestamp updatedTimestamp = resultSet.getTimestamp(6, Calendar.getInstance(HSQLDBRepository.UTC)); + Timestamp updatedTimestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)); Long updated = updatedTimestamp == null ? null : updatedTimestamp.getTime(); - byte[] reference = resultSet.getBytes(7); - boolean isForSale = resultSet.getBoolean(8); - BigDecimal salePrice = resultSet.getBigDecimal(9); + byte[] reference = resultSet.getBytes(6); + boolean isForSale = resultSet.getBoolean(7); + BigDecimal salePrice = resultSet.getBigDecimal(8); - names.add(new NameData(registrantPublicKey, owner, name, data, registered, updated, reference, isForSale, salePrice)); + names.add(new NameData(owner, name, data, registered, updated, reference, isForSale, salePrice)); } while (resultSet.next()); return names; @@ -93,25 +91,24 @@ public class HSQLDBNameRepository implements NameRepository { List names = new ArrayList<>(); try (ResultSet resultSet = this.repository - .checkedExecute("SELECT name, data, registrant, registered, updated, reference, is_for_sale, sale_price FROM Names WHERE owner = ?", owner)) { + .checkedExecute("SELECT name, data, registered, updated, reference, is_for_sale, sale_price FROM Names WHERE owner = ?", owner)) { if (resultSet == null) return names; do { String name = resultSet.getString(1); String data = resultSet.getString(2); - byte[] registrantPublicKey = resultSet.getBytes(3); - long registered = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long registered = resultSet.getTimestamp(3, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); // Special handling for possibly-NULL "updated" column - Timestamp updatedTimestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC)); + Timestamp updatedTimestamp = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)); Long updated = updatedTimestamp == null ? null : updatedTimestamp.getTime(); - byte[] reference = resultSet.getBytes(6); - boolean isForSale = resultSet.getBoolean(7); - BigDecimal salePrice = resultSet.getBigDecimal(8); + byte[] reference = resultSet.getBytes(5); + boolean isForSale = resultSet.getBoolean(6); + BigDecimal salePrice = resultSet.getBigDecimal(7); - names.add(new NameData(registrantPublicKey, owner, name, data, registered, updated, reference, isForSale, salePrice)); + names.add(new NameData(owner, name, data, registered, updated, reference, isForSale, salePrice)); } while (resultSet.next()); return names; @@ -128,7 +125,7 @@ public class HSQLDBNameRepository implements NameRepository { Long updated = nameData.getUpdated(); Timestamp updatedTimestamp = updated == null ? null : new Timestamp(updated); - saveHelper.bind("registrant", nameData.getRegistrantPublicKey()).bind("owner", nameData.getOwner()).bind("name", nameData.getName()) + saveHelper.bind("owner", nameData.getOwner()).bind("name", nameData.getName()) .bind("data", nameData.getData()).bind("registered", new Timestamp(nameData.getRegistered())).bind("updated", updatedTimestamp) .bind("reference", nameData.getReference()).bind("is_for_sale", nameData.getIsForSale()).bind("sale_price", nameData.getSalePrice()); diff --git a/src/main/java/org/qora/transaction/Transaction.java b/src/main/java/org/qora/transaction/Transaction.java index ae975cea..6a0bb2ba 100644 --- a/src/main/java/org/qora/transaction/Transaction.java +++ b/src/main/java/org/qora/transaction/Transaction.java @@ -441,7 +441,7 @@ public abstract class Transaction { public ValidationResult isValidUnconfirmed() throws DataException { // Transactions with a timestamp prior to latest block's timestamp are too old BlockData latestBlock = repository.getBlockRepository().getLastBlock(); - if (this.transactionData.getTimestamp() <= latestBlock.getTimestamp()) + if (this.getDeadline() <= latestBlock.getTimestamp()) return ValidationResult.TIMESTAMP_TOO_OLD; // Transactions with a timestamp too far into future are too new diff --git a/src/main/java/org/qora/transaction/UpdateNameTransaction.java b/src/main/java/org/qora/transaction/UpdateNameTransaction.java index cb9b9661..45cb2d94 100644 --- a/src/main/java/org/qora/transaction/UpdateNameTransaction.java +++ b/src/main/java/org/qora/transaction/UpdateNameTransaction.java @@ -29,6 +29,10 @@ public class UpdateNameTransaction extends Transaction { super(repository, transactionData); this.updateNameTransactionData = (UpdateNameTransactionData) this.transactionData; + + // XXX This is horrible - thanks to JAXB unmarshalling not calling constructor + if (this.transactionData.getCreatorPublicKey() == null) + this.transactionData.setCreatorPublicKey(this.updateNameTransactionData.getOwnerPublicKey()); } // More information