HSQLDB issue, resource leak prevention, v1 differences

HSQLDB v2.4.0 had some issue with non-padded, case-insensitive string comparisons.
This is fixed in svn r5836-ish of HSQLDB but yet to be pushed out to new HSQLDB release.
So this commit includes hsqldb-r5836.jar and modified pom.xml/.classpath for now.

No need for duplicate, hidden creatorPublicKey in CancelOrderTransactionData,
CreateOrderTransactionData and CreatePollTransactionData.

Various changes to use more try-with-resources, especially with JDBC objects like
Connection, Statement, PreparedStatement, ResultSet.

Added loads of missing @Override annotations.

Fixed bug in Asset exchange order matching where the matching logic loop
would incorrectly adjust temporary amount fulfilled
by the "want" asset amount (in matchedAmount)
instead of the "have" asset amount (in tradePrice).

Disabled check for duplicate asset name in IssueAssetTransactions for old v1 transactions.

In HSQLDB repository we now use ResultSet.getTimestamp(index, UTC-calendar) to make sure we
only store/fetch UTC timestamps. The UTC-calendar is made using static final TimeZone called
HSQLDBRepository.UTC.

To keep asset IDs in line with v1, Assets.asset_id values are generated on-the-fly in HSQLDB
using a "before insert" trigger on Assets table. Corresponding code
calling HSQLDBRepository.callIdentity() replaced with SELECT statement instead.

Moved most of the HSQLDB connection properties from the connection URL to explicit code in
HSQLDBRepositoryFactory.

Fixed incorrect 'amount' lengths in PaymentTransformer, as used by MultiPayment and Arbitrary
transaction types.

Added support for mangled arbitrary transaction bytes when generating/verifying a v1 transaction signature.
In v1 Arbitrary transactions, bytes-for-signing are lost prior to final payment (but only if there are any payments).
Added corresponding code for multi-payment transactions in the same vein.
This commit is contained in:
catbref 2018-08-07 15:44:41 +01:00
parent 7da84b2b85
commit e56d8f5e02
55 changed files with 744 additions and 614 deletions

View File

@ -17,5 +17,6 @@
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/4"/>
<classpathentry kind="lib" path="lib/hsqldb-r5836.jar"/>
<classpathentry kind="output" path="target/classes"/>
</classpath>

BIN
lib/hsqldb-r5836.jar Normal file

Binary file not shown.

View File

@ -17,11 +17,6 @@
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId>

View File

@ -7,7 +7,6 @@ import qora.transaction.Transaction;
public class CancelOrderTransactionData extends TransactionData {
// Properties
private byte[] creatorPublicKey;
private byte[] orderId;
// Constructors
@ -15,7 +14,6 @@ public class CancelOrderTransactionData extends TransactionData {
public CancelOrderTransactionData(byte[] creatorPublicKey, byte[] orderId, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) {
super(Transaction.TransactionType.CANCEL_ASSET_ORDER, fee, creatorPublicKey, timestamp, reference, signature);
this.creatorPublicKey = creatorPublicKey;
this.orderId = orderId;
}
@ -25,10 +23,6 @@ public class CancelOrderTransactionData extends TransactionData {
// Getters/Setters
public byte[] getCreatorPublicKey() {
return this.creatorPublicKey;
}
public byte[] getOrderId() {
return this.orderId;
}

View File

@ -7,7 +7,6 @@ import qora.transaction.Transaction.TransactionType;
public class CreateOrderTransactionData extends TransactionData {
// Properties
private byte[] creatorPublicKey;
private long haveAssetId;
private long wantAssetId;
private BigDecimal amount;
@ -19,7 +18,6 @@ public class CreateOrderTransactionData extends TransactionData {
long timestamp, byte[] reference, byte[] signature) {
super(TransactionType.CREATE_ASSET_ORDER, fee, creatorPublicKey, timestamp, reference, signature);
this.creatorPublicKey = creatorPublicKey;
this.haveAssetId = haveAssetId;
this.wantAssetId = wantAssetId;
this.amount = amount;
@ -33,10 +31,6 @@ public class CreateOrderTransactionData extends TransactionData {
// Getters/Setters
public byte[] getCreatorPublicKey() {
return this.creatorPublicKey;
}
public long getHaveAssetId() {
return this.haveAssetId;
}

View File

@ -9,7 +9,6 @@ import qora.transaction.Transaction;
public class CreatePollTransactionData extends TransactionData {
// Properties
private byte[] creatorPublicKey;
private String owner;
private String pollName;
private String description;
@ -21,7 +20,6 @@ public class CreatePollTransactionData extends TransactionData {
BigDecimal fee, long timestamp, byte[] reference, byte[] signature) {
super(Transaction.TransactionType.CREATE_POLL, fee, creatorPublicKey, timestamp, reference, signature);
this.creatorPublicKey = creatorPublicKey;
this.owner = owner;
this.pollName = pollName;
this.description = description;
@ -35,10 +33,6 @@ public class CreatePollTransactionData extends TransactionData {
// Getters/setters
public byte[] getCreatorPublicKey() {
return this.creatorPublicKey;
}
public String getOwner() {
return this.owner;
}

View File

@ -44,21 +44,14 @@ public class migrate {
private static Map<String, byte[]> publicKeyByAddress = new HashMap<String, byte[]>();
public static Object fetchBlockJSON(int height) throws IOException {
InputStream is;
try {
is = new URL("http://localhost:9085/blocks/byheight/" + height).openStream();
try (InputStream is = new URL("http://localhost:9085/blocks/byheight/" + height).openStream();
InputStreamReader isr = new InputStreamReader(is, Charset.forName("UTF-8"));
BufferedReader reader = new BufferedReader(isr)) {
return JSONValue.parseWithException(reader);
} catch (IOException e) {
return null;
}
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(is, Charset.forName("UTF-8")));
return JSONValue.parseWithException(reader);
} catch (ParseException e) {
return null;
} finally {
is.close();
}
}
@ -69,8 +62,8 @@ public class migrate {
InputStream is = new URL("http://localhost:9085/addresses/publickey/" + address).openStream();
try {
String publicKey58 = CharStreams.toString(new InputStreamReader(is, Charset.forName("UTF-8")));
try (InputStreamReader isr = new InputStreamReader(is, Charset.forName("UTF-8"))) {
String publicKey58 = CharStreams.toString(isr);
byte[] publicKey = Base58.decode(publicKey58);
publicKeyByAddress.put(address, publicKey);
@ -81,14 +74,14 @@ public class migrate {
}
public static void savePublicKeys(Connection connection) throws SQLException {
PreparedStatement pStmt = connection.prepareStatement("INSERT IGNORE INTO Test_public_keys VALUES (?, ?)");
try (PreparedStatement pStmt = connection.prepareStatement("INSERT IGNORE INTO Test_public_keys VALUES (?, ?)")) {
for (Entry<String, byte[]> entry : publicKeyByAddress.entrySet()) {
pStmt.setString(1, entry.getKey());
pStmt.setBytes(2, entry.getValue());
pStmt.execute();
}
}
}
public static String formatWithPlaceholders(String... columns) {
String[] placeholders = new String[columns.length];
@ -103,6 +96,7 @@ public class migrate {
return output.toString();
}
@SuppressWarnings("resource")
public static void main(String args[]) throws SQLException, DataException, IOException {
// Genesis public key
publicKeyByAddress.put(GENESIS_ADDRESS, GENESIS_PUBLICKEY);
@ -132,14 +126,14 @@ public class migrate {
.prepareStatement("INSERT INTO PaymentTransactions " + formatWithPlaceholders("signature", "sender", "recipient", "amount"));
PreparedStatement registerNamePStmt = c
.prepareStatement("INSERT INTO RegisterNameTransactions " + formatWithPlaceholders("signature", "registrant", "name", "owner", "data"));
PreparedStatement updateNamePStmt = c
.prepareStatement("INSERT INTO UpdateNameTransactions " + formatWithPlaceholders("signature", "owner", "name", "new_owner", "new_data", "name_reference"));
PreparedStatement updateNamePStmt = c.prepareStatement(
"INSERT INTO UpdateNameTransactions " + formatWithPlaceholders("signature", "owner", "name", "new_owner", "new_data", "name_reference"));
PreparedStatement sellNamePStmt = c
.prepareStatement("INSERT INTO SellNameTransactions " + formatWithPlaceholders("signature", "owner", "name", "amount"));
PreparedStatement cancelSellNamePStmt = c
.prepareStatement("INSERT INTO CancelSellNameTransactions " + formatWithPlaceholders("signature", "owner", "name"));
PreparedStatement buyNamePStmt = c
.prepareStatement("INSERT INTO BuyNameTransactions " + formatWithPlaceholders("signature", "buyer", "name", "seller", "amount", "name_reference"));
PreparedStatement buyNamePStmt = c.prepareStatement(
"INSERT INTO BuyNameTransactions " + formatWithPlaceholders("signature", "buyer", "name", "seller", "amount", "name_reference"));
PreparedStatement createPollPStmt = c
.prepareStatement("INSERT INTO CreatePollTransactions " + formatWithPlaceholders("signature", "creator", "owner", "poll_name", "description"));
PreparedStatement createPollOptionPStmt = c

View File

@ -163,7 +163,7 @@ public class Order {
// Trade can go ahead!
// Calculate the total cost to us based on their price
// Calculate the total cost to us, in have-asset, based on their price
BigDecimal tradePrice = matchedAmount.multiply(theirOrderData.getPrice()).setScale(8);
// Construct trade
@ -174,7 +174,7 @@ public class Order {
trade.process();
// Update our order in terms of fulfilment, etc. but do not save into repository as that's handled by Trade above
this.orderData.setFulfilled(this.orderData.getFulfilled().add(matchedAmount));
this.orderData.setFulfilled(this.orderData.getFulfilled().add(tradePrice));
// Continue on to process other open orders in case we still have amount left to match
}

View File

@ -38,9 +38,11 @@ public class BlockChain {
private static final long ASSETS_RELEASE_TIMESTAMP = 0L; // From Qora epoch
private static final long VOTING_RELEASE_TIMESTAMP = 1403715600000L; // 2014-06-25T17:00:00+00:00
private static final long ARBITRARY_RELEASE_TIMESTAMP = 1405702800000L; // 2014-07-18T17:00:00+00:00
private static final long CREATE_POLL_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 CREATE POLL transactions
private static final long ISSUE_ASSET_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 ISSUE ASSET transactions
private static final long CREATE_ORDER_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 CREATE ORDER transactions
private static final long ARBITRARY_TRANSACTION_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 ARBITRARY transactions
/**
* Some sort start-up/initialization/checking method.
@ -164,4 +166,11 @@ public class BlockChain {
return CREATE_ORDER_V2_TIMESTAMP;
}
public static long getArbitraryTransactionV2Timestamp() {
if (Settings.getInstance().isTestNet())
return 0;
return ARBITRARY_TRANSACTION_V2_TIMESTAMP;
}
}

View File

@ -44,6 +44,7 @@ public class ArbitraryTransaction extends Transaction {
// More information
@Override
public List<Account> getRecipientAccounts() throws DataException {
List<Account> recipients = new ArrayList<Account>();
@ -54,6 +55,7 @@ public class ArbitraryTransaction extends Transaction {
return recipients;
}
@Override
public boolean isInvolved(Account account) throws DataException {
String address = account.getAddress();
@ -68,6 +70,7 @@ public class ArbitraryTransaction extends Transaction {
return false;
}
@Override
public BigDecimal getAmount(Account account) throws DataException {
String address = account.getAddress();
BigDecimal amount = BigDecimal.ZERO.setScale(8);

View File

@ -32,14 +32,17 @@ public class CancelOrderTransaction extends Transaction {
// More information
@Override
public List<Account> getRecipientAccounts() {
return new ArrayList<Account>();
}
@Override
public boolean isInvolved(Account account) throws DataException {
return account.getAddress().equals(this.getCreator().getAddress());
}
@Override
public BigDecimal getAmount(Account account) throws DataException {
BigDecimal amount = BigDecimal.ZERO.setScale(8);
@ -51,6 +54,7 @@ public class CancelOrderTransaction extends Transaction {
// Navigation
@Override
public Account getCreator() throws DataException {
return new PublicKeyAccount(this.repository, cancelOrderTransactionData.getCreatorPublicKey());
}

View File

@ -33,14 +33,17 @@ public class CreateOrderTransaction extends Transaction {
// More information
@Override
public List<Account> getRecipientAccounts() {
return new ArrayList<Account>();
}
@Override
public boolean isInvolved(Account account) throws DataException {
return account.getAddress().equals(this.getCreator().getAddress());
}
@Override
public BigDecimal getAmount(Account account) throws DataException {
BigDecimal amount = BigDecimal.ZERO.setScale(8);
@ -52,6 +55,7 @@ public class CreateOrderTransaction extends Transaction {
// Navigation
@Override
public Account getCreator() throws DataException {
return new PublicKeyAccount(this.repository, createOrderTransactionData.getCreatorPublicKey());
}

View File

@ -35,10 +35,12 @@ public class CreatePollTransaction extends Transaction {
// More information
@Override
public List<Account> getRecipientAccounts() throws DataException {
return Collections.singletonList(getOwner());
}
@Override
public boolean isInvolved(Account account) throws DataException {
String address = account.getAddress();
@ -51,6 +53,7 @@ public class CreatePollTransaction extends Transaction {
return false;
}
@Override
public BigDecimal getAmount(Account account) throws DataException {
String address = account.getAddress();
BigDecimal amount = BigDecimal.ZERO.setScale(8);
@ -63,6 +66,7 @@ public class CreatePollTransaction extends Transaction {
// Navigation
@Override
public Account getCreator() throws DataException {
return new PublicKeyAccount(this.repository, this.createPollTransactionData.getCreatorPublicKey());
}

View File

@ -36,10 +36,12 @@ public class GenesisTransaction extends Transaction {
// More information
@Override
public List<Account> getRecipientAccounts() throws DataException {
return Collections.singletonList(new Account(this.repository, genesisTransactionData.getRecipient()));
}
@Override
public boolean isInvolved(Account account) throws DataException {
String address = account.getAddress();
@ -52,6 +54,7 @@ public class GenesisTransaction extends Transaction {
return false;
}
@Override
public BigDecimal getAmount(Account account) throws DataException {
String address = account.getAddress();
BigDecimal amount = BigDecimal.ZERO.setScale(8);

View File

@ -36,10 +36,12 @@ public class IssueAssetTransaction extends Transaction {
// More information
@Override
public List<Account> getRecipientAccounts() throws DataException {
return Collections.singletonList(getOwner());
}
@Override
public boolean isInvolved(Account account) throws DataException {
String address = account.getAddress();
@ -52,6 +54,7 @@ public class IssueAssetTransaction extends Transaction {
return false;
}
@Override
public BigDecimal getAmount(Account account) throws DataException {
String address = account.getAddress();
BigDecimal amount = BigDecimal.ZERO.setScale(8);
@ -117,6 +120,7 @@ public class IssueAssetTransaction extends Transaction {
return ValidationResult.NO_BALANCE;
// XXX: Surely we want to check the asset name isn't already taken? This check is not present in gen1.
if (issueAssetTransactionData.getTimestamp() >= BlockChain.getIssueAssetV2Timestamp())
if (this.repository.getAssetRepository().assetExists(issueAssetTransactionData.getAssetName()))
return ValidationResult.ASSET_ALREADY_EXISTS;

View File

@ -34,10 +34,12 @@ public class MessageTransaction extends Transaction {
// More information
@Override
public List<Account> getRecipientAccounts() throws DataException {
return Collections.singletonList(new Account(this.repository, messageTransactionData.getRecipient()));
}
@Override
public boolean isInvolved(Account account) throws DataException {
String address = account.getAddress();
@ -50,6 +52,7 @@ public class MessageTransaction extends Transaction {
return false;
}
@Override
public BigDecimal getAmount(Account account) throws DataException {
String address = account.getAddress();
BigDecimal amount = BigDecimal.ZERO.setScale(8);

View File

@ -34,6 +34,7 @@ public class MultiPaymentTransaction extends Transaction {
// More information
@Override
public List<Account> getRecipientAccounts() throws DataException {
List<Account> recipients = new ArrayList<Account>();
@ -43,6 +44,7 @@ public class MultiPaymentTransaction extends Transaction {
return recipients;
}
@Override
public boolean isInvolved(Account account) throws DataException {
String address = account.getAddress();
@ -56,6 +58,7 @@ public class MultiPaymentTransaction extends Transaction {
return false;
}
@Override
public BigDecimal getAmount(Account account) throws DataException {
String address = account.getAddress();
BigDecimal amount = BigDecimal.ZERO.setScale(8);

View File

@ -30,10 +30,12 @@ public class PaymentTransaction extends Transaction {
// More information
@Override
public List<Account> getRecipientAccounts() throws DataException {
return Collections.singletonList(new Account(this.repository, paymentTransactionData.getRecipient()));
}
@Override
public boolean isInvolved(Account account) throws DataException {
String address = account.getAddress();
@ -46,6 +48,7 @@ public class PaymentTransaction extends Transaction {
return false;
}
@Override
public BigDecimal getAmount(Account account) throws DataException {
String address = account.getAddress();
BigDecimal amount = BigDecimal.ZERO.setScale(8);

View File

@ -31,10 +31,12 @@ public class TransferAssetTransaction extends Transaction {
// More information
@Override
public List<Account> getRecipientAccounts() throws DataException {
return Collections.singletonList(new Account(this.repository, transferAssetTransactionData.getRecipient()));
}
@Override
public boolean isInvolved(Account account) throws DataException {
String address = account.getAddress();
@ -47,6 +49,7 @@ public class TransferAssetTransaction extends Transaction {
return false;
}
@Override
public BigDecimal getAmount(Account account) throws DataException {
String address = account.getAddress();
BigDecimal amount = BigDecimal.ZERO.setScale(8);

View File

@ -18,6 +18,7 @@ public interface Repository extends AutoCloseable {
public void discardChanges() throws DataException;
@Override
public void close() throws DataException;
public void rebuild() throws DataException;

View File

@ -17,9 +17,9 @@ public class HSQLDBAccountRepository implements AccountRepository {
this.repository = repository;
}
@Override
public AccountData getAccount(String address) throws DataException {
try {
ResultSet resultSet = this.repository.checkedExecute("SELECT reference FROM Accounts WHERE account = ?", address);
try (ResultSet resultSet = this.repository.checkedExecute("SELECT reference FROM Accounts WHERE account = ?", address)) {
if (resultSet == null)
return null;
@ -29,6 +29,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public void save(AccountData accountData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts");
saveHelper.bind("account", accountData.getAddress()).bind("reference", accountData.getReference());
@ -40,9 +41,9 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public AccountBalanceData getBalance(String address, long assetId) throws DataException {
try {
ResultSet resultSet = this.repository.checkedExecute("SELECT balance FROM AccountBalances WHERE account = ? and asset_id = ?", address, assetId);
try (ResultSet resultSet = this.repository.checkedExecute("SELECT balance FROM AccountBalances WHERE account = ? and asset_id = ?", address, assetId)) {
if (resultSet == null)
return null;
@ -54,6 +55,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public void save(AccountBalanceData accountBalanceData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("AccountBalances");
saveHelper.bind("account", accountBalanceData.getAddress()).bind("asset_id", accountBalanceData.getAssetId()).bind("balance",
@ -66,6 +68,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public void delete(String address, long assetId) throws DataException {
try {
this.repository.delete("AccountBalances", "account = ? and asset_id = ?", address, assetId);

View File

@ -5,6 +5,7 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import data.assets.AssetData;
@ -23,10 +24,10 @@ public class HSQLDBAssetRepository implements AssetRepository {
// Assets
@Override
public AssetData fromAssetId(long assetId) throws DataException {
try {
ResultSet resultSet = this.repository
.checkedExecute("SELECT owner, asset_name, description, quantity, is_divisible, reference FROM Assets WHERE asset_id = ?", assetId);
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT owner, asset_name, description, quantity, is_divisible, reference FROM Assets WHERE asset_id = ?", assetId)) {
if (resultSet == null)
return null;
@ -43,10 +44,10 @@ public class HSQLDBAssetRepository implements AssetRepository {
}
}
@Override
public AssetData fromAssetName(String assetName) throws DataException {
try {
ResultSet resultSet = this.repository
.checkedExecute("SELECT owner, asset_id, description, quantity, is_divisible, reference FROM Assets WHERE asset_name = ?", assetName);
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT owner, asset_id, description, quantity, is_divisible, reference FROM Assets WHERE asset_name = ?", assetName)) {
if (resultSet == null)
return null;
@ -63,6 +64,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
}
}
@Override
public boolean assetExists(long assetId) throws DataException {
try {
return this.repository.exists("Assets", "asset_id = ?", assetId);
@ -71,6 +73,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
}
}
@Override
public boolean assetExists(String assetName) throws DataException {
try {
return this.repository.exists("Assets", "asset_name = ?", assetName);
@ -79,6 +82,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
}
}
@Override
public void save(AssetData assetData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Assets");
@ -89,13 +93,21 @@ public class HSQLDBAssetRepository implements AssetRepository {
try {
saveHelper.execute(this.repository);
if (assetData.getAssetId() == null)
assetData.setAssetId(this.repository.callIdentity());
if (assetData.getAssetId() == null) {
// Fetch new assetId
try (ResultSet resultSet = this.repository.checkedExecute("SELECT asset_id FROM Assets WHERE reference = ?", assetData.getReference())) {
if (resultSet == null)
throw new DataException("Unable to fetch new asset ID from repository");
assetData.setAssetId(resultSet.getLong(1));
}
}
} catch (SQLException e) {
throw new DataException("Unable to save asset into repository", e);
}
}
@Override
public void delete(long assetId) throws DataException {
try {
this.repository.delete("Assets", "asset_id = ?", assetId);
@ -106,11 +118,11 @@ public class HSQLDBAssetRepository implements AssetRepository {
// Orders
@Override
public OrderData fromOrderId(byte[] orderId) throws DataException {
try {
ResultSet resultSet = this.repository.checkedExecute(
try (ResultSet resultSet = this.repository.checkedExecute(
"SELECT creator, have_asset_id, want_asset_id, amount, fulfilled, price, ordered, is_closed, is_fulfilled FROM AssetOrders WHERE asset_order_id = ?",
orderId);
orderId)) {
if (resultSet == null)
return null;
@ -120,7 +132,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
BigDecimal amount = resultSet.getBigDecimal(4);
BigDecimal fulfilled = resultSet.getBigDecimal(5);
BigDecimal price = resultSet.getBigDecimal(6);
long timestamp = resultSet.getTimestamp(7).getTime();
long timestamp = resultSet.getTimestamp(7, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
boolean isClosed = resultSet.getBoolean(8);
boolean isFulfilled = resultSet.getBoolean(9);
@ -130,14 +142,14 @@ public class HSQLDBAssetRepository implements AssetRepository {
}
}
@Override
public List<OrderData> getOpenOrders(long haveAssetId, long wantAssetId) throws DataException {
List<OrderData> orders = new ArrayList<OrderData>();
try {
ResultSet resultSet = this.repository.checkedExecute(
try (ResultSet resultSet = this.repository.checkedExecute(
"SELECT creator, asset_order_id, amount, fulfilled, price, ordered FROM AssetOrders "
+ "WHERE have_asset_id = ? AND want_asset_id = ? AND is_closed = FALSE AND is_fulfilled = FALSE ORDER BY price ASC",
haveAssetId, wantAssetId);
haveAssetId, wantAssetId)) {
if (resultSet == null)
return orders;
@ -147,7 +159,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
BigDecimal amount = resultSet.getBigDecimal(3);
BigDecimal fulfilled = resultSet.getBigDecimal(4);
BigDecimal price = resultSet.getBigDecimal(5);
long timestamp = resultSet.getTimestamp(6).getTime();
long timestamp = resultSet.getTimestamp(6, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
boolean isClosed = false;
boolean isFulfilled = false;
@ -162,6 +174,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
}
}
@Override
public void save(OrderData orderData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("AssetOrders");
@ -177,6 +190,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
}
}
@Override
public void delete(byte[] orderId) throws DataException {
try {
this.repository.delete("AssetOrders", "asset_order_id = ?", orderId);
@ -187,12 +201,12 @@ public class HSQLDBAssetRepository implements AssetRepository {
// Trades
@Override
public List<TradeData> getOrdersTrades(byte[] initiatingOrderId) throws DataException {
List<TradeData> trades = new ArrayList<TradeData>();
try {
ResultSet resultSet = this.repository.checkedExecute("SELECT target_order_id, amount, price, traded FROM AssetTrades WHERE initiating_order_id = ?",
initiatingOrderId);
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT target_order_id, amount, price, traded FROM AssetTrades WHERE initiating_order_id = ?", initiatingOrderId)) {
if (resultSet == null)
return trades;
@ -200,7 +214,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
byte[] targetOrderId = resultSet.getBytes(1);
BigDecimal amount = resultSet.getBigDecimal(2);
BigDecimal price = resultSet.getBigDecimal(3);
long timestamp = resultSet.getTimestamp(4).getTime();
long timestamp = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
TradeData trade = new TradeData(initiatingOrderId, targetOrderId, amount, price, timestamp);
trades.add(trade);
@ -212,6 +226,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
}
}
@Override
public void save(TradeData tradeData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("AssetTrades");
@ -225,6 +240,7 @@ public class HSQLDBAssetRepository implements AssetRepository {
}
}
@Override
public void delete(TradeData tradeData) throws DataException {
try {
this.repository.delete("AssetTrades", "initiating_order_id = ? AND target_order_id = ? AND amount = ? AND price = ?", tradeData.getInitiator(),

View File

@ -5,6 +5,7 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import data.block.BlockData;
@ -25,23 +26,23 @@ public class HSQLDBBlockRepository implements BlockRepository {
this.repository = repository;
}
private BlockData getBlockFromResultSet(ResultSet rs) throws DataException {
if (rs == null)
private BlockData getBlockFromResultSet(ResultSet resultSet) throws DataException {
if (resultSet == null)
return null;
try {
int version = rs.getInt(1);
byte[] reference = rs.getBytes(2);
int transactionCount = rs.getInt(3);
BigDecimal totalFees = rs.getBigDecimal(4);
byte[] transactionsSignature = rs.getBytes(5);
int height = rs.getInt(6);
long timestamp = rs.getTimestamp(7).getTime();
BigDecimal generatingBalance = rs.getBigDecimal(8);
byte[] generatorPublicKey = rs.getBytes(9);
byte[] generatorSignature = rs.getBytes(10);
byte[] atBytes = rs.getBytes(11);
BigDecimal atFees = rs.getBigDecimal(12);
int version = resultSet.getInt(1);
byte[] reference = resultSet.getBytes(2);
int transactionCount = resultSet.getInt(3);
BigDecimal totalFees = resultSet.getBigDecimal(4);
byte[] transactionsSignature = resultSet.getBytes(5);
int height = resultSet.getInt(6);
long timestamp = resultSet.getTimestamp(7, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
BigDecimal generatingBalance = resultSet.getBigDecimal(8);
byte[] generatorPublicKey = resultSet.getBytes(9);
byte[] generatorSignature = resultSet.getBytes(10);
byte[] atBytes = resultSet.getBytes(11);
BigDecimal atFees = resultSet.getBigDecimal(12);
return new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance,
generatorPublicKey, generatorSignature, atBytes, atFees);
@ -50,76 +51,77 @@ public class HSQLDBBlockRepository implements BlockRepository {
}
}
@Override
public BlockData fromSignature(byte[] signature) throws DataException {
try {
ResultSet rs = this.repository.checkedExecute("SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE signature = ?", signature);
return getBlockFromResultSet(rs);
try (ResultSet resultSet = this.repository.checkedExecute("SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE signature = ?", signature)) {
return getBlockFromResultSet(resultSet);
} catch (SQLException e) {
throw new DataException("Error loading data from DB", e);
}
}
@Override
public BlockData fromReference(byte[] reference) throws DataException {
try {
ResultSet rs = this.repository.checkedExecute("SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE height = ?", reference);
return getBlockFromResultSet(rs);
try (ResultSet resultSet = this.repository.checkedExecute("SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE height = ?", reference)) {
return getBlockFromResultSet(resultSet);
} catch (SQLException e) {
throw new DataException("Error loading data from DB", e);
}
}
@Override
public BlockData fromHeight(int height) throws DataException {
try {
ResultSet rs = this.repository.checkedExecute("SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE height = ?", height);
return getBlockFromResultSet(rs);
try (ResultSet resultSet = this.repository.checkedExecute("SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE height = ?", height)) {
return getBlockFromResultSet(resultSet);
} catch (SQLException e) {
throw new DataException("Error loading data from DB", e);
}
}
@Override
public int getHeightFromSignature(byte[] signature) throws DataException {
try {
ResultSet rs = this.repository.checkedExecute("SELECT height FROM Blocks WHERE signature = ?", signature);
if (rs == null)
try (ResultSet resultSet = this.repository.checkedExecute("SELECT height FROM Blocks WHERE signature = ?", signature)) {
if (resultSet == null)
return 0;
return rs.getInt(1);
return resultSet.getInt(1);
} catch (SQLException e) {
throw new DataException("Error obtaining block height from repository", e);
}
}
@Override
public int getBlockchainHeight() throws DataException {
try {
ResultSet rs = this.repository.checkedExecute("SELECT MAX(height) FROM Blocks");
if (rs == null)
try (ResultSet resultSet = this.repository.checkedExecute("SELECT MAX(height) FROM Blocks")) {
if (resultSet == null)
return 0;
return rs.getInt(1);
return resultSet.getInt(1);
} catch (SQLException e) {
throw new DataException("Error obtaining blockchain height from repository", e);
}
}
@Override
public BlockData getLastBlock() throws DataException {
return fromHeight(getBlockchainHeight());
}
@Override
public List<TransactionData> getTransactionsFromSignature(byte[] signature) throws DataException {
List<TransactionData> transactions = new ArrayList<TransactionData>();
try {
ResultSet rs = this.repository.checkedExecute("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ?", signature);
if (rs == null)
try (ResultSet resultSet = this.repository.checkedExecute("SELECT transaction_signature FROM BlockTransactions WHERE block_signature = ?", signature)) {
if (resultSet == null)
return transactions; // No transactions in this block
TransactionRepository transactionRepo = this.repository.getTransactionRepository();
// NB: do-while loop because .checkedExecute() implicitly calls ResultSet.next() for us
do {
byte[] transactionSignature = rs.getBytes(1);
byte[] transactionSignature = resultSet.getBytes(1);
transactions.add(transactionRepo.fromSignature(transactionSignature));
} while (rs.next());
} while (resultSet.next());
} catch (SQLException e) {
throw new DataException(e);
}
@ -127,6 +129,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
return transactions;
}
@Override
public void save(BlockData blockData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Blocks");
@ -144,6 +147,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
}
}
@Override
public void delete(BlockData blockData) throws DataException {
try {
this.repository.delete("Blocks", "signature = ?", blockData.getSignature());
@ -152,6 +156,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
}
}
@Override
public void save(BlockTransactionData blockTransactionData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("BlockTransactions");
saveHelper.bind("block_signature", blockTransactionData.getBlockSignature()).bind("sequence", blockTransactionData.getSequence())
@ -164,6 +169,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
}
}
@Override
public void delete(BlockTransactionData blockTransactionData) throws DataException {
try {
this.repository.delete("BlockTransactions", "block_signature = ? AND sequence = ? AND transaction_signature = ?",

View File

@ -36,13 +36,11 @@ public class HSQLDBDatabaseUpdates {
private static int fetchDatabaseVersion(Connection connection) throws SQLException {
int databaseVersion = 0;
try {
Statement stmt = connection.createStatement();
if (stmt.execute("SELECT version FROM DatabaseInfo")) {
ResultSet rs = stmt.getResultSet();
if (rs.next())
databaseVersion = rs.getInt(1);
try (Statement stmt = connection.createStatement()) {
if (stmt.execute("SELECT version FROM DatabaseInfo"))
try (ResultSet resultSet = stmt.getResultSet()) {
if (resultSet.next())
databaseVersion = resultSet.getInt(1);
}
} catch (SQLException e) {
// empty database
@ -60,16 +58,16 @@ public class HSQLDBDatabaseUpdates {
private static boolean databaseUpdating(Connection connection) throws SQLException {
int databaseVersion = fetchDatabaseVersion(connection);
Statement stmt = connection.createStatement();
try (Statement stmt = connection.createStatement()) {
/*
* Try not to add too many constraints as much of these checks will be performed during transaction validation. Also some constraints might be too harsh
* on competing unconfirmed transactions.
* Try not to add too many constraints as much of these checks will be performed during transaction validation. Also some constraints might be too
* harsh on competing unconfirmed transactions.
*
* Only really add "ON DELETE CASCADE" to sub-tables that store type-specific data. For example on sub-types of Transactions like PaymentTransactions. A
* counterexample would be adding "ON DELETE CASCADE" to Assets using Assets' "reference" as a foreign key referring to Transactions' "signature". We
* want to database to automatically delete complete transaction data (Transactions row and corresponding PaymentTransactions row), but leave deleting
* less related table rows (Assets) to the Java logic.
* Only really add "ON DELETE CASCADE" to sub-tables that store type-specific data. For example on sub-types of Transactions like
* PaymentTransactions. A counterexample would be adding "ON DELETE CASCADE" to Assets using Assets' "reference" as a foreign key referring to
* Transactions' "signature". We want to database to automatically delete complete transaction data (Transactions row and corresponding
* PaymentTransactions row), but leave deleting less related table rows (Assets) to the Java logic.
*/
switch (databaseVersion) {
@ -104,7 +102,7 @@ public class HSQLDBDatabaseUpdates {
// Blocks
stmt.execute("CREATE TABLE Blocks (signature BlockSignature PRIMARY KEY, version TINYINT NOT NULL, reference BlockSignature, "
+ "transaction_count INTEGER NOT NULL, total_fees QoraAmount NOT NULL, transactions_signature Signature NOT NULL, "
+ "height INTEGER NOT NULL, generation TIMESTAMP NOT NULL, generating_balance QoraAmount NOT NULL, "
+ "height INTEGER NOT NULL, generation TIMESTAMP WITH TIME ZONE NOT NULL, generating_balance QoraAmount NOT NULL, "
+ "generator QoraPublicKey NOT NULL, generator_signature Signature NOT NULL, AT_data VARBINARY(20000), AT_fees QoraAmount)");
// For finding blocks by height.
stmt.execute("CREATE INDEX BlockHeightIndex ON Blocks (height)");
@ -119,7 +117,7 @@ public class HSQLDBDatabaseUpdates {
case 2:
// Generic transactions (null reference, creator and milestone_block for genesis transactions)
stmt.execute("CREATE TABLE Transactions (signature Signature PRIMARY KEY, reference Signature, type TINYINT NOT NULL, "
+ "creator QoraPublicKey, creation TIMESTAMP NOT NULL, fee QoraAmount NOT NULL, milestone_block BlockSignature)");
+ "creator QoraPublicKey, creation TIMESTAMP WITH TIME ZONE NOT NULL, fee QoraAmount NOT NULL, milestone_block BlockSignature)");
// For finding transactions by transaction type.
stmt.execute("CREATE INDEX TransactionTypeIndex ON Transactions (type)");
// For finding transactions using timestamp.
@ -140,7 +138,7 @@ public class HSQLDBDatabaseUpdates {
// Unconfirmed transactions
// XXX Do we need this? If a transaction doesn't have a corresponding BlockTransactions record then it's unconfirmed?
stmt.execute("CREATE TABLE UnconfirmedTransactions (signature Signature PRIMARY KEY, expiry TIMESTAMP NOT NULL)");
stmt.execute("CREATE TABLE UnconfirmedTransactions (signature Signature PRIMARY KEY, expiry TIMESTAMP WITH TIME ZONE NOT NULL)");
stmt.execute("CREATE INDEX UnconfirmedTransactionExpiryIndex ON UnconfirmedTransactions (expiry)");
// Transaction recipients
@ -285,9 +283,13 @@ public class HSQLDBDatabaseUpdates {
case 21:
// Assets (including QORA coin itself)
stmt.execute("CREATE TABLE Assets (asset_id AssetID, owner QoraAddress NOT NULL, "
+ "asset_name AssetName NOT NULL, description VARCHAR(4000) NOT NULL, "
+ "quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, reference Signature NOT NULL, PRIMARY KEY (asset_id))");
// We need a corresponding trigger to make sure new asset_id values are assigned sequentially
stmt.execute(
"CREATE TABLE Assets (asset_id AssetID IDENTITY, owner QoraAddress NOT NULL, asset_name AssetName NOT NULL, description VARCHAR(4000) NOT NULL, "
+ "quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, reference Signature NOT NULL)");
"CREATE TRIGGER Asset_ID_Trigger BEFORE INSERT ON Assets REFERENCING NEW ROW AS new_row FOR EACH ROW WHEN (new_row.asset_id IS NULL) "
+ "SET new_row.asset_id = (SELECT IFNULL(MAX(asset_id) + 1, 0) FROM Assets)");
// For when a user wants to lookup an asset by name
stmt.execute("CREATE INDEX AssetNameIndex on Assets (asset_name)");
break;
@ -295,8 +297,8 @@ public class HSQLDBDatabaseUpdates {
case 22:
// Accounts
stmt.execute("CREATE TABLE Accounts (account QoraAddress, reference Signature, PRIMARY KEY (account))");
stmt.execute(
"CREATE TABLE AccountBalances (account QoraAddress, asset_id AssetID, balance QoraAmount NOT NULL, PRIMARY KEY (account, asset_id))");
stmt.execute("CREATE TABLE AccountBalances (account QoraAddress, asset_id AssetID, balance QoraAmount NOT NULL, "
+ "PRIMARY KEY (account, asset_id))");
break;
case 23:
@ -304,7 +306,8 @@ public class HSQLDBDatabaseUpdates {
stmt.execute(
"CREATE TABLE AssetOrders (asset_order_id AssetOrderID, creator QoraPublicKey NOT NULL, have_asset_id AssetID NOT NULL, want_asset_id AssetID NOT NULL, "
+ "amount QoraAmount NOT NULL, fulfilled QoraAmount NOT NULL, price QoraAmount NOT NULL, "
+ "ordered TIMESTAMP NOT NULL, is_closed BOOLEAN NOT NULL, is_fulfilled BOOLEAN NOT NULL, " + "PRIMARY KEY (asset_order_id))");
+ "ordered TIMESTAMP WITH TIME ZONE NOT NULL, is_closed BOOLEAN NOT NULL, is_fulfilled BOOLEAN NOT NULL, "
+ "PRIMARY KEY (asset_order_id))");
// For quick matching of orders. is_closed are is_fulfilled included so inactive orders can be filtered out.
stmt.execute("CREATE INDEX AssetOrderMatchingIndex on AssetOrders (have_asset_id, want_asset_id, is_closed, is_fulfilled)");
// For when a user wants to look up their current/historic orders. is_closed included so user can filter by active/inactive orders.
@ -314,7 +317,7 @@ public class HSQLDBDatabaseUpdates {
case 24:
// Asset Trades
stmt.execute("CREATE TABLE AssetTrades (initiating_order_id AssetOrderId NOT NULL, target_order_id AssetOrderId NOT NULL, "
+ "amount QoraAmount NOT NULL, price QoraAmount NOT NULL, traded TIMESTAMP NOT NULL)");
+ "amount QoraAmount NOT NULL, price QoraAmount NOT NULL, traded TIMESTAMP WITH TIME ZONE NOT NULL)");
// For looking up historic trades based on orders
stmt.execute("CREATE INDEX AssetTradeBuyOrderIndex on AssetTrades (initiating_order_id, traded)");
stmt.execute("CREATE INDEX AssetTradeSellOrderIndex on AssetTrades (target_order_id, traded)");
@ -324,7 +327,7 @@ public class HSQLDBDatabaseUpdates {
// Polls/Voting
stmt.execute(
"CREATE TABLE Polls (poll_name PollName, description VARCHAR(4000) NOT NULL, creator QoraPublicKey NOT NULL, owner QoraAddress NOT NULL, "
+ "published TIMESTAMP NOT NULL, " + "PRIMARY KEY (poll_name))");
+ "published TIMESTAMP WITH TIME ZONE NOT NULL, " + "PRIMARY KEY (poll_name))");
// Various options available on a poll
stmt.execute("CREATE TABLE PollOptions (poll_name PollName, option_index TINYINT NOT NULL, option_name PollOption, "
+ "PRIMARY KEY (poll_name, option_index), FOREIGN KEY (poll_name) REFERENCES Polls (poll_name) ON DELETE CASCADE)");
@ -339,7 +342,7 @@ public class HSQLDBDatabaseUpdates {
// Registered Names
stmt.execute(
"CREATE TABLE Names (name RegisteredName, data VARCHAR(4000) NOT NULL, registrant QoraPublicKey NOT NULL, owner QoraAddress NOT NULL, "
+ "registered TIMESTAMP NOT NULL, updated TIMESTAMP, reference Signature, is_for_sale BOOLEAN NOT NULL, sale_price QoraAmount, "
+ "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;
@ -347,6 +350,7 @@ public class HSQLDBDatabaseUpdates {
// nothing to do
return false;
}
}
// database was updated
return true;

View File

@ -4,6 +4,7 @@ import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Calendar;
import data.naming.NameData;
import repository.NameRepository;
@ -19,19 +20,18 @@ 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);
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT registrant, 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).getTime();
long registered = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
// Special handling for possibly-NULL "updated" column
Timestamp updatedTimestamp = resultSet.getTimestamp(5);
Timestamp updatedTimestamp = resultSet.getTimestamp(5, Calendar.getInstance(HSQLDBRepository.UTC));
Long updated = updatedTimestamp == null ? null : updatedTimestamp.getTime();
byte[] reference = resultSet.getBytes(6);

View File

@ -6,6 +6,7 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.TimeZone;
import repository.AccountRepository;
import repository.AssetRepository;
@ -19,6 +20,8 @@ import repository.hsqldb.transaction.HSQLDBTransactionRepository;
public class HSQLDBRepository implements Repository {
public static final TimeZone UTC = TimeZone.getTimeZone("UTC");
protected Connection connection;
// NB: no visibility modifier so only callable from within same package
@ -74,24 +77,22 @@ public class HSQLDBRepository implements Repository {
}
}
// TODO prevent leaking of connections if .close() is not called before garbage collection of the repository.
// Maybe use PhantomReference to call .close() on connection after repository destruction?
@Override
public void close() throws DataException {
try {
try (Statement stmt = this.connection.createStatement()) {
// Diagnostic check for uncommitted changes
Statement stmt = this.connection.createStatement();
if (!stmt.execute("SELECT transaction, transaction_size FROM information_schema.system_sessions")) // TRANSACTION_SIZE() broken?
throw new DataException("Unable to check repository status during close");
ResultSet rs = stmt.getResultSet();
if (rs == null || !rs.next())
throw new DataException("Unable to check repository status during close");
try (ResultSet resultSet = stmt.getResultSet()) {
if (resultSet == null || !resultSet.next())
System.out.println("Unable to check repository status during close");
boolean inTransaction = rs.getBoolean(1);
int transactionCount = rs.getInt(2);
boolean inTransaction = resultSet.getBoolean(1);
int transactionCount = resultSet.getInt(2);
if (inTransaction && transactionCount != 0)
System.out.println("Uncommitted changes (" + transactionCount + ") during repository close");
}
// give connection back to the pool
this.connection.close();
@ -115,9 +116,12 @@ public class HSQLDBRepository implements Repository {
* @return ResultSet, or null if there are no found rows
* @throws SQLException
*/
@SuppressWarnings("resource")
public ResultSet checkedExecute(String sql, Object... objects) throws SQLException {
PreparedStatement preparedStatement = this.connection.prepareStatement(sql);
// Close the PreparedStatement when the ResultSet is closed otherwise there's a potential resource leak.
// We can't use try-with-resources here as closing the PreparedStatement on return would also prematurely close the ResultSet.
preparedStatement.closeOnCompletion();
return this.checkedExecuteResultSet(preparedStatement, objects);
}
@ -198,13 +202,14 @@ public class HSQLDBRepository implements Repository {
* @throws SQLException
*/
public Long callIdentity() throws SQLException {
PreparedStatement preparedStatement = this.connection.prepareStatement("CALL IDENTITY()");
ResultSet resultSet = this.checkedExecuteResultSet(preparedStatement);
try (PreparedStatement preparedStatement = this.connection.prepareStatement("CALL IDENTITY()");
ResultSet resultSet = this.checkedExecuteResultSet(preparedStatement)) {
if (resultSet == null)
return null;
return resultSet.getLong(1);
}
}
/**
* Efficiently query database for existence of matching row.
@ -224,13 +229,14 @@ public class HSQLDBRepository implements Repository {
* @throws SQLException
*/
public boolean exists(String tableName, String whereClause, Object... objects) throws SQLException {
PreparedStatement preparedStatement = this.connection.prepareStatement("SELECT TRUE FROM " + tableName + " WHERE " + whereClause + " LIMIT 1");
ResultSet resultSet = this.checkedExecuteResultSet(preparedStatement, objects);
try (PreparedStatement preparedStatement = this.connection.prepareStatement("SELECT TRUE FROM " + tableName + " WHERE " + whereClause + " LIMIT 1");
ResultSet resultSet = this.checkedExecuteResultSet(preparedStatement, objects)) {
if (resultSet == null)
return false;
return true;
}
}
/**
* Delete rows from database table.
@ -241,8 +247,9 @@ public class HSQLDBRepository implements Repository {
* @throws SQLException
*/
public void delete(String tableName, String whereClause, Object... objects) throws SQLException {
PreparedStatement preparedStatement = this.connection.prepareStatement("DELETE FROM " + tableName + " WHERE " + whereClause);
try (PreparedStatement preparedStatement = this.connection.prepareStatement("DELETE FROM " + tableName + " WHERE " + whereClause)) {
this.checkedExecuteUpdateCount(preparedStatement, objects);
}
}
}

View File

@ -3,6 +3,7 @@ package repository.hsqldb;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
import org.hsqldb.jdbc.JDBCPool;
@ -22,6 +23,14 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
this.connectionPool = new JDBCPool();
this.connectionPool.setUrl(this.connectionUrl);
Properties properties = new Properties();
properties.setProperty("close_result", "true"); // Auto-close old ResultSet if Statement creates new ResultSet
properties.setProperty("sql.strict_exec", "true"); // No multi-SQL execute() or DDL/DML executeQuery()
properties.setProperty("sql.enforce_names", "true"); // SQL keywords cannot be used as DB object names, e.g. table names
properties.setProperty("sql.syntax_mys", "true"); // Required for our use of INSERT ... ON DUPLICATE KEY UPDATE ... syntax
properties.setProperty("sql.pad_space", "false"); // Do not pad strings to same length before comparison
this.connectionPool.setProperties(properties);
// Perform DB updates?
try (final Connection connection = this.connectionPool.getConnection()) {
HSQLDBDatabaseUpdates.updateDatabase(connection);
@ -30,6 +39,7 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
}
}
@Override
public Repository getRepository() throws DataException {
try {
return new HSQLDBRepository(this.getConnection());
@ -41,22 +51,23 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory {
private Connection getConnection() throws SQLException {
Connection connection = this.connectionPool.getConnection();
// start transaction
// Set transaction level
connection.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
connection.setAutoCommit(false);
return connection;
}
@Override
public void close() throws DataException {
try {
// Close all existing connections immediately
this.connectionPool.close(0);
// Now that all connections are closed, create a dedicated connection to shut down repository
Connection connection = DriverManager.getConnection(this.connectionUrl);
try (Connection connection = DriverManager.getConnection(this.connectionUrl)) {
connection.createStatement().execute("SHUTDOWN");
connection.close();
}
} catch (SQLException e) {
throw new DataException("Error during repository shutdown", e);
}

View File

@ -3,8 +3,10 @@ package repository.hsqldb;
import java.math.BigDecimal;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
/**
@ -56,12 +58,12 @@ public class HSQLDBSaver {
*/
public boolean execute(HSQLDBRepository repository) throws SQLException {
String sql = this.formatInsertWithPlaceholders();
PreparedStatement preparedStatement = repository.connection.prepareStatement(sql);
try (PreparedStatement preparedStatement = repository.connection.prepareStatement(sql)) {
this.bindValues(preparedStatement);
return preparedStatement.execute();
}
}
/**
* Format table and column names into an INSERT INTO ... SQL statement.
@ -107,11 +109,15 @@ public class HSQLDBSaver {
for (int i = 0; i < this.objects.size(); ++i) {
Object object = this.objects.get(i);
if (object instanceof BigDecimal) {
// Special treatment for BigDecimals so that they retain their "scale",
// which would otherwise be assumed as 0.
if (object instanceof BigDecimal) {
preparedStatement.setBigDecimal(i + 1, (BigDecimal) object);
preparedStatement.setBigDecimal(i + this.objects.size() + 1, (BigDecimal) object);
} else if (object instanceof Timestamp) {
// Special treatment for Timestamps so that they are stored as UTC
preparedStatement.setTimestamp(i + 1, (Timestamp) object, Calendar.getInstance(HSQLDBRepository.UTC));
preparedStatement.setTimestamp(i + this.objects.size() + 1, (Timestamp) object, Calendar.getInstance(HSQLDBRepository.UTC));
} else {
preparedStatement.setObject(i + 1, object);
preparedStatement.setObject(i + this.objects.size() + 1, object);

View File

@ -4,6 +4,7 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import data.voting.PollData;
@ -22,36 +23,39 @@ public class HSQLDBVotingRepository implements VotingRepository {
// Polls
@Override
public PollData fromPollName(String pollName) throws DataException {
try {
ResultSet resultSet = this.repository.checkedExecute("SELECT description, creator, owner, published FROM Polls WHERE poll_name = ?", pollName);
try (ResultSet resultSet = this.repository.checkedExecute("SELECT description, creator, owner, published FROM Polls WHERE poll_name = ?", pollName)) {
if (resultSet == null)
return null;
String description = resultSet.getString(1);
byte[] creatorPublicKey = resultSet.getBytes(2);
String owner = resultSet.getString(3);
long published = resultSet.getTimestamp(4).getTime();
long published = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
resultSet = this.repository.checkedExecute("SELECT option_name FROM PollOptions where poll_name = ? ORDER BY option_index ASC", pollName);
if (resultSet == null)
try (ResultSet optionsResultSet = this.repository
.checkedExecute("SELECT option_name FROM PollOptions where poll_name = ? ORDER BY option_index ASC", pollName)) {
if (optionsResultSet == null)
return null;
List<PollOptionData> pollOptions = new ArrayList<PollOptionData>();
// NOTE: do-while because checkedExecute() above has already called rs.next() for us
do {
String optionName = resultSet.getString(1);
String optionName = optionsResultSet.getString(1);
pollOptions.add(new PollOptionData(optionName));
} while (resultSet.next());
} while (optionsResultSet.next());
return new PollData(creatorPublicKey, owner, pollName, description, pollOptions, published);
}
} catch (SQLException e) {
throw new DataException("Unable to fetch poll from repository", e);
}
}
@Override
public boolean pollExists(String pollName) throws DataException {
try {
return this.repository.exists("Polls", "poll_name = ?", pollName);
@ -60,6 +64,7 @@ public class HSQLDBVotingRepository implements VotingRepository {
}
}
@Override
public void save(PollData pollData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Polls");
@ -89,6 +94,7 @@ public class HSQLDBVotingRepository implements VotingRepository {
}
}
@Override
public void delete(String pollName) throws DataException {
// NOTE: The corresponding rows in PollOptions are deleted automatically by the database thanks to "ON DELETE CASCADE" in the PollOptions' FOREIGN KEY
// definition.
@ -101,11 +107,11 @@ public class HSQLDBVotingRepository implements VotingRepository {
// Votes
@Override
public List<VoteOnPollData> getVotes(String pollName) throws DataException {
List<VoteOnPollData> votes = new ArrayList<VoteOnPollData>();
try {
ResultSet resultSet = this.repository.checkedExecute("SELECT voter, option_index FROM PollVotes WHERE poll_name = ?", pollName);
try (ResultSet resultSet = this.repository.checkedExecute("SELECT voter, option_index FROM PollVotes WHERE poll_name = ?", pollName)) {
if (resultSet == null)
return votes;
@ -123,10 +129,10 @@ public class HSQLDBVotingRepository implements VotingRepository {
}
}
@Override
public VoteOnPollData getVote(String pollName, byte[] voterPublicKey) throws DataException {
try {
ResultSet resultSet = this.repository.checkedExecute("SELECT option_index FROM PollVotes WHERE poll_name = ? AND voter = ?", pollName,
voterPublicKey);
try (ResultSet resultSet = this.repository.checkedExecute("SELECT option_index FROM PollVotes WHERE poll_name = ? AND voter = ?", pollName,
voterPublicKey)) {
if (resultSet == null)
return null;
@ -138,6 +144,7 @@ public class HSQLDBVotingRepository implements VotingRepository {
}
}
@Override
public void save(VoteOnPollData voteOnPollData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("PollVotes");
@ -151,6 +158,7 @@ public class HSQLDBVotingRepository implements VotingRepository {
}
}
@Override
public void delete(String pollName, byte[] voterPublicKey) throws DataException {
try {
this.repository.delete("PollVotes", "poll_name = ? AND voter = ?", pollName, voterPublicKey);

View File

@ -20,16 +20,15 @@ public class HSQLDBArbitraryTransactionRepository extends HSQLDBTransactionRepos
}
TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException {
try {
ResultSet rs = this.repository.checkedExecute("SELECT sender, version, service, data_hash from ArbitraryTransactions WHERE signature = ?",
signature);
if (rs == null)
try (ResultSet resultSet = this.repository.checkedExecute("SELECT sender, version, service, data_hash from ArbitraryTransactions WHERE signature = ?",
signature)) {
if (resultSet == null)
return null;
byte[] senderPublicKey = rs.getBytes(1);
int version = rs.getInt(2);
int service = rs.getInt(3);
byte[] dataHash = rs.getBytes(4);
byte[] senderPublicKey = resultSet.getBytes(1);
int version = resultSet.getInt(2);
int service = resultSet.getInt(3);
byte[] dataHash = resultSet.getBytes(4);
List<PaymentData> payments = this.getPaymentsFromSignature(signature);
@ -51,7 +50,8 @@ public class HSQLDBArbitraryTransactionRepository extends HSQLDBTransactionRepos
HSQLDBSaver saveHelper = new HSQLDBSaver("ArbitraryTransactions");
saveHelper.bind("signature", arbitraryTransactionData.getSignature()).bind("sender", arbitraryTransactionData.getSenderPublicKey())
.bind("version", arbitraryTransactionData.getVersion()).bind("service", arbitraryTransactionData.getService()).bind("data_hash", arbitraryTransactionData.getData());
.bind("version", arbitraryTransactionData.getVersion()).bind("service", arbitraryTransactionData.getService())
.bind("data_hash", arbitraryTransactionData.getData());
try {
saveHelper.execute(this.repository);

View File

@ -17,16 +17,15 @@ public class HSQLDBBuyNameTransactionRepository extends HSQLDBTransactionReposit
}
TransactionData fromBase(byte[] signature, byte[] reference, byte[] buyerPublicKey, long timestamp, BigDecimal fee) throws DataException {
try {
ResultSet rs = this.repository.checkedExecute("SELECT name, amount, seller, name_reference FROM BuyNameTransactions WHERE signature = ?",
signature);
if (rs == null)
try (ResultSet resultSet = this.repository.checkedExecute("SELECT name, amount, seller, name_reference FROM BuyNameTransactions WHERE signature = ?",
signature)) {
if (resultSet == null)
return null;
String name = rs.getString(1);
BigDecimal amount = rs.getBigDecimal(2);
String seller = rs.getString(3);
byte[] nameReference = rs.getBytes(4);
String name = resultSet.getString(1);
BigDecimal amount = resultSet.getBigDecimal(2);
String seller = resultSet.getString(3);
byte[] nameReference = resultSet.getBytes(4);
return new BuyNameTransactionData(buyerPublicKey, name, amount, seller, nameReference, fee, timestamp, reference, signature);
} catch (SQLException e) {

View File

@ -17,12 +17,11 @@ public class HSQLDBCancelOrderTransactionRepository extends HSQLDBTransactionRep
}
TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException {
try {
ResultSet rs = this.repository.checkedExecute("SELECT asset_order_id FROM CancelAssetOrderTransactions WHERE signature = ?", signature);
if (rs == null)
try (ResultSet resultSet = this.repository.checkedExecute("SELECT asset_order_id FROM CancelAssetOrderTransactions WHERE signature = ?", signature)) {
if (resultSet == null)
return null;
byte[] assetOrderId = rs.getBytes(1);
byte[] assetOrderId = resultSet.getBytes(1);
return new CancelOrderTransactionData(creatorPublicKey, assetOrderId, fee, timestamp, reference, signature);
} catch (SQLException e) {

View File

@ -17,12 +17,11 @@ public class HSQLDBCancelSellNameTransactionRepository extends HSQLDBTransaction
}
TransactionData fromBase(byte[] signature, byte[] reference, byte[] ownerPublicKey, long timestamp, BigDecimal fee) throws DataException {
try {
ResultSet rs = this.repository.checkedExecute("SELECT name FROM CancelSellNameTransactions WHERE signature = ?", signature);
if (rs == null)
try (ResultSet resultSet = this.repository.checkedExecute("SELECT name FROM CancelSellNameTransactions WHERE signature = ?", signature)) {
if (resultSet == null)
return null;
String name = rs.getString(1);
String name = resultSet.getString(1);
return new CancelSellNameTransactionData(ownerPublicKey, name, fee, timestamp, reference, signature);
} catch (SQLException e) {

View File

@ -17,16 +17,15 @@ public class HSQLDBCreateOrderTransactionRepository extends HSQLDBTransactionRep
}
TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException {
try {
ResultSet rs = this.repository
.checkedExecute("SELECT have_asset_id, amount, want_asset_id, price FROM CreateAssetOrderTransactions WHERE signature = ?", signature);
if (rs == null)
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT have_asset_id, amount, want_asset_id, price FROM CreateAssetOrderTransactions WHERE signature = ?", signature)) {
if (resultSet == null)
return null;
long haveAssetId = rs.getLong(1);
BigDecimal amount = rs.getBigDecimal(2);
long wantAssetId = rs.getLong(3);
BigDecimal price = rs.getBigDecimal(4);
long haveAssetId = resultSet.getLong(1);
BigDecimal amount = resultSet.getBigDecimal(2);
long wantAssetId = resultSet.getLong(3);
BigDecimal price = resultSet.getBigDecimal(4);
return new CreateOrderTransactionData(creatorPublicKey, haveAssetId, wantAssetId, amount, price, fee, timestamp, reference, signature);
} catch (SQLException e) {

View File

@ -20,30 +20,31 @@ public class HSQLDBCreatePollTransactionRepository extends HSQLDBTransactionRepo
}
TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException {
try {
ResultSet rs = this.repository.checkedExecute("SELECT owner, poll_name, description FROM CreatePollTransactions WHERE signature = ?", signature);
if (rs == null)
try (ResultSet resultSet = this.repository.checkedExecute("SELECT owner, poll_name, description FROM CreatePollTransactions WHERE signature = ?",
signature)) {
if (resultSet == null)
return null;
String owner = rs.getString(1);
String pollName = rs.getString(2);
String description = rs.getString(3);
String owner = resultSet.getString(1);
String pollName = resultSet.getString(2);
String description = resultSet.getString(3);
rs = this.repository.checkedExecute("SELECT option_name FROM CreatePollTransactionOptions where signature = ? ORDER BY option_index ASC",
signature);
if (rs == null)
try (ResultSet optionsResultSet = this.repository
.checkedExecute("SELECT option_name FROM CreatePollTransactionOptions where signature = ? ORDER BY option_index ASC", signature)) {
if (optionsResultSet == null)
return null;
List<PollOptionData> pollOptions = new ArrayList<PollOptionData>();
// NOTE: do-while because checkedExecute() above has already called rs.next() for us
do {
String optionName = rs.getString(1);
String optionName = optionsResultSet.getString(1);
pollOptions.add(new PollOptionData(optionName));
} while (rs.next());
} while (optionsResultSet.next());
return new CreatePollTransactionData(creatorPublicKey, owner, pollName, description, pollOptions, fee, timestamp, reference, signature);
}
} catch (SQLException e) {
throw new DataException("Unable to fetch create poll transaction from repository", e);
}

View File

@ -17,13 +17,12 @@ public class HSQLDBGenesisTransactionRepository extends HSQLDBTransactionReposit
}
TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException {
try {
ResultSet rs = this.repository.checkedExecute("SELECT recipient, amount FROM GenesisTransactions WHERE signature = ?", signature);
if (rs == null)
try (ResultSet resultSet = this.repository.checkedExecute("SELECT recipient, amount FROM GenesisTransactions WHERE signature = ?", signature)) {
if (resultSet == null)
return null;
String recipient = rs.getString(1);
BigDecimal amount = rs.getBigDecimal(2).setScale(8);
String recipient = resultSet.getString(1);
BigDecimal amount = resultSet.getBigDecimal(2).setScale(8);
return new GenesisTransactionData(recipient, amount, timestamp, signature);
} catch (SQLException e) {

View File

@ -17,20 +17,18 @@ public class HSQLDBIssueAssetTransactionRepository extends HSQLDBTransactionRepo
}
TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException {
try {
ResultSet rs = this.repository.checkedExecute(
"SELECT issuer, owner, asset_name, description, quantity, is_divisible, asset_id FROM IssueAssetTransactions WHERE signature = ?",
signature);
if (rs == null)
try (ResultSet resultSet = this.repository.checkedExecute(
"SELECT issuer, owner, asset_name, description, quantity, is_divisible, asset_id FROM IssueAssetTransactions WHERE signature = ?", signature)) {
if (resultSet == null)
return null;
byte[] issuerPublicKey = rs.getBytes(1);
String owner = rs.getString(2);
String assetName = rs.getString(3);
String description = rs.getString(4);
long quantity = rs.getLong(5);
boolean isDivisible = rs.getBoolean(6);
Long assetId = rs.getLong(7);
byte[] issuerPublicKey = resultSet.getBytes(1);
String owner = resultSet.getString(2);
String assetName = resultSet.getString(3);
String description = resultSet.getString(4);
long quantity = resultSet.getLong(5);
boolean isDivisible = resultSet.getBoolean(6);
Long assetId = resultSet.getLong(7);
return new IssueAssetTransactionData(assetId, issuerPublicKey, owner, assetName, description, quantity, isDivisible, fee, timestamp, reference,
signature);

View File

@ -17,20 +17,19 @@ public class HSQLDBMessageTransactionRepository extends HSQLDBTransactionReposit
}
TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException {
try {
ResultSet rs = this.repository.checkedExecute(
"SELECT version, sender, recipient, is_text, is_encrypted, amount, asset_id, data FROM MessageTransactions WHERE signature = ?", signature);
if (rs == null)
try (ResultSet resultSet = this.repository.checkedExecute(
"SELECT version, sender, recipient, is_text, is_encrypted, amount, asset_id, data FROM MessageTransactions WHERE signature = ?", signature)) {
if (resultSet == null)
return null;
int version = rs.getInt(1);
byte[] senderPublicKey = rs.getBytes(2);
String recipient = rs.getString(3);
boolean isText = rs.getBoolean(4);
boolean isEncrypted = rs.getBoolean(5);
BigDecimal amount = rs.getBigDecimal(6);
Long assetId = rs.getLong(7);
byte[] data = rs.getBytes(8);
int version = resultSet.getInt(1);
byte[] senderPublicKey = resultSet.getBytes(2);
String recipient = resultSet.getString(3);
boolean isText = resultSet.getBoolean(4);
boolean isEncrypted = resultSet.getBoolean(5);
BigDecimal amount = resultSet.getBigDecimal(6);
Long assetId = resultSet.getLong(7);
byte[] data = resultSet.getBytes(8);
return new MessageTransactionData(version, senderPublicKey, recipient, assetId, amount, data, isText, isEncrypted, fee, timestamp, reference,
signature);

View File

@ -19,12 +19,11 @@ public class HSQLDBMultiPaymentTransactionRepository extends HSQLDBTransactionRe
}
TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException {
try {
ResultSet rs = this.repository.checkedExecute("SELECT sender from MultiPaymentTransactions WHERE signature = ?", signature);
if (rs == null)
try (ResultSet resultSet = this.repository.checkedExecute("SELECT sender from MultiPaymentTransactions WHERE signature = ?", signature)) {
if (resultSet == null)
return null;
byte[] senderPublicKey = rs.getBytes(1);
byte[] senderPublicKey = resultSet.getBytes(1);
List<PaymentData> payments = this.getPaymentsFromSignature(signature);

View File

@ -17,14 +17,13 @@ public class HSQLDBPaymentTransactionRepository extends HSQLDBTransactionReposit
}
TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException {
try {
ResultSet rs = this.repository.checkedExecute("SELECT sender, recipient, amount FROM PaymentTransactions WHERE signature = ?", signature);
if (rs == null)
try (ResultSet resultSet = this.repository.checkedExecute("SELECT sender, recipient, amount FROM PaymentTransactions WHERE signature = ?", signature)) {
if (resultSet == null)
return null;
byte[] senderPublicKey = rs.getBytes(1);
String recipient = rs.getString(2);
BigDecimal amount = rs.getBigDecimal(3);
byte[] senderPublicKey = resultSet.getBytes(1);
String recipient = resultSet.getString(2);
BigDecimal amount = resultSet.getBigDecimal(3);
return new PaymentTransactionData(senderPublicKey, recipient, amount, fee, timestamp, reference, signature);
} catch (SQLException e) {

View File

@ -17,14 +17,13 @@ public class HSQLDBRegisterNameTransactionRepository extends HSQLDBTransactionRe
}
TransactionData fromBase(byte[] signature, byte[] reference, byte[] registrantPublicKey, long timestamp, BigDecimal fee) throws DataException {
try {
ResultSet rs = this.repository.checkedExecute("SELECT owner, name, data FROM RegisterNameTransactions WHERE signature = ?", signature);
if (rs == null)
try (ResultSet resultSet = this.repository.checkedExecute("SELECT owner, name, data FROM RegisterNameTransactions WHERE signature = ?", signature)) {
if (resultSet == null)
return null;
String owner = rs.getString(1);
String name = rs.getString(2);
String data = rs.getString(3);
String owner = resultSet.getString(1);
String name = resultSet.getString(2);
String data = resultSet.getString(3);
return new RegisterNameTransactionData(registrantPublicKey, owner, name, data, fee, timestamp, reference, signature);
} catch (SQLException e) {

View File

@ -17,13 +17,12 @@ public class HSQLDBSellNameTransactionRepository extends HSQLDBTransactionReposi
}
TransactionData fromBase(byte[] signature, byte[] reference, byte[] ownerPublicKey, long timestamp, BigDecimal fee) throws DataException {
try {
ResultSet rs = this.repository.checkedExecute("SELECT name, amount FROM SellNameTransactions WHERE signature = ?", signature);
if (rs == null)
try (ResultSet resultSet = this.repository.checkedExecute("SELECT name, amount FROM SellNameTransactions WHERE signature = ?", signature)) {
if (resultSet == null)
return null;
String name = rs.getString(1);
BigDecimal amount = rs.getBigDecimal(2);
String name = resultSet.getString(1);
BigDecimal amount = resultSet.getBigDecimal(2);
return new SellNameTransactionData(ownerPublicKey, name, amount, fee, timestamp, reference, signature);
} catch (SQLException e) {

View File

@ -5,6 +5,7 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import data.PaymentData;
@ -59,17 +60,18 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
protected HSQLDBTransactionRepository() {
}
@Override
public TransactionData fromSignature(byte[] signature) throws DataException {
try {
ResultSet rs = this.repository.checkedExecute("SELECT type, reference, creator, creation, fee FROM Transactions WHERE signature = ?", signature);
if (rs == null)
try (ResultSet resultSet = this.repository.checkedExecute("SELECT type, reference, creator, creation, fee FROM Transactions WHERE signature = ?",
signature)) {
if (resultSet == null)
return null;
TransactionType type = TransactionType.valueOf(rs.getInt(1));
byte[] reference = rs.getBytes(2);
byte[] creatorPublicKey = rs.getBytes(3);
long timestamp = rs.getTimestamp(4).getTime();
BigDecimal fee = rs.getBigDecimal(5).setScale(8);
TransactionType type = TransactionType.valueOf(resultSet.getInt(1));
byte[] reference = resultSet.getBytes(2);
byte[] creatorPublicKey = resultSet.getBytes(3);
long timestamp = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
BigDecimal fee = resultSet.getBigDecimal(5).setScale(8);
return this.fromBase(type, signature, reference, creatorPublicKey, timestamp, fee);
} catch (SQLException e) {
@ -77,17 +79,18 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
}
@Override
public TransactionData fromReference(byte[] reference) throws DataException {
try {
ResultSet rs = this.repository.checkedExecute("SELECT type, signature, creator, creation, fee FROM Transactions WHERE reference = ?", reference);
if (rs == null)
try (ResultSet resultSet = this.repository.checkedExecute("SELECT type, signature, creator, creation, fee FROM Transactions WHERE reference = ?",
reference)) {
if (resultSet == null)
return null;
TransactionType type = TransactionType.valueOf(rs.getInt(1));
byte[] signature = rs.getBytes(2);
byte[] creatorPublicKey = rs.getBytes(3);
long timestamp = rs.getTimestamp(4).getTime();
BigDecimal fee = rs.getBigDecimal(5).setScale(8);
TransactionType type = TransactionType.valueOf(resultSet.getInt(1));
byte[] signature = resultSet.getBytes(2);
byte[] creatorPublicKey = resultSet.getBytes(3);
long timestamp = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
BigDecimal fee = resultSet.getBigDecimal(5).setScale(8);
return this.fromBase(type, signature, reference, creatorPublicKey, timestamp, fee);
} catch (SQLException e) {
@ -152,21 +155,21 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
protected List<PaymentData> getPaymentsFromSignature(byte[] signature) throws DataException {
try {
ResultSet rs = this.repository.checkedExecute("SELECT recipient, amount, asset_id FROM SharedTransactionPayments WHERE signature = ?", signature);
if (rs == null)
try (ResultSet resultSet = this.repository.checkedExecute("SELECT recipient, amount, asset_id FROM SharedTransactionPayments WHERE signature = ?",
signature)) {
if (resultSet == null)
return null;
List<PaymentData> payments = new ArrayList<PaymentData>();
// NOTE: do-while because checkedExecute() above has already called rs.next() for us
do {
String recipient = rs.getString(1);
BigDecimal amount = rs.getBigDecimal(2);
long assetId = rs.getLong(3);
String recipient = resultSet.getString(1);
BigDecimal amount = resultSet.getBigDecimal(2);
long assetId = resultSet.getLong(3);
payments.add(new PaymentData(recipient, assetId, amount));
} while (rs.next());
} while (resultSet.next());
return payments;
} catch (SQLException e) {
@ -194,16 +197,15 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
if (signature == null)
return 0;
// in one go?
try {
ResultSet rs = this.repository.checkedExecute(
// Fetch height using join via block's transactions
try (ResultSet resultSet = this.repository.checkedExecute(
"SELECT height from BlockTransactions JOIN Blocks ON Blocks.signature = BlockTransactions.block_signature WHERE transaction_signature = ? LIMIT 1",
signature);
signature)) {
if (rs == null)
if (resultSet == null)
return 0;
return rs.getInt(1);
return resultSet.getInt(1);
} catch (SQLException e) {
throw new DataException("Unable to fetch transaction's height from repository", e);
}
@ -215,12 +217,12 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
return null;
// Fetch block signature (if any)
try {
ResultSet rs = this.repository.checkedExecute("SELECT block_signature from BlockTransactions WHERE transaction_signature = ? LIMIT 1", signature);
if (rs == null)
try (ResultSet resultSet = this.repository.checkedExecute("SELECT block_signature from BlockTransactions WHERE transaction_signature = ? LIMIT 1",
signature)) {
if (resultSet == null)
return null;
byte[] blockSignature = rs.getBytes(1);
byte[] blockSignature = resultSet.getBytes(1);
return this.repository.getBlockRepository().fromSignature(blockSignature);
} catch (SQLException | DataException e) {

View File

@ -17,16 +17,15 @@ public class HSQLDBTransferAssetTransactionRepository extends HSQLDBTransactionR
}
TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException {
try {
ResultSet rs = this.repository.checkedExecute("SELECT sender, recipient, asset_id, amount FROM TransferAssetTransactions WHERE signature = ?",
signature);
if (rs == null)
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT sender, recipient, asset_id, amount FROM TransferAssetTransactions WHERE signature = ?", signature)) {
if (resultSet == null)
return null;
byte[] senderPublicKey = rs.getBytes(1);
String recipient = rs.getString(2);
long assetId = rs.getLong(3);
BigDecimal amount = rs.getBigDecimal(4);
byte[] senderPublicKey = resultSet.getBytes(1);
String recipient = resultSet.getString(2);
long assetId = resultSet.getLong(3);
BigDecimal amount = resultSet.getBigDecimal(4);
return new TransferAssetTransactionData(senderPublicKey, recipient, amount, assetId, fee, timestamp, reference, signature);
} catch (SQLException e) {

View File

@ -17,15 +17,15 @@ public class HSQLDBUpdateNameTransactionRepository extends HSQLDBTransactionRepo
}
TransactionData fromBase(byte[] signature, byte[] reference, byte[] ownerPublicKey, long timestamp, BigDecimal fee) throws DataException {
try {
ResultSet rs = this.repository.checkedExecute("SELECT new_owner, name, new_data, name_reference FROM UpdateNameTransactions WHERE signature = ?", signature);
if (rs == null)
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT new_owner, name, new_data, name_reference FROM UpdateNameTransactions WHERE signature = ?", signature)) {
if (resultSet == null)
return null;
String newOwner = rs.getString(1);
String name = rs.getString(2);
String newData = rs.getString(3);
byte[] nameReference = rs.getBytes(4);
String newOwner = resultSet.getString(1);
String name = resultSet.getString(2);
String newData = resultSet.getString(3);
byte[] nameReference = resultSet.getBytes(4);
return new UpdateNameTransactionData(ownerPublicKey, newOwner, name, newData, nameReference, fee, timestamp, reference, signature);
} catch (SQLException e) {

View File

@ -17,15 +17,14 @@ public class HSQLDBVoteOnPollTransactionRepository extends HSQLDBTransactionRepo
}
TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException {
try {
ResultSet rs = this.repository
.checkedExecute("SELECT poll_name, option_index, previous_option_index FROM VoteOnPollTransactions WHERE signature = ?", signature);
if (rs == null)
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT poll_name, option_index, previous_option_index FROM VoteOnPollTransactions WHERE signature = ?", signature)) {
if (resultSet == null)
return null;
String pollName = rs.getString(1);
int optionIndex = rs.getInt(2);
Integer previousOptionIndex = rs.getInt(3);
String pollName = resultSet.getString(1);
int optionIndex = resultSet.getInt(2);
Integer previousOptionIndex = resultSet.getInt(3);
return new VoteOnPollTransactionData(creatorPublicKey, pollName, optionIndex, previousOptionIndex, fee, timestamp, reference, signature);
} catch (SQLException e) {

View File

@ -10,7 +10,8 @@ import repository.hsqldb.HSQLDBRepositoryFactory;
public class Common {
public static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true;close_result=true;sql.strict_exec=true;sql.enforce_names=true;sql.syntax_mys=true;sql.pad_space=false";
// public static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true;close_result=true;sql.strict_exec=true;sql.enforce_names=true;sql.syntax_mys=true;sql.pad_space=false";
public static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true";
@BeforeClass
public static void setRepository() throws DataException {

View File

@ -35,7 +35,7 @@ public class RepositoryTests extends Common {
@Test
public void testAccessAfterClose() throws DataException {
Repository repository = RepositoryManager.getRepository();
try (Repository repository = RepositoryManager.getRepository()) {
assertNotNull(repository);
repository.close();
@ -46,5 +46,6 @@ public class RepositoryTests extends Common {
} catch (NullPointerException | DataException e) {
}
}
}
}

View File

@ -18,17 +18,16 @@ public class PaymentTransformer extends Transformer {
// Property lengths
private static final int RECIPIENT_LENGTH = ADDRESS_LENGTH;
private static final int ASSET_ID_LENGTH = LONG_LENGTH;
private static final int AMOUNT_LENGTH = BIG_DECIMAL_LENGTH;
private static final int AMOUNT_LENGTH = 12;
private static final int TOTAL_LENGTH = RECIPIENT_LENGTH + ASSET_ID_LENGTH + AMOUNT_LENGTH;
public static PaymentData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
if (byteBuffer.remaining() < TOTAL_LENGTH)
throw new TransformationException("Byte data too short for payment information");
String recipient = Serialization.deserializeAddress(byteBuffer);
long assetId = byteBuffer.getLong();
BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer);
BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer, AMOUNT_LENGTH);
return new PaymentData(recipient, assetId, amount);
}
@ -42,8 +41,10 @@ public class PaymentTransformer extends Transformer {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
Serialization.serializeAddress(bytes, paymentData.getRecipient());
bytes.write(Longs.toByteArray(paymentData.getAssetId()));
Serialization.serializeBigDecimal(bytes, paymentData.getAmount());
Serialization.serializeBigDecimal(bytes, paymentData.getAmount(), AMOUNT_LENGTH);
return bytes.toByteArray();
} catch (IOException | ClassCastException e) {

View File

@ -79,10 +79,12 @@ public class BlockTransformer extends Transformer {
byteBuffer.get(reference);
BigDecimal generatingBalance = BigDecimal.valueOf(byteBuffer.getLong()).setScale(8);
byte[] generatorPublicKey = Serialization.deserializePublicKey(byteBuffer);
byte[] transactionsSignature = new byte[TRANSACTIONS_SIGNATURE_LENGTH];
byteBuffer.get(transactionsSignature);
byte[] generatorSignature = new byte[GENERATOR_SIGNATURE_LENGTH];
byteBuffer.get(generatorSignature);
@ -105,13 +107,16 @@ public class BlockTransformer extends Transformer {
// Parse transactions now, compared to deferred parsing in Gen1, so we can throw ParseException if need be.
List<TransactionData> transactions = new ArrayList<TransactionData>();
BigDecimal totalFees = BigDecimal.ZERO.setScale(8);
for (int t = 0; t < transactionCount; ++t) {
if (byteBuffer.remaining() < TRANSACTION_SIZE_LENGTH)
throw new TransformationException("Byte data too short for Block Transaction length");
int transactionLength = byteBuffer.getInt();
if (byteBuffer.remaining() < transactionLength)
throw new TransformationException("Byte data too short for Block Transaction");
if (transactionLength > Block.MAX_BLOCK_BYTES)
throw new TransformationException("Byte data too long for Block Transaction");
@ -293,7 +298,7 @@ public class BlockTransformer extends Transformer {
for (Transaction transaction : block.getTransactions()) {
if (!transaction.isSignatureValid())
return null;
throw new TransformationException("Transaction signature invalid when building block's transactions signature");
bytes.write(transaction.getTransactionData().getSignature());
}

View File

@ -5,6 +5,7 @@ import java.io.IOException;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.json.simple.JSONArray;
@ -16,6 +17,7 @@ import com.google.common.primitives.Longs;
import data.transaction.TransactionData;
import qora.account.PublicKeyAccount;
import qora.block.BlockChain;
import qora.transaction.ArbitraryTransaction;
import data.PaymentData;
import data.transaction.ArbitraryTransactionData;
@ -120,6 +122,34 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer {
}
}
/**
* In Qora v1, the bytes used for verification are really mangled so we need to test for v1-ness and adjust the bytes accordingly.
*
* @param transactionData
* @return byte[]
* @throws TransformationException
*/
public static byte[] toBytesForSigningImpl(TransactionData transactionData) throws TransformationException {
ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) transactionData;
byte[] bytes = TransactionTransformer.toBytesForSigningImpl(transactionData);
if (arbitraryTransactionData.getVersion() == 1 || transactionData.getTimestamp() >= BlockChain.getArbitraryTransactionV2Timestamp())
return bytes;
// Special v1 version
// In v1, a coding error means that all bytes prior to final payment entry are lost!
// If there are no payments then we can skip mangling
if (arbitraryTransactionData.getPayments().size() == 0)
return bytes;
// So we're left with: final payment entry, service, data size, data, fee
int v1Length = PaymentTransformer.getDataLength() + SERVICE_LENGTH + DATA_SIZE_LENGTH + arbitraryTransactionData.getData().length + FEE_LENGTH;
int v1Start = bytes.length - v1Length;
return Arrays.copyOfRange(bytes, v1Start, bytes.length);
}
@SuppressWarnings("unchecked")
public static JSONObject toJSON(TransactionData transactionData) throws TransformationException {
JSONObject json = TransactionTransformer.getBaseJSON(transactionData);

View File

@ -155,7 +155,7 @@ public class CreatePollTransactionTransformer extends TransactionTransformer {
// Special v1 version
// Replace transaction type with incorrect Register Name value
System.arraycopy(Ints.toByteArray(TransactionType.REGISTER_NAME.value), 0, bytes, 0, TransactionTransformer.INT_LENGTH);
System.arraycopy(Ints.toByteArray(TransactionType.REGISTER_NAME.value), 0, bytes, 0, INT_LENGTH);
System.out.println(HashCode.fromBytes(bytes).toString());

View File

@ -131,8 +131,8 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer {
// Special v1 version
// Zero duplicate signature/reference
int start = bytes.length - TransactionTransformer.SIGNATURE_LENGTH - TransactionTransformer.BIG_DECIMAL_LENGTH;
int end = start + TransactionTransformer.SIGNATURE_LENGTH;
int start = bytes.length - SIGNATURE_LENGTH - BIG_DECIMAL_LENGTH;
int end = start + SIGNATURE_LENGTH;
Arrays.fill(bytes, start, end, (byte) 0);
return bytes;

View File

@ -5,6 +5,7 @@ import java.io.IOException;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.json.simple.JSONArray;
@ -16,6 +17,7 @@ import com.google.common.primitives.Longs;
import data.transaction.TransactionData;
import qora.account.PublicKeyAccount;
import qora.block.BlockChain;
import data.PaymentData;
import data.transaction.MultiPaymentTransactionData;
import transform.PaymentTransformer;
@ -87,6 +89,29 @@ public class MultiPaymentTransactionTransformer extends TransactionTransformer {
}
}
/**
* In Qora v1, the bytes used for verification are really mangled so we need to test for v1-ness and adjust the bytes accordingly.
*
* @param transactionData
* @return byte[]
* @throws TransformationException
*/
public static byte[] toBytesForSigningImpl(TransactionData transactionData) throws TransformationException {
byte[] bytes = TransactionTransformer.toBytesForSigningImpl(transactionData);
if (transactionData.getTimestamp() >= BlockChain.getIssueAssetV2Timestamp())
return bytes;
// Special v1 version
// In v1, a coding error means that all bytes prior to final payment entry are lost!
// So we're left with: final payment entry and fee. Signature has already been stripped
int v1Length = PaymentTransformer.getDataLength() + FEE_LENGTH;
int v1Start = bytes.length - v1Length;
return Arrays.copyOfRange(bytes, v1Start, bytes.length);
}
@SuppressWarnings("unchecked")
public static JSONObject toJSON(TransactionData transactionData) throws TransformationException {
JSONObject json = TransactionTransformer.getBaseJSON(transactionData);

View File

@ -36,27 +36,27 @@ public class v1feeder extends Thread {
private static final int DEFAULT_PORT = 9084;
private static final int MAGIC_LENGTH = 4;
private static final int TYPE_LENGTH = 4;
// private static final int TYPE_LENGTH = 4;
private static final int HAS_ID_LENGTH = 1;
private static final int ID_LENGTH = 4;
private static final int DATA_SIZE_LENGTH = 4;
// private static final int ID_LENGTH = 4;
// private static final int DATA_SIZE_LENGTH = 4;
private static final int CHECKSUM_LENGTH = 4;
private static final int SIGNATURE_LENGTH = 128;
private static final byte[] MAINNET_MAGIC = { 0x12, 0x34, 0x56, 0x78 };
private static final int GET_PEERS_TYPE = 1;
private static final int PEERS_TYPE = 2;
// private static final int GET_PEERS_TYPE = 1;
// private static final int PEERS_TYPE = 2;
private static final int HEIGHT_TYPE = 3;
private static final int GET_SIGNATURES_TYPE = 4;
private static final int SIGNATURES_TYPE = 5;
private static final int GET_BLOCK_TYPE = 6;
private static final int BLOCK_TYPE = 7;
private static final int TRANSACTION_TYPE = 8;
// private static final int TRANSACTION_TYPE = 8;
private static final int PING_TYPE = 9;
private static final int VERSION_TYPE = 10;
private static final int FIND_MYSELF_TYPE = 11;
// private static final int FIND_MYSELF_TYPE = 11;
private Socket socket;
private OutputStream out;
@ -237,7 +237,7 @@ public class v1feeder extends Thread {
break;
case VERSION_TYPE:
long timestamp = byteBuffer.getLong();
@SuppressWarnings("unused") long timestamp = byteBuffer.getLong();
int versionLength = byteBuffer.getInt();
byte[] versionBytes = new byte[versionLength];
byteBuffer.get(versionBytes);
@ -299,6 +299,7 @@ public class v1feeder extends Thread {
return newBufferEnd;
}
@Override
public void run() {
try {
DataInputStream in = new DataInputStream(socket.getInputStream());