API + new tx restrictions

Added GET /names to list all registered name.
Added GET /names/{name} for more info on a specific name.
Added GET /names/address/{address} for names owned by address.

Renamed GET /assets/all to GET /assets in line with above.

Fixed edge cases with AnnotationPostProcessor.

Fixed incorrectly exposed "blockHeight" in API UI examples/values.
Changed example transaction timestamp.

Added checks on building/signing/processing new transactions via API
so that they are not too old (older than latest block's timestamp),
too new (more than 24 hours in the future) or the tx creator doesn't
already have a lot of existing unconfirmed transactions (default 100).

Configurable via settings.json properties maxUnconfirmedPerAccount
and maxTransactionTimestampFuture.

Improved /transactions/search to not return unconfirmed transactions
and to order by timestamp.

Transaction.getCreator() now returns PublicKeyAccount, not Account.
This commit is contained in:
catbref 2019-01-08 17:30:23 +00:00
parent 7998166c0a
commit 95d640cc8c
15 changed files with 290 additions and 33 deletions

View File

@ -0,0 +1,31 @@
package org.qora.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import org.qora.data.naming.NameData;
@XmlAccessorType(XmlAccessType.NONE)
public class NameSummary {
private NameData nameData;
protected NameSummary() {
}
public NameSummary(NameData nameData) {
this.nameData = nameData;
}
@XmlElement(name = "name")
public String getName() {
return this.nameData.getName();
}
@XmlElement(name = "owner")
public String getOwner() {
return this.nameData.getOwner();
}
}

View File

@ -79,7 +79,7 @@ public class AnnotationPostProcessor implements ReaderListener {
private PathItem getPathItemFromMethod(OpenAPI openAPI, String classPathString, Method method) { private PathItem getPathItemFromMethod(OpenAPI openAPI, String classPathString, Method method) {
Path path = method.getAnnotation(Path.class); Path path = method.getAnnotation(Path.class);
if (path == null) if (path == null)
throw new RuntimeException("API method has no @Path annotation?"); return openAPI.getPaths().get(classPathString);
String pathString = path.value(); String pathString = path.value();
return openAPI.getPaths().get(classPathString + pathString); return openAPI.getPaths().get(classPathString + pathString);

View File

@ -55,7 +55,6 @@ public class AssetsResource {
HttpServletRequest request; HttpServletRequest request;
@GET @GET
@Path("/all")
@Operation( @Operation(
summary = "List all known assets", summary = "List all known assets",
responses = { responses = {

View File

@ -1,22 +1,33 @@
package org.qora.api.resource; package org.qora.api.resource;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import org.qora.api.ApiError; import org.qora.api.ApiError;
import org.qora.api.ApiErrors; import org.qora.api.ApiErrors;
import org.qora.api.ApiExceptionFactory; import org.qora.api.ApiExceptionFactory;
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.RegisterNameTransactionData;
import org.qora.repository.DataException; import org.qora.repository.DataException;
import org.qora.repository.Repository; import org.qora.repository.Repository;
@ -28,13 +39,94 @@ import org.qora.transform.transaction.RegisterNameTransactionTransformer;
import org.qora.utils.Base58; import org.qora.utils.Base58;
@Path("/names") @Path("/names")
@Produces({ MediaType.TEXT_PLAIN}) @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@Tag(name = "Names") @Tag(name = "Names")
public class NamesResource { public class NamesResource {
@Context @Context
HttpServletRequest request; HttpServletRequest request;
@GET
@Operation(
summary = "List all registered names",
responses = {
@ApiResponse(
description = "registered name info",
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
array = @ArraySchema(schema = @Schema(implementation = NameSummary.class))
)
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public List<NameSummary> getAllNames(@Parameter(ref = "limit") @QueryParam("limit") int limit, @Parameter(ref = "offset") @QueryParam("offset") int offset) {
try (final Repository repository = RepositoryManager.getRepository()) {
List<NameData> names = repository.getNameRepository().getAllNames();
// Pagination would take effect here (or as part of the repository access)
int fromIndex = Integer.min(offset, names.size());
int toIndex = limit == 0 ? names.size() : Integer.min(fromIndex + limit, names.size());
names = names.subList(fromIndex, toIndex);
return names.stream().map(nameData -> new NameSummary(nameData)).collect(Collectors.toList());
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/address/{address}")
@Operation(
summary = "List all names owned by address",
responses = {
@ApiResponse(
description = "registered name info",
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
array = @ArraySchema(schema = @Schema(implementation = NameSummary.class))
)
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public List<NameSummary> getNamesByAddress(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
List<NameData> names = repository.getNameRepository().getNamesByOwner(address);
return names.stream().map(nameData -> new NameSummary(nameData)).collect(Collectors.toList());
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@GET
@Path("/{name}")
@Operation(
summary = "Info on registered name",
responses = {
@ApiResponse(
description = "registered name info",
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(implementation = NameData.class)
)
)
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public NameData getName(@PathParam("name") String name) {
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getNameRepository().fromName(name);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST @POST
@Path("/register") @Path("/register")
@Operation( @Operation(

View File

@ -2,6 +2,11 @@ package org.qora.data.naming;
import java.math.BigDecimal; import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
// All properties to be converted to JSON via JAX-RS
@XmlAccessorType(XmlAccessType.FIELD)
public class NameData { public class NameData {
// Properties // Properties
@ -17,6 +22,10 @@ public class NameData {
// Constructors // Constructors
// necessary for JAX-RS serialization
protected NameData() {
}
public NameData(byte[] registrantPublicKey, String owner, String name, String data, long registered, Long updated, byte[] reference, boolean isForSale, public NameData(byte[] registrantPublicKey, String owner, String name, String data, long registered, Long updated, byte[] reference, boolean isForSale,
BigDecimal salePrice) { BigDecimal salePrice) {
this.registrantPublicKey = registrantPublicKey; this.registrantPublicKey = registrantPublicKey;

View File

@ -41,7 +41,7 @@ public abstract class TransactionData {
@XmlTransient // represented in transaction-specific properties @XmlTransient // represented in transaction-specific properties
@Schema(hidden = true) @Schema(hidden = true)
protected byte[] creatorPublicKey; protected byte[] creatorPublicKey;
@Schema(description = "timestamp when transaction created, in milliseconds since unix epoch", example = "1545062012000") @Schema(description = "timestamp when transaction created, in milliseconds since unix epoch", example = "__unix_epoch_time_milliseconds__")
protected long timestamp; protected long timestamp;
@Schema(description = "sender's last transaction ID", example = "real_transaction_reference_in_base58") @Schema(description = "sender's last transaction ID", example = "real_transaction_reference_in_base58")
protected byte[] reference; protected byte[] reference;
@ -51,7 +51,7 @@ public abstract class TransactionData {
protected byte[] signature; protected byte[] signature;
// For JAX-RS use // For JAX-RS use
@Schema(accessMode = AccessMode.READ_ONLY, description = "height of block containing transaction") @Schema(accessMode = AccessMode.READ_ONLY, hidden = true, description = "height of block containing transaction")
protected Integer blockHeight; protected Integer blockHeight;
// Constructors // Constructors
@ -120,6 +120,7 @@ public abstract class TransactionData {
this.creatorPublicKey = creatorPublicKey; this.creatorPublicKey = creatorPublicKey;
} }
@XmlTransient
public void setBlockHeight(int blockHeight) { public void setBlockHeight(int blockHeight) {
this.blockHeight = blockHeight; this.blockHeight = blockHeight;
} }

View File

@ -1,5 +1,7 @@
package org.qora.repository; package org.qora.repository;
import java.util.List;
import org.qora.data.naming.NameData; import org.qora.data.naming.NameData;
public interface NameRepository { public interface NameRepository {
@ -8,6 +10,10 @@ public interface NameRepository {
public boolean nameExists(String name) throws DataException; public boolean nameExists(String name) throws DataException;
public List<NameData> getAllNames() throws DataException;
public List<NameData> getNamesByOwner(String address) throws DataException;
public void save(NameData nameData) throws DataException; public void save(NameData nameData) throws DataException;
public void delete(String name) throws DataException; public void delete(String name) throws DataException;

View File

@ -4,7 +4,9 @@ import java.math.BigDecimal;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Timestamp; import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.List;
import org.qora.data.naming.NameData; import org.qora.data.naming.NameData;
import org.qora.repository.DataException; import org.qora.repository.DataException;
@ -53,6 +55,71 @@ public class HSQLDBNameRepository implements NameRepository {
} }
} }
@Override
public List<NameData> getAllNames() throws DataException {
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")) {
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();
// Special handling for possibly-NULL "updated" column
Timestamp updatedTimestamp = resultSet.getTimestamp(6, 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);
names.add(new NameData(registrantPublicKey, owner, name, data, registered, updated, reference, isForSale, salePrice));
} while (resultSet.next());
return names;
} catch (SQLException e) {
throw new DataException("Unable to fetch names from repository", e);
}
}
@Override
public List<NameData> getNamesByOwner(String owner) throws DataException {
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)) {
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();
// Special handling for possibly-NULL "updated" column
Timestamp updatedTimestamp = resultSet.getTimestamp(5, 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);
names.add(new NameData(registrantPublicKey, owner, name, data, registered, updated, reference, isForSale, salePrice));
} while (resultSet.next());
return names;
} catch (SQLException e) {
throw new DataException("Unable to fetch account's names from repository", e);
}
}
@Override @Override
public void save(NameData nameData) throws DataException { public void save(NameData nameData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Names"); HSQLDBSaver saveHelper = new HSQLDBSaver("Names");

View File

@ -8,6 +8,8 @@ import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.List; import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qora.data.PaymentData; import org.qora.data.PaymentData;
import org.qora.data.transaction.TransactionData; import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException; import org.qora.repository.DataException;
@ -18,6 +20,8 @@ import org.qora.transaction.Transaction.TransactionType;
public class HSQLDBTransactionRepository implements TransactionRepository { public class HSQLDBTransactionRepository implements TransactionRepository {
private static final Logger LOGGER = LogManager.getLogger(HSQLDBTransactionRepository.class);
protected HSQLDBRepository repository; protected HSQLDBRepository repository;
private HSQLDBGenesisTransactionRepository genesisTransactionRepository; private HSQLDBGenesisTransactionRepository genesisTransactionRepository;
private HSQLDBPaymentTransactionRepository paymentTransactionRepository; private HSQLDBPaymentTransactionRepository paymentTransactionRepository;
@ -314,34 +318,24 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
String signatureColumn = "NULL"; String signatureColumn = "NULL";
List<Object> bindParams = new ArrayList<Object>(); List<Object> bindParams = new ArrayList<Object>();
String groupBy = "";
// Table JOINs first // Table JOINs first
List<String> tableJoins = new ArrayList<String>(); List<String> tableJoins = new ArrayList<String>();
if (hasHeightRange) { // Always JOIN BlockTransactions as we only ever want confirmed transactions
tableJoins.add("Blocks"); tableJoins.add("Blocks");
tableJoins.add("BlockTransactions ON BlockTransactions.block_signature = Blocks.signature"); tableJoins.add("BlockTransactions ON BlockTransactions.block_signature = Blocks.signature");
signatureColumn = "BlockTransactions.transaction_signature"; signatureColumn = "BlockTransactions.transaction_signature";
}
if (hasTxType) { // Always JOIN Transactions as we want to order by timestamp
if (hasHeightRange)
tableJoins.add("Transactions ON Transactions.signature = BlockTransactions.transaction_signature"); tableJoins.add("Transactions ON Transactions.signature = BlockTransactions.transaction_signature");
else
tableJoins.add("Transactions");
signatureColumn = "Transactions.signature"; signatureColumn = "Transactions.signature";
}
if (hasAddress) { if (hasAddress) {
if (hasTxType)
tableJoins.add("TransactionParticipants ON TransactionParticipants.signature = Transactions.signature"); tableJoins.add("TransactionParticipants ON TransactionParticipants.signature = Transactions.signature");
else if (hasHeightRange)
tableJoins.add("TransactionParticipants ON TransactionParticipants.signature = BlockTransactions.transaction_signature");
else
tableJoins.add("TransactionParticipants");
signatureColumn = "TransactionParticipants.signature"; signatureColumn = "TransactionParticipants.signature";
groupBy = " GROUP BY TransactionParticipants.signature, Transactions.creation";
} }
// WHERE clauses next // WHERE clauses next
@ -362,7 +356,8 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
bindParams.add(address); bindParams.add(address);
} }
String sql = "SELECT " + signatureColumn + " FROM " + String.join(" JOIN ", tableJoins) + " WHERE " + String.join(" AND ", whereClauses); String sql = "SELECT " + signatureColumn + " FROM " + String.join(" JOIN ", tableJoins) + " WHERE " + String.join(" AND ", whereClauses) + groupBy + " ORDER BY Transactions.creation ASC";
LOGGER.trace(sql);
try (ResultSet resultSet = this.repository.checkedExecute(sql, bindParams.toArray())) { try (ResultSet resultSet = this.repository.checkedExecute(sql, bindParams.toArray())) {
if (resultSet == null) if (resultSet == null)

View File

@ -28,6 +28,10 @@ public class Settings {
private boolean useBitcoinTestNet = false; private boolean useBitcoinTestNet = false;
private boolean wipeUnconfirmedOnStart = true; private boolean wipeUnconfirmedOnStart = true;
private String blockchainConfigPath = "blockchain.json"; private String blockchainConfigPath = "blockchain.json";
/** Maximum number of unconfirmed transactions allowed per account */
private int maxUnconfirmedPerAccount = 100;
/** Max milliseconds into future for accepting new, unconfirmed transactions */
private long maxTransactionTimestampFuture = 24 * 60 * 60 * 1000; // milliseconds
// RPC // RPC
private int rpcPort = 9085; private int rpcPort = 9085;
@ -131,11 +135,19 @@ public class Settings {
if (json.containsKey("rpcenabled")) if (json.containsKey("rpcenabled"))
this.rpcEnabled = ((Boolean) json.get("rpcenabled")).booleanValue(); this.rpcEnabled = ((Boolean) json.get("rpcenabled")).booleanValue();
// Blockchain config // Node-specific behaviour
if (json.containsKey("wipeUnconfirmedOnStart")) if (json.containsKey("wipeUnconfirmedOnStart"))
this.wipeUnconfirmedOnStart = (Boolean) getTypedJson(json, "wipeUnconfirmedOnStart", Boolean.class); this.wipeUnconfirmedOnStart = (Boolean) getTypedJson(json, "wipeUnconfirmedOnStart", Boolean.class);
if (json.containsKey("maxUnconfirmedPerAccount"))
this.maxUnconfirmedPerAccount = ((Long) getTypedJson(json, "maxUnconfirmedPerAccount", Long.class)).intValue();
if (json.containsKey("maxTransactionTimestampFuture"))
this.maxTransactionTimestampFuture = (Long) getTypedJson(json, "maxTransactionTimestampFuture", Long.class);
// Blockchain config
if (json.containsKey("blockchainConfig")) if (json.containsKey("blockchainConfig"))
blockchainConfigPath = (String) getTypedJson(json, "blockchainConfig", String.class); blockchainConfigPath = (String) getTypedJson(json, "blockchainConfig", String.class);
@ -182,6 +194,14 @@ public class Settings {
return this.wipeUnconfirmedOnStart; return this.wipeUnconfirmedOnStart;
} }
public int getMaxUnconfirmedPerAccount() {
return this.maxUnconfirmedPerAccount;
}
public long getMaxTransactionTimestampFuture() {
return this.maxTransactionTimestampFuture;
}
// Config parsing // Config parsing
public static Object getTypedJson(JSONObject json, String key, Class<?> clazz) { public static Object getTypedJson(JSONObject json, String key, Class<?> clazz) {

View File

@ -55,7 +55,7 @@ public class CancelOrderTransaction extends Transaction {
// Navigation // Navigation
@Override @Override
public Account getCreator() throws DataException { public PublicKeyAccount getCreator() throws DataException {
return new PublicKeyAccount(this.repository, cancelOrderTransactionData.getCreatorPublicKey()); return new PublicKeyAccount(this.repository, cancelOrderTransactionData.getCreatorPublicKey());
} }

View File

@ -56,7 +56,7 @@ public class CreateOrderTransaction extends Transaction {
// Navigation // Navigation
@Override @Override
public Account getCreator() throws DataException { public PublicKeyAccount getCreator() throws DataException {
return new PublicKeyAccount(this.repository, createOrderTransactionData.getCreatorPublicKey()); return new PublicKeyAccount(this.repository, createOrderTransactionData.getCreatorPublicKey());
} }

View File

@ -67,7 +67,7 @@ public class CreatePollTransaction extends Transaction {
// Navigation // Navigation
@Override @Override
public Account getCreator() throws DataException { public PublicKeyAccount getCreator() throws DataException {
return new PublicKeyAccount(this.repository, this.createPollTransactionData.getCreatorPublicKey()); return new PublicKeyAccount(this.repository, this.createPollTransactionData.getCreatorPublicKey());
} }

View File

@ -4,6 +4,7 @@ import java.math.BigDecimal;
import java.math.BigInteger; import java.math.BigInteger;
import java.math.MathContext; import java.math.MathContext;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -18,6 +19,7 @@ import org.qora.data.block.BlockData;
import org.qora.data.transaction.TransactionData; import org.qora.data.transaction.TransactionData;
import org.qora.repository.DataException; import org.qora.repository.DataException;
import org.qora.repository.Repository; import org.qora.repository.Repository;
import org.qora.settings.Settings;
import org.qora.transform.TransformationException; import org.qora.transform.TransformationException;
import org.qora.transform.transaction.TransactionTransformer; import org.qora.transform.transaction.TransactionTransformer;
import org.qora.utils.Base58; import org.qora.utils.Base58;
@ -109,6 +111,9 @@ public abstract class Transaction {
ASSET_DOES_NOT_MATCH_AT(41), ASSET_DOES_NOT_MATCH_AT(41),
ASSET_ALREADY_EXISTS(43), ASSET_ALREADY_EXISTS(43),
MISSING_CREATOR(44), MISSING_CREATOR(44),
TIMESTAMP_TOO_OLD(45),
TIMESTAMP_TOO_NEW(46),
TOO_MANY_UNCONFIRMED(47),
NOT_YET_RELEASED(1000); NOT_YET_RELEASED(1000);
public final int value; public final int value;
@ -364,7 +369,7 @@ public abstract class Transaction {
* @return creator * @return creator
* @throws DataException * @throws DataException
*/ */
protected Account getCreator() throws DataException { protected PublicKeyAccount getCreator() throws DataException {
if (this.transactionData.getCreatorPublicKey() == null) if (this.transactionData.getCreatorPublicKey() == null)
return null; return null;
@ -434,18 +439,49 @@ public abstract class Transaction {
* @throws DataException * @throws DataException
*/ */
public ValidationResult isValidUnconfirmed() throws DataException { 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())
return ValidationResult.TIMESTAMP_TOO_OLD;
// Transactions with a timestamp too far into future are too new
long maxTimestamp = NTP.getTime() + Settings.getInstance().getMaxTransactionTimestampFuture();
if (this.transactionData.getTimestamp() > maxTimestamp)
return ValidationResult.TIMESTAMP_TOO_NEW;
try { try {
Account creator = this.getCreator(); PublicKeyAccount creator = this.getCreator();
if (creator == null) if (creator == null)
return ValidationResult.MISSING_CREATOR; return ValidationResult.MISSING_CREATOR;
creator.setLastReference(creator.getUnconfirmedLastReference()); creator.setLastReference(creator.getUnconfirmedLastReference());
return this.isValid(); ValidationResult result = this.isValid();
// Reject if unconfirmed pile already has X transactions from same creator
if (result == ValidationResult.OK && countUnconfirmedByCreator(creator) >= Settings.getInstance().getMaxUnconfirmedPerAccount())
return ValidationResult.TOO_MANY_UNCONFIRMED;
return result;
} finally { } finally {
repository.discardChanges(); repository.discardChanges();
} }
} }
private int countUnconfirmedByCreator(PublicKeyAccount creator) throws DataException {
List<TransactionData> unconfirmedTransactions = repository.getTransactionRepository().getAllUnconfirmedTransactions();
int count = 0;
for (TransactionData transactionData : unconfirmedTransactions) {
Transaction transaction = Transaction.fromData(repository, transactionData);
PublicKeyAccount otherCreator = transaction.getCreator();
if (Arrays.equals(creator.getPublicKey(), otherCreator.getPublicKey()))
++count;
}
return count;
}
/** /**
* Returns sorted, unconfirmed transactions, deleting invalid. * Returns sorted, unconfirmed transactions, deleting invalid.
* <p> * <p>

View File

@ -16,6 +16,7 @@ public final class NTP {
private static long lastUpdate = 0; private static long lastUpdate = 0;
private static long offset = 0; private static long offset = 0;
/** Returns NTP-synced current time from unix epoch, in milliseconds. */
public static long getTime() { public static long getTime() {
// Every so often use NTP to find out offset between this system's time and internet time // Every so often use NTP to find out offset between this system's time and internet time
if (System.currentTimeMillis() > lastUpdate + TIME_TILL_UPDATE) { if (System.currentTimeMillis() > lastUpdate + TIME_TILL_UPDATE) {