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).
This commit is contained in:
catbref 2019-01-09 14:41:49 +00:00
parent 95d640cc8c
commit 22c87a6e08
10 changed files with 158 additions and 47 deletions

View File

@ -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);
}
}
}

View File

@ -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.
* <p>
* Used when constructing a new block during forging.
* <p>
* 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.
* <p>
@ -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;
}

View File

@ -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() {

View File

@ -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;
}

View File

@ -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,

View File

@ -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());
}

View File

@ -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;

View File

@ -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<NameData> 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<NameData> 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());

View File

@ -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

View File

@ -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