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