Browse Source

Added Vote-on-poll transaction

* Added simple genesis block timestamp setting to help testing

* Fixed setting account's last reference

* Moved some Poll constants from CreatePollTransaction to Poll

* HSQLDB: added TYPE PollOptionIndex
* HSQLDB: added previous_option_index to VoteOnPollTransactions table to help orphaning
* HSQLDB: renamed "poll" to "poll_name" in same table
* HSQLDB: PollOptions now has additional option_index column

* Improved TransactionTests to allow for different genesis block timestamps.
* Also added VoteOnPollTransaction test
split-DB
catbref 6 years ago
parent
commit
fe6cb4e366
  1. 57
      src/data/transaction/VoteOnPollTransactionData.java
  2. 32
      src/data/voting/VoteOnPollData.java
  3. 2
      src/qora/account/Account.java
  4. 8
      src/qora/block/GenesisBlock.java
  5. 13
      src/qora/transaction/CreatePollTransaction.java
  6. 10
      src/qora/transaction/Transaction.java
  7. 162
      src/qora/transaction/VoteOnPollTransaction.java
  8. 5
      src/qora/voting/Poll.java
  9. 11
      src/repository/VotingRepository.java
  10. 11
      src/repository/hsqldb/HSQLDBDatabaseUpdates.java
  11. 50
      src/repository/hsqldb/HSQLDBVotingRepository.java
  12. 9
      src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java
  13. 53
      src/repository/hsqldb/transaction/HSQLDBVoteOnPollTransactionRepository.java
  14. 27
      src/settings/Settings.java
  15. 74
      src/test/TransactionTests.java
  16. 18
      src/transform/transaction/CreatePollTransactionTransformer.java
  17. 12
      src/transform/transaction/TransactionTransformer.java
  18. 117
      src/transform/transaction/VoteOnPollTransactionTransformer.java

57
src/data/transaction/VoteOnPollTransactionData.java

@ -0,0 +1,57 @@
package data.transaction;
import java.math.BigDecimal;
import qora.transaction.Transaction.TransactionType;
public class VoteOnPollTransactionData extends TransactionData {
// Properties
private byte[] voterPublicKey;
private String pollName;
private int optionIndex;
private Integer previousOptionIndex;
// Constructors
public VoteOnPollTransactionData(byte[] voterPublicKey, String pollName, int optionIndex, Integer previousOptionIndex, BigDecimal fee, long timestamp,
byte[] reference, byte[] signature) {
super(TransactionType.VOTE_ON_POLL, fee, voterPublicKey, timestamp, reference, signature);
this.voterPublicKey = voterPublicKey;
this.pollName = pollName;
this.optionIndex = optionIndex;
this.previousOptionIndex = previousOptionIndex;
}
public VoteOnPollTransactionData(byte[] voterPublicKey, String pollName, int optionIndex, BigDecimal fee, long timestamp, byte[] reference,
byte[] signature) {
this(voterPublicKey, pollName, optionIndex, null, fee, timestamp, reference, signature);
}
public VoteOnPollTransactionData(byte[] voterPublicKey, String pollName, int optionIndex, BigDecimal fee, long timestamp, byte[] reference) {
this(voterPublicKey, pollName, optionIndex, null, fee, timestamp, reference, null);
}
// Getters / setters
public byte[] getVoterPublicKey() {
return this.voterPublicKey;
}
public String getPollName() {
return this.pollName;
}
public int getOptionIndex() {
return this.optionIndex;
}
public Integer getPreviousOptionIndex() {
return this.previousOptionIndex;
}
public void setPreviousOptionIndex(Integer previousOptionIndex) {
this.previousOptionIndex = previousOptionIndex;
}
}

32
src/data/voting/VoteOnPollData.java

@ -0,0 +1,32 @@
package data.voting;
public class VoteOnPollData {
// Properties
private String pollName;
private byte[] voterPublicKey;
private int optionIndex;
// Constructors
public VoteOnPollData(String pollName, byte[] voterPublicKey, int optionIndex) {
this.pollName = pollName;
this.voterPublicKey = voterPublicKey;
this.optionIndex = optionIndex;
}
// Getters/setters
public String getPollName() {
return this.pollName;
}
public byte[] getVoterPublicKey() {
return this.voterPublicKey;
}
public int getOptionIndex() {
return this.optionIndex;
}
}

2
src/qora/account/Account.java

@ -158,6 +158,8 @@ public class Account {
* @throws DataException
*/
public void setLastReference(byte[] reference) throws DataException {
accountData.setReference(reference);
this.repository.getAccountRepository().save(accountData);
}

8
src/qora/block/GenesisBlock.java

@ -17,22 +17,24 @@ import qora.crypto.Crypto;
import qora.transaction.Transaction;
import repository.DataException;
import repository.Repository;
import utils.NTP;
import settings.Settings;
public class GenesisBlock extends Block {
// Properties
private static final int GENESIS_BLOCK_VERSION = 1;
private static final byte[] GENESIS_REFERENCE = new byte[] { 1, 1, 1, 1, 1, 1, 1, 1 }; // NOTE: Neither 64 nor 128 bytes!
private static final BigDecimal GENESIS_GENERATING_BALANCE = BigDecimal.valueOf(10_000_000L).setScale(8);
private static final byte[] GENESIS_GENERATOR_PUBLIC_KEY = GenesisAccount.PUBLIC_KEY; // NOTE: 8 bytes not 32 bytes!
private static final long GENESIS_TIMESTAMP = 1400247274336L; // QORA RELEASE: Fri May 16 13:34:34.336 2014 UTC
public static final long GENESIS_TIMESTAMP = 1400247274336L; // QORA RELEASE: Fri May 16 13:34:34.336 2014 UTC
private static final byte[] GENESIS_GENERATOR_SIGNATURE = calcSignature();
private static final byte[] GENESIS_TRANSACTIONS_SIGNATURE = calcSignature();
// Constructors
public GenesisBlock(Repository repository) throws DataException {
super(repository, new BlockData(GENESIS_BLOCK_VERSION, GENESIS_REFERENCE, 0, BigDecimal.ZERO.setScale(8), GENESIS_TRANSACTIONS_SIGNATURE, 1,
GENESIS_TIMESTAMP, GENESIS_GENERATING_BALANCE, GENESIS_GENERATOR_PUBLIC_KEY, GENESIS_GENERATOR_SIGNATURE, null, null));
Settings.getInstance().getGenesisTimestamp(), GENESIS_GENERATING_BALANCE, GENESIS_GENERATOR_PUBLIC_KEY, GENESIS_GENERATOR_SIGNATURE, null, null));
this.transactions = new ArrayList<Transaction>();

13
src/qora/transaction/CreatePollTransaction.java

@ -24,11 +24,6 @@ public class CreatePollTransaction extends Transaction {
// Properties
private CreatePollTransactionData createPollTransactionData;
// Other useful constants
public static final int MAX_NAME_SIZE = 400;
public static final int MAX_DESCRIPTION_SIZE = 4000;
public static final int MAX_OPTIONS = 100;
// Constructors
public CreatePollTransaction(Repository repository, TransactionData transactionData) {
@ -89,12 +84,12 @@ public class CreatePollTransaction extends Transaction {
return ValidationResult.INVALID_ADDRESS;
// Check name size bounds
if (createPollTransactionData.getPollName().length() < 1 || createPollTransactionData.getPollName().length() > CreatePollTransaction.MAX_NAME_SIZE)
if (createPollTransactionData.getPollName().length() < 1 || createPollTransactionData.getPollName().length() > Poll.MAX_NAME_SIZE)
return ValidationResult.INVALID_NAME_LENGTH;
// Check description size bounds
if (createPollTransactionData.getDescription().length() < 1
|| createPollTransactionData.getDescription().length() > CreatePollTransaction.MAX_DESCRIPTION_SIZE)
|| createPollTransactionData.getDescription().length() > Poll.MAX_DESCRIPTION_SIZE)
return ValidationResult.INVALID_DESCRIPTION_LENGTH;
// Check poll name is lowercase
@ -110,7 +105,7 @@ public class CreatePollTransaction extends Transaction {
// Check number of options
List<PollOptionData> pollOptions = createPollTransactionData.getPollOptions();
int pollOptionsCount = pollOptions.size();
if (pollOptionsCount < 1 || pollOptionsCount > MAX_OPTIONS)
if (pollOptionsCount < 1 || pollOptionsCount > Poll.MAX_OPTIONS)
return ValidationResult.INVALID_OPTIONS_COUNT;
// Check each option
@ -118,7 +113,7 @@ public class CreatePollTransaction extends Transaction {
for (PollOptionData pollOptionData : pollOptions) {
// Check option length
int optionNameLength = pollOptionData.getOptionName().getBytes(StandardCharsets.UTF_8).length;
if (optionNameLength < 1 || optionNameLength > MAX_NAME_SIZE)
if (optionNameLength < 1 || optionNameLength > Poll.MAX_NAME_SIZE)
return ValidationResult.INVALID_OPTION_LENGTH;
// Check option is unique. NOTE: NOT case-sensitive!

10
src/qora/transaction/Transaction.java

@ -45,9 +45,10 @@ public abstract class Transaction {
public enum ValidationResult {
OK(1), INVALID_ADDRESS(2), NEGATIVE_AMOUNT(3), NEGATIVE_FEE(4), NO_BALANCE(5), INVALID_REFERENCE(6), INVALID_NAME_LENGTH(7), INVALID_AMOUNT(
15), NAME_NOT_LOWER_CASE(17), INVALID_DESCRIPTION_LENGTH(18), INVALID_OPTIONS_COUNT(19), INVALID_OPTION_LENGTH(20), DUPLICATE_OPTION(
21), POLL_ALREADY_EXISTS(22), INVALID_DATA_LENGTH(27), INVALID_QUANTITY(28), ASSET_DOES_NOT_EXIST(29), INVALID_RETURN(
30), HAVE_EQUALS_WANT(31), ORDER_DOES_NOT_EXIST(32), INVALID_ORDER_CREATOR(
33), INVALID_PAYMENTS_COUNT(34), NEGATIVE_PRICE(35), ASSET_ALREADY_EXISTS(43), NOT_YET_RELEASED(1000);
21), POLL_ALREADY_EXISTS(22), POLL_DOES_NOT_EXIST(24), POLL_OPTION_DOES_NOT_EXIST(25), ALREADY_VOTED_FOR_THAT_OPTION(
26), INVALID_DATA_LENGTH(27), INVALID_QUANTITY(28), ASSET_DOES_NOT_EXIST(29), INVALID_RETURN(30), HAVE_EQUALS_WANT(
31), ORDER_DOES_NOT_EXIST(32), INVALID_ORDER_CREATOR(
33), INVALID_PAYMENTS_COUNT(34), NEGATIVE_PRICE(35), ASSET_ALREADY_EXISTS(43), NOT_YET_RELEASED(1000);
public final int value;
@ -106,6 +107,9 @@ public abstract class Transaction {
case CREATE_POLL:
return new CreatePollTransaction(repository, transactionData);
case VOTE_ON_POLL:
return new VoteOnPollTransaction(repository, transactionData);
case ISSUE_ASSET:
return new IssueAssetTransaction(repository, transactionData);

162
src/qora/transaction/VoteOnPollTransaction.java

@ -0,0 +1,162 @@
package qora.transaction;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import data.transaction.TransactionData;
import data.transaction.VoteOnPollTransactionData;
import data.voting.PollData;
import data.voting.PollOptionData;
import data.voting.VoteOnPollData;
import qora.account.Account;
import qora.account.PublicKeyAccount;
import qora.assets.Asset;
import qora.block.BlockChain;
import qora.voting.Poll;
import repository.DataException;
import repository.Repository;
import repository.VotingRepository;
public class VoteOnPollTransaction extends Transaction {
// Properties
private VoteOnPollTransactionData voteOnPollTransactionData;
// Constructors
public VoteOnPollTransaction(Repository repository, TransactionData transactionData) {
super(repository, transactionData);
this.voteOnPollTransactionData = (VoteOnPollTransactionData) this.transactionData;
}
// 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);
if (account.getAddress().equals(this.getCreator().getAddress()))
amount = amount.subtract(this.transactionData.getFee());
return amount;
}
// Processing
@Override
public ValidationResult isValid() throws DataException {
// Are VoteOnPollTransactions even allowed at this point?
// XXX In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used?
if (this.voteOnPollTransactionData.getTimestamp() < BlockChain.VOTING_RELEASE_TIMESTAMP)
return ValidationResult.NOT_YET_RELEASED;
// Check name size bounds
if (voteOnPollTransactionData.getPollName().length() < 1 || voteOnPollTransactionData.getPollName().length() > Poll.MAX_NAME_SIZE)
return ValidationResult.INVALID_NAME_LENGTH;
// Check poll name is lowercase
if (!voteOnPollTransactionData.getPollName().equals(voteOnPollTransactionData.getPollName().toLowerCase()))
return ValidationResult.NAME_NOT_LOWER_CASE;
VotingRepository votingRepository = this.repository.getVotingRepository();
// Check poll exists
PollData pollData = votingRepository.fromPollName(voteOnPollTransactionData.getPollName());
if (pollData == null)
return ValidationResult.POLL_DOES_NOT_EXIST;
// Check poll option index is within bounds
List<PollOptionData> pollOptions = pollData.getPollOptions();
int optionIndex = voteOnPollTransactionData.getOptionIndex();
if (optionIndex < 0 || optionIndex > pollOptions.size() - 1)
return ValidationResult.POLL_OPTION_DOES_NOT_EXIST;
// Check if vote already exists
VoteOnPollData voteOnPollData = votingRepository.getVote(voteOnPollTransactionData.getPollName(), voteOnPollTransactionData.getVoterPublicKey());
if (voteOnPollData != null && voteOnPollData.getOptionIndex() == optionIndex)
return ValidationResult.ALREADY_VOTED_FOR_THAT_OPTION;
// Check fee is positive
if (voteOnPollTransactionData.getFee().compareTo(BigDecimal.ZERO) <= 0)
return ValidationResult.NEGATIVE_FEE;
// Check reference is correct
PublicKeyAccount creator = new PublicKeyAccount(this.repository, voteOnPollTransactionData.getCreatorPublicKey());
if (!Arrays.equals(creator.getLastReference(), voteOnPollTransactionData.getReference()))
return ValidationResult.INVALID_REFERENCE;
// Check issuer has enough funds
if (creator.getConfirmedBalance(Asset.QORA).compareTo(voteOnPollTransactionData.getFee()) == -1)
return ValidationResult.NO_BALANCE;
return ValidationResult.OK;
}
@Override
public void process() throws DataException {
// Update voter's balance
Account voter = new PublicKeyAccount(this.repository, voteOnPollTransactionData.getVoterPublicKey());
voter.setConfirmedBalance(Asset.QORA, voter.getConfirmedBalance(Asset.QORA).subtract(voteOnPollTransactionData.getFee()));
// Update vote's reference
voter.setLastReference(voteOnPollTransactionData.getSignature());
VotingRepository votingRepository = this.repository.getVotingRepository();
// Check for previous vote so we can save option in case of orphaning
VoteOnPollData previousVoteOnPollData = votingRepository.getVote(voteOnPollTransactionData.getPollName(),
voteOnPollTransactionData.getVoterPublicKey());
if (previousVoteOnPollData != null)
voteOnPollTransactionData.setPreviousOptionIndex(previousVoteOnPollData.getOptionIndex());
// Save this transaction, now with possible previous vote
this.repository.getTransactionRepository().save(voteOnPollTransactionData);
// Apply vote to poll
VoteOnPollData newVoteOnPollData = new VoteOnPollData(voteOnPollTransactionData.getPollName(), voteOnPollTransactionData.getVoterPublicKey(),
voteOnPollTransactionData.getOptionIndex());
votingRepository.save(newVoteOnPollData);
}
@Override
public void orphan() throws DataException {
// Update issuer's balance
Account voter = new PublicKeyAccount(this.repository, voteOnPollTransactionData.getVoterPublicKey());
voter.setConfirmedBalance(Asset.QORA, voter.getConfirmedBalance(Asset.QORA).add(voteOnPollTransactionData.getFee()));
// Update issuer's reference
voter.setLastReference(voteOnPollTransactionData.getReference());
// Does this transaction have previous vote info?
VotingRepository votingRepository = this.repository.getVotingRepository();
Integer previousOptionIndex = voteOnPollTransactionData.getPreviousOptionIndex();
if (previousOptionIndex != null) {
// Reinstate previous vote
VoteOnPollData previousVoteOnPollData = new VoteOnPollData(voteOnPollTransactionData.getPollName(), voteOnPollTransactionData.getVoterPublicKey(),
previousOptionIndex);
votingRepository.save(previousVoteOnPollData);
} else {
// Delete vote
votingRepository.delete(voteOnPollTransactionData.getPollName(), voteOnPollTransactionData.getVoterPublicKey());
}
// Delete this transaction itself
this.repository.getTransactionRepository().delete(voteOnPollTransactionData);
}
}

5
src/qora/voting/Poll.java

@ -11,6 +11,11 @@ public class Poll {
private Repository repository;
private PollData pollData;
// Other useful constants
public static final int MAX_NAME_SIZE = 400;
public static final int MAX_DESCRIPTION_SIZE = 4000;
public static final int MAX_OPTIONS = 100;
// Constructors
/**

11
src/repository/VotingRepository.java

@ -1,9 +1,12 @@
package repository;
import data.voting.PollData;
import data.voting.VoteOnPollData;
public interface VotingRepository {
// Polls
public PollData fromPollName(String pollName) throws DataException;
public boolean pollExists(String pollName) throws DataException;
@ -12,4 +15,12 @@ public interface VotingRepository {
public void delete(String pollName) throws DataException;
// Votes
public VoteOnPollData getVote(String pollName, byte[] voterPublicKey) throws DataException;
public void save(VoteOnPollData voteOnPollData) throws DataException;
public void delete(String pollName, byte[] voterPublicKey) throws DataException;
}

11
src/repository/hsqldb/HSQLDBDatabaseUpdates.java

@ -88,6 +88,7 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE TYPE NameData AS VARCHAR(4000)");
stmt.execute("CREATE TYPE PollName AS VARCHAR(400) COLLATE SQL_TEXT_UCC");
stmt.execute("CREATE TYPE PollOption AS VARCHAR(400) COLLATE SQL_TEXT_UCC");
stmt.execute("CREATE TYPE PollOptionIndex AS INTEGER");
stmt.execute("CREATE TYPE DataHash AS VARCHAR(100)");
stmt.execute("CREATE TYPE AssetID AS BIGINT");
stmt.execute("CREATE TYPE AssetName AS VARCHAR(400) COLLATE SQL_TEXT_UCC");
@ -206,8 +207,8 @@ public class HSQLDBDatabaseUpdates {
case 11:
// Vote On Poll Transactions
stmt.execute("CREATE TABLE VoteOnPollTransactions (signature Signature, voter QoraPublicKey NOT NULL, poll PollName NOT NULL, "
+ "option_index INTEGER NOT NULL, "
stmt.execute("CREATE TABLE VoteOnPollTransactions (signature Signature, voter QoraPublicKey NOT NULL, poll_name PollName NOT NULL, "
+ "option_index PollOptionIndex NOT NULL, previous_option_index PollOptionIndex, "
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
break;
@ -314,10 +315,10 @@ public class HSQLDBDatabaseUpdates {
"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))");
// Various options available on a poll
stmt.execute("CREATE TABLE PollOptions (poll_name PollName, option_name PollOption, "
+ "PRIMARY KEY (poll_name, option_name), FOREIGN KEY (poll_name) REFERENCES Polls (poll_name) ON DELETE CASCADE)");
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)");
// Actual votes cast on a poll by voting users. NOTE: only one vote per user supported at this time.
stmt.execute("CREATE TABLE PollVotes (poll_name PollName, voter QoraPublicKey, option_name PollOption, "
stmt.execute("CREATE TABLE PollVotes (poll_name PollName, voter QoraPublicKey, option_index PollOptionIndex NOT NULL, "
+ "PRIMARY KEY (poll_name, voter), FOREIGN KEY (poll_name) REFERENCES Polls (poll_name) ON DELETE CASCADE)");
// For when a user wants to lookup poll they own
stmt.execute("CREATE INDEX PollOwnerIndex on Polls (owner)");

50
src/repository/hsqldb/HSQLDBVotingRepository.java

@ -8,6 +8,7 @@ import java.util.List;
import data.voting.PollData;
import data.voting.PollOptionData;
import data.voting.VoteOnPollData;
import repository.VotingRepository;
import repository.DataException;
@ -19,6 +20,8 @@ public class HSQLDBVotingRepository implements VotingRepository {
this.repository = repository;
}
// Votes
public PollData fromPollName(String pollName) throws DataException {
try {
ResultSet resultSet = this.repository.checkedExecute("SELECT description, creator, owner, published FROM Polls WHERE poll_name = ?", pollName);
@ -30,7 +33,7 @@ public class HSQLDBVotingRepository implements VotingRepository {
String owner = resultSet.getString(3);
long published = resultSet.getTimestamp(4).getTime();
resultSet = this.repository.checkedExecute("SELECT option_name FROM PollOptions where poll_name = ?", pollName);
resultSet = this.repository.checkedExecute("SELECT option_name FROM PollOptions where poll_name = ? ORDER BY option_index ASC", pollName);
if (resultSet == null)
return null;
@ -70,10 +73,13 @@ public class HSQLDBVotingRepository implements VotingRepository {
}
// Now attempt to save poll options
for (PollOptionData pollOptionData : pollData.getPollOptions()) {
List<PollOptionData> pollOptions = pollData.getPollOptions();
for (int optionIndex = 0; optionIndex < pollOptions.size(); ++optionIndex) {
PollOptionData pollOptionData = pollOptions.get(optionIndex);
HSQLDBSaver optionSaveHelper = new HSQLDBSaver("PollOptions");
optionSaveHelper.bind("poll_name", pollData.getPollName()).bind("option_name", pollOptionData.getOptionName());
optionSaveHelper.bind("poll_name", pollData.getPollName()).bind("option_index", optionIndex).bind("option_name", pollOptionData.getOptionName());
try {
optionSaveHelper.execute(this.repository);
@ -93,4 +99,42 @@ public class HSQLDBVotingRepository implements VotingRepository {
}
}
// Votes
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);
if (resultSet == null)
return null;
int optionIndex = resultSet.getInt(1);
return new VoteOnPollData(pollName, voterPublicKey, optionIndex);
} catch (SQLException e) {
throw new DataException("Unable to fetch poll vote from repository", e);
}
}
public void save(VoteOnPollData voteOnPollData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("PollVotes");
saveHelper.bind("poll_name", voteOnPollData.getPollName()).bind("voter", voteOnPollData.getVoterPublicKey()).bind("option_index",
voteOnPollData.getOptionIndex());
try {
saveHelper.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to save poll vote into repository", e);
}
}
public void delete(String pollName, byte[] voterPublicKey) throws DataException {
try {
this.repository.checkedExecute("DELETE FROM PollVotes WHERE poll_name = ? AND voter = ?", pollName, voterPublicKey);
} catch (SQLException e) {
throw new DataException("Unable to delete poll vote from repository", e);
}
}
}

9
src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java

@ -22,6 +22,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
private HSQLDBGenesisTransactionRepository genesisTransactionRepository;
private HSQLDBPaymentTransactionRepository paymentTransactionRepository;
private HSQLDBCreatePollTransactionRepository createPollTransactionRepository;
private HSQLDBVoteOnPollTransactionRepository voteOnPollTransactionRepository;
private HSQLDBIssueAssetTransactionRepository issueAssetTransactionRepository;
private HSQLDBTransferAssetTransactionRepository transferAssetTransactionRepository;
private HSQLDBCreateOrderTransactionRepository createOrderTransactionRepository;
@ -34,6 +35,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
this.genesisTransactionRepository = new HSQLDBGenesisTransactionRepository(repository);
this.paymentTransactionRepository = new HSQLDBPaymentTransactionRepository(repository);
this.createPollTransactionRepository = new HSQLDBCreatePollTransactionRepository(repository);
this.voteOnPollTransactionRepository = new HSQLDBVoteOnPollTransactionRepository(repository);
this.issueAssetTransactionRepository = new HSQLDBIssueAssetTransactionRepository(repository);
this.transferAssetTransactionRepository = new HSQLDBTransferAssetTransactionRepository(repository);
this.createOrderTransactionRepository = new HSQLDBCreateOrderTransactionRepository(repository);
@ -93,6 +95,9 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
case CREATE_POLL:
return this.createPollTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee);
case VOTE_ON_POLL:
return this.voteOnPollTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee);
case ISSUE_ASSET:
return this.issueAssetTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee);
@ -219,6 +224,10 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
this.createPollTransactionRepository.save(transactionData);
break;
case VOTE_ON_POLL:
this.voteOnPollTransactionRepository.save(transactionData);
break;
case ISSUE_ASSET:
this.issueAssetTransactionRepository.save(transactionData);
break;

53
src/repository/hsqldb/transaction/HSQLDBVoteOnPollTransactionRepository.java

@ -0,0 +1,53 @@
package repository.hsqldb.transaction;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import data.transaction.VoteOnPollTransactionData;
import data.transaction.TransactionData;
import repository.DataException;
import repository.hsqldb.HSQLDBRepository;
import repository.hsqldb.HSQLDBSaver;
public class HSQLDBVoteOnPollTransactionRepository extends HSQLDBTransactionRepository {
public HSQLDBVoteOnPollTransactionRepository(HSQLDBRepository repository) {
this.repository = repository;
}
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)
return null;
String pollName = rs.getString(1);
int optionIndex = rs.getInt(2);
Integer previousOptionIndex = rs.getInt(3);
return new VoteOnPollTransactionData(creatorPublicKey, pollName, optionIndex, previousOptionIndex, fee, timestamp, reference, signature);
} catch (SQLException e) {
throw new DataException("Unable to fetch vote on poll transaction from repository", e);
}
}
@Override
public void save(TransactionData transactionData) throws DataException {
VoteOnPollTransactionData voteOnPollTransactionData = (VoteOnPollTransactionData) transactionData;
HSQLDBSaver saveHelper = new HSQLDBSaver("VoteOnPollTransactions");
saveHelper.bind("signature", voteOnPollTransactionData.getSignature()).bind("poll_name", voteOnPollTransactionData.getPollName())
.bind("voter", voteOnPollTransactionData.getVoterPublicKey()).bind("option_index", voteOnPollTransactionData.getOptionIndex())
.bind("previous_option_index", voteOnPollTransactionData.getPreviousOptionIndex());
try {
saveHelper.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to save vote on poll transaction into repository", e);
}
}
}

27
src/settings/Settings.java

@ -1,13 +1,38 @@
package settings;
import qora.block.GenesisBlock;
public class Settings {
private static Settings instance;
// Properties
private long genesisTimestamp = -1;
public static Settings getInstance() {
return new Settings();
if (instance == null)
instance = new Settings();
return instance;
}
public int getMaxBytePerFee() {
return 1024;
}
public long getGenesisTimestamp() {
if (this.genesisTimestamp != -1)
return this.genesisTimestamp;
return GenesisBlock.GENESIS_TIMESTAMP;
}
public void setGenesisTimestamp(long timestamp) {
this.genesisTimestamp = timestamp;
}
public void unsetGenesisTimestamp() {
this.genesisTimestamp = -1;
}
}

74
src/test/TransactionTests.java

@ -4,12 +4,10 @@ import static org.junit.Assert.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import com.google.common.hash.HashCode;
@ -19,6 +17,7 @@ import data.account.AccountData;
import data.block.BlockData;
import data.transaction.CreatePollTransactionData;
import data.transaction.PaymentTransactionData;
import data.transaction.VoteOnPollTransactionData;
import data.voting.PollData;
import data.voting.PollOptionData;
import qora.account.Account;
@ -31,12 +30,14 @@ import qora.transaction.CreatePollTransaction;
import qora.transaction.PaymentTransaction;
import qora.transaction.Transaction;
import qora.transaction.Transaction.ValidationResult;
import qora.transaction.VoteOnPollTransaction;
import repository.AccountRepository;
import repository.DataException;
import repository.Repository;
import repository.RepositoryFactory;
import repository.RepositoryManager;
import repository.hsqldb.HSQLDBRepositoryFactory;
import settings.Settings;
// Don't extend Common as we want to use an in-memory database
public class TransactionTests {
@ -57,8 +58,7 @@ public class TransactionTests {
private PrivateKeyAccount generator;
private byte[] reference;
@Before
public void createTestAccounts() throws DataException {
public void createTestAccounts(Long genesisTimestamp) throws DataException {
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl);
RepositoryManager.setRepositoryFactory(repositoryFactory);
@ -66,6 +66,12 @@ public class TransactionTests {
assertEquals("Blockchain should be empty for this test", 0, repository.getBlockRepository().getBlockchainHeight());
}
// [Un]set genesis timestamp as required by test
if (genesisTimestamp != null)
Settings.getInstance().setGenesisTimestamp(genesisTimestamp);
else
Settings.getInstance().unsetGenesisTimestamp();
// This needs to be called outside of acquiring our own repository or it will deadlock
BlockChain.validate();
@ -102,6 +108,8 @@ public class TransactionTests {
@Test
public void testPaymentTransaction() throws DataException {
createTestAccounts(null);
// Make a new payment transaction
Account recipient = new PublicKeyAccount(repository, recipientSeed);
BigDecimal amount = BigDecimal.valueOf(1_000L);
@ -144,7 +152,8 @@ public class TransactionTests {
@Test
public void testCreatePollTransaction() throws DataException {
// XXX This test fails unless GenesisBlock's timestamp is set to something after BlockChain.VOTING_RELEASE_TIMESTAMP so we need better testing setup
// This test requires GenesisBlock's timestamp is set to something after BlockChain.VOTING_RELEASE_TIMESTAMP
createTestAccounts(BlockChain.VOTING_RELEASE_TIMESTAMP + 1_000L);
// Make a new create poll transaction
String pollName = "test poll";
@ -166,7 +175,7 @@ public class TransactionTests {
assertTrue(createPollTransaction.isSignatureValid());
assertEquals(ValidationResult.OK, createPollTransaction.isValid());
// Forge new block with payment transaction
// Forge new block with transaction
Block block = new Block(repository, genesisBlockData, generator, null, null);
block.addTransaction(createPollTransactionData);
block.sign();
@ -190,6 +199,57 @@ public class TransactionTests {
// Check poll was created
PollData actualPollData = this.repository.getVotingRepository().fromPollName(pollName);
assertNotNull(actualPollData);
// Check sender's reference
assertTrue("Sender's new reference incorrect", Arrays.equals(createPollTransactionData.getSignature(), sender.getLastReference()));
// Update reference variable for use by other tests
reference = sender.getLastReference();
}
@Test
public void testVoteOnPollTransaction() throws DataException {
// Create poll using another test
testCreatePollTransaction();
// Try all options, plus invalid optionIndex (note use of <= for this)
String pollName = "test poll";
int pollOptionsSize = 3;
BigDecimal fee = BigDecimal.ONE;
long timestamp = genesisBlockData.getTimestamp() + 1_000;
BlockData previousBlockData = genesisBlockData;
for (int optionIndex = 0; optionIndex <= pollOptionsSize; ++optionIndex) {
// Make a vote-on-poll transaction
VoteOnPollTransactionData voteOnPollTransactionData = new VoteOnPollTransactionData(sender.getPublicKey(), pollName, optionIndex, fee, timestamp,
reference);
Transaction voteOnPollTransaction = new VoteOnPollTransaction(repository, voteOnPollTransactionData);
voteOnPollTransaction.calcSignature(sender);
assertTrue(voteOnPollTransaction.isSignatureValid());
if (optionIndex == pollOptionsSize) {
assertEquals(ValidationResult.POLL_OPTION_DOES_NOT_EXIST, voteOnPollTransaction.isValid());
break;
}
assertEquals(ValidationResult.OK, voteOnPollTransaction.isValid());
// Forge new block with transaction
Block block = new Block(repository, previousBlockData, generator, null, null);
block.addTransaction(voteOnPollTransactionData);
block.sign();
assertTrue("Block signatures invalid", block.isSignatureValid());
assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid());
block.process();
repository.saveChanges();
// update variables for next round
previousBlockData = block.getBlockData();
timestamp += 1_000;
reference = voteOnPollTransaction.getTransactionData().getSignature();
}
}
}

18
src/transform/transaction/CreatePollTransactionTransformer.java

@ -18,7 +18,7 @@ import data.transaction.CreatePollTransactionData;
import data.transaction.TransactionData;
import data.voting.PollOptionData;
import qora.account.PublicKeyAccount;
import qora.transaction.CreatePollTransaction;
import qora.voting.Poll;
import transform.TransformationException;
import utils.Base58;
import utils.Serialization;
@ -46,20 +46,20 @@ public class CreatePollTransactionTransformer extends TransactionTransformer {
String owner = Serialization.deserializeRecipient(byteBuffer);
String pollName = Serialization.deserializeSizedString(byteBuffer, CreatePollTransaction.MAX_NAME_SIZE);
String description = Serialization.deserializeSizedString(byteBuffer, CreatePollTransaction.MAX_DESCRIPTION_SIZE);
String pollName = Serialization.deserializeSizedString(byteBuffer, Poll.MAX_NAME_SIZE);
String description = Serialization.deserializeSizedString(byteBuffer, Poll.MAX_DESCRIPTION_SIZE);
// Make sure there are enough bytes left for poll options
if (byteBuffer.remaining() < OPTIONS_SIZE_LENGTH)
throw new TransformationException("Byte data too short for CreatePollTransaction");
int optionsCount = byteBuffer.getInt();
if (optionsCount < 1 || optionsCount > CreatePollTransaction.MAX_OPTIONS)
if (optionsCount < 1 || optionsCount > Poll.MAX_OPTIONS)
throw new TransformationException("Invalid number of options for CreatePollTransaction");
List<PollOptionData> pollOptions = new ArrayList<PollOptionData>();
for (int i = 0; i < optionsCount; ++i) {
String optionName = Serialization.deserializeSizedString(byteBuffer, CreatePollTransaction.MAX_NAME_SIZE);
for (int optionIndex = 0; optionIndex < optionsCount; ++optionIndex) {
String optionName = Serialization.deserializeSizedString(byteBuffer, Poll.MAX_NAME_SIZE);
pollOptions.add(new PollOptionData(optionName));
}
@ -106,9 +106,8 @@ public class CreatePollTransactionTransformer extends TransactionTransformer {
List<PollOptionData> pollOptions = createPollTransactionData.getPollOptions();
bytes.write(Ints.toByteArray(pollOptions.size()));
for (PollOptionData pollOptionData : pollOptions) {
for (PollOptionData pollOptionData : pollOptions)
Serialization.serializeSizedString(bytes, pollOptionData.getOptionName());
}
Serialization.serializeBigDecimal(bytes, createPollTransactionData.getFee());
@ -138,9 +137,8 @@ public class CreatePollTransactionTransformer extends TransactionTransformer {
json.put("description", createPollTransactionData.getDescription());
JSONArray options = new JSONArray();
for (PollOptionData optionData : createPollTransactionData.getPollOptions()) {
for (PollOptionData optionData : createPollTransactionData.getPollOptions())
options.add(optionData.getOptionName());
}
json.put("options", options);
} catch (ClassCastException e) {

12
src/transform/transaction/TransactionTransformer.java

@ -40,6 +40,9 @@ public class TransactionTransformer extends Transformer {
case CREATE_POLL:
return CreatePollTransactionTransformer.fromByteBuffer(byteBuffer);
case VOTE_ON_POLL:
return VoteOnPollTransactionTransformer.fromByteBuffer(byteBuffer);
case ISSUE_ASSET:
return IssueAssetTransactionTransformer.fromByteBuffer(byteBuffer);
@ -74,6 +77,9 @@ public class TransactionTransformer extends Transformer {
case CREATE_POLL:
return CreatePollTransactionTransformer.getDataLength(transactionData);
case VOTE_ON_POLL:
return VoteOnPollTransactionTransformer.getDataLength(transactionData);
case ISSUE_ASSET:
return IssueAssetTransactionTransformer.getDataLength(transactionData);
@ -108,6 +114,9 @@ public class TransactionTransformer extends Transformer {
case CREATE_POLL:
return CreatePollTransactionTransformer.toBytes(transactionData);
case VOTE_ON_POLL:
return VoteOnPollTransactionTransformer.toBytes(transactionData);
case ISSUE_ASSET:
return IssueAssetTransactionTransformer.toBytes(transactionData);
@ -142,6 +151,9 @@ public class TransactionTransformer extends Transformer {
case CREATE_POLL:
return CreatePollTransactionTransformer.toJSON(transactionData);
case VOTE_ON_POLL:
return VoteOnPollTransactionTransformer.toJSON(transactionData);
case ISSUE_ASSET:
return IssueAssetTransactionTransformer.toJSON(transactionData);

117
src/transform/transaction/VoteOnPollTransactionTransformer.java

@ -0,0 +1,117 @@
package transform.transaction;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import org.json.simple.JSONObject;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import data.transaction.TransactionData;
import data.transaction.VoteOnPollTransactionData;
import qora.account.PublicKeyAccount;
import qora.voting.Poll;
import transform.TransformationException;
import utils.Serialization;
public class VoteOnPollTransactionTransformer extends TransactionTransformer {
// Property lengths
private static final int VOTER_LENGTH = ADDRESS_LENGTH;
private static final int NAME_SIZE_LENGTH = INT_LENGTH;
private static final int OPTION_LENGTH = INT_LENGTH;
private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + VOTER_LENGTH + NAME_SIZE_LENGTH;
static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
if (byteBuffer.remaining() < TYPELESS_DATALESS_LENGTH)
throw new TransformationException("Byte data too short for VoteOnPollTransaction");
long timestamp = byteBuffer.getLong();
byte[] reference = new byte[REFERENCE_LENGTH];
byteBuffer.get(reference);
byte[] voterPublicKey = Serialization.deserializePublicKey(byteBuffer);
String pollName = Serialization.deserializeSizedString(byteBuffer, Poll.MAX_NAME_SIZE);
// Make sure there are enough bytes left for poll options
if (byteBuffer.remaining() < OPTION_LENGTH)
throw new TransformationException("Byte data too short for VoteOnPollTransaction");
int optionIndex = byteBuffer.getInt();
if (optionIndex < 0 || optionIndex >= Poll.MAX_OPTIONS)
throw new TransformationException("Invalid option number for VoteOnPollTransaction");
// Still need to make sure there are enough bytes left for remaining fields
if (byteBuffer.remaining() < FEE_LENGTH + SIGNATURE_LENGTH)
throw new TransformationException("Byte data too short for VoteOnPollTransaction");
BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer);
byte[] signature = new byte[SIGNATURE_LENGTH];
byteBuffer.get(signature);
return new VoteOnPollTransactionData(voterPublicKey, pollName, optionIndex, fee, timestamp, reference, signature);
}
public static int getDataLength(TransactionData transactionData) throws TransformationException {
VoteOnPollTransactionData voteOnPollTransactionData = (VoteOnPollTransactionData) transactionData;
int dataLength = TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + voteOnPollTransactionData.getPollName().length();
return dataLength;
}
public static byte[] toBytes(TransactionData transactionData) throws TransformationException {
try {
VoteOnPollTransactionData voteOnPollTransactionData = (VoteOnPollTransactionData) transactionData;
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(Ints.toByteArray(voteOnPollTransactionData.getType().value));
bytes.write(Longs.toByteArray(voteOnPollTransactionData.getTimestamp()));
bytes.write(voteOnPollTransactionData.getReference());
bytes.write(voteOnPollTransactionData.getVoterPublicKey());
Serialization.serializeSizedString(bytes, voteOnPollTransactionData.getPollName());
bytes.write(voteOnPollTransactionData.getOptionIndex());
Serialization.serializeBigDecimal(bytes, voteOnPollTransactionData.getFee());
if (voteOnPollTransactionData.getSignature() != null)
bytes.write(voteOnPollTransactionData.getSignature());
return bytes.toByteArray();
} catch (IOException | ClassCastException e) {
throw new TransformationException(e);
}
}
@SuppressWarnings("unchecked")
public static JSONObject toJSON(TransactionData transactionData) throws TransformationException {
JSONObject json = TransactionTransformer.getBaseJSON(transactionData);
try {
VoteOnPollTransactionData voteOnPollTransactionData = (VoteOnPollTransactionData) transactionData;
byte[] voterPublicKey = voteOnPollTransactionData.getVoterPublicKey();
json.put("voter", PublicKeyAccount.getAddress(voterPublicKey));
json.put("voterPublicKey", HashCode.fromBytes(voterPublicKey).toString());
json.put("name", voteOnPollTransactionData.getPollName());
json.put("optionIndex", voteOnPollTransactionData.getOptionIndex());
} catch (ClassCastException e) {
throw new TransformationException(e);
}
return json;
}
}
Loading…
Cancel
Save