/****************************************************************************** * Copyright © 2013-2016 The Nxt Core Developers. * * * * See the AUTHORS.txt, DEVELOPER-AGREEMENT.txt and LICENSE.txt files at * * the top-level directory of this distribution for the individual copyright * * holder information and the developer policies on copyright and licensing. * * * * Unless otherwise agreed in a custom licensing agreement, no part of the * * Nxt software, including this file, may be copied, modified, propagated, * * or distributed except according to the terms contained in the LICENSE.txt * * file. * * * * Removal or modification of this copyright notice is prohibited. * * * ******************************************************************************/ package nxt; import nxt.crypto.HashFunction; import nxt.db.DbClause; import nxt.db.DbIterator; import nxt.db.DbKey; import nxt.db.DbUtils; import nxt.db.EntityDbTable; import nxt.db.ValuesDbTable; import nxt.util.Convert; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Set; public final class PhasingPoll extends AbstractPoll { public static final Set<HashFunction> acceptedHashFunctions = Collections.unmodifiableSet(EnumSet.of(HashFunction.SHA256, HashFunction.RIPEMD160, HashFunction.RIPEMD160_SHA256)); public static HashFunction getHashFunction(byte code) { try { HashFunction hashFunction = HashFunction.getHashFunction(code); if (acceptedHashFunctions.contains(hashFunction)) { return hashFunction; } } catch (IllegalArgumentException ignore) {} return null; } public static final class PhasingPollResult { private final long id; private final DbKey dbKey; private final long result; private final boolean approved; private final int height; private PhasingPollResult(PhasingPoll poll, long result) { this.id = poll.getId(); this.dbKey = resultDbKeyFactory.newKey(this.id); this.result = result; this.approved = result >= poll.getQuorum(); this.height = Nxt.getBlockchain().getHeight(); } private PhasingPollResult(ResultSet rs) throws SQLException { this.id = rs.getLong("id"); this.dbKey = resultDbKeyFactory.newKey(this.id); this.result = rs.getLong("result"); this.approved = rs.getBoolean("approved"); this.height = rs.getInt("height"); } private void save(Connection con) throws SQLException { try (PreparedStatement pstmt = con.prepareStatement("INSERT INTO phasing_poll_result (id, " + "result, approved, height) VALUES (?, ?, ?, ?)")) { int i = 0; pstmt.setLong(++i, id); pstmt.setLong(++i, result); pstmt.setBoolean(++i, approved); pstmt.setInt(++i, height); pstmt.executeUpdate(); } } public long getId() { return id; } public long getResult() { return result; } public boolean isApproved() { return approved; } public int getHeight() { return height; } } private static final DbKey.LongKeyFactory<PhasingPoll> phasingPollDbKeyFactory = new DbKey.LongKeyFactory<PhasingPoll>("id") { @Override public DbKey newKey(PhasingPoll poll) { return poll.dbKey; } }; private static final EntityDbTable<PhasingPoll> phasingPollTable = new EntityDbTable<PhasingPoll>("phasing_poll", phasingPollDbKeyFactory) { @Override protected PhasingPoll load(Connection con, ResultSet rs) throws SQLException { return new PhasingPoll(rs); } @Override protected void save(Connection con, PhasingPoll poll) throws SQLException { poll.save(con); } @Override public void trim(int height) { super.trim(height); try (Connection con = Db.db.getConnection(); DbIterator<PhasingPoll> pollsToTrim = phasingPollTable.getManyBy(new DbClause.IntClause("finish_height", DbClause.Op.LT, height), 0, -1); PreparedStatement pstmt1 = con.prepareStatement("DELETE FROM phasing_poll WHERE id = ?"); PreparedStatement pstmt2 = con.prepareStatement("DELETE FROM phasing_poll_voter WHERE transaction_id = ?"); PreparedStatement pstmt3 = con.prepareStatement("DELETE FROM phasing_vote WHERE transaction_id = ?"); PreparedStatement pstmt4 = con.prepareStatement("DELETE FROM phasing_poll_linked_transaction WHERE transaction_id = ?")) { while (pollsToTrim.hasNext()) { long id = pollsToTrim.next().getId(); pstmt1.setLong(1, id); pstmt1.executeUpdate(); pstmt2.setLong(1, id); pstmt2.executeUpdate(); pstmt3.setLong(1, id); pstmt3.executeUpdate(); pstmt4.setLong(1, id); pstmt4.executeUpdate(); } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } }; private static final DbKey.LongKeyFactory<PhasingPoll> votersDbKeyFactory = new DbKey.LongKeyFactory<PhasingPoll>("transaction_id") { @Override public DbKey newKey(PhasingPoll poll) { return poll.dbKey; } }; private static final ValuesDbTable<PhasingPoll, Long> votersTable = new ValuesDbTable<PhasingPoll, Long>("phasing_poll_voter", votersDbKeyFactory) { @Override protected Long load(Connection con, ResultSet rs) throws SQLException { return rs.getLong("voter_id"); } @Override protected void save(Connection con, PhasingPoll poll, Long accountId) throws SQLException { try (PreparedStatement pstmt = con.prepareStatement("INSERT INTO phasing_poll_voter (transaction_id, " + "voter_id, height) VALUES (?, ?, ?)")) { int i = 0; pstmt.setLong(++i, poll.getId()); pstmt.setLong(++i, accountId); pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); pstmt.executeUpdate(); } } }; private static final DbKey.LongKeyFactory<PhasingPoll> linkedTransactionDbKeyFactory = new DbKey.LongKeyFactory<PhasingPoll>("transaction_id") { @Override public DbKey newKey(PhasingPoll poll) { return poll.dbKey; } }; private static final ValuesDbTable<PhasingPoll, byte[]> linkedTransactionTable = new ValuesDbTable<PhasingPoll, byte[]>("phasing_poll_linked_transaction", linkedTransactionDbKeyFactory) { @Override protected byte[] load(Connection con, ResultSet rs) throws SQLException { return rs.getBytes("linked_full_hash"); } @Override protected void save(Connection con, PhasingPoll poll, byte[] linkedFullHash) throws SQLException { try (PreparedStatement pstmt = con.prepareStatement("INSERT INTO phasing_poll_linked_transaction (transaction_id, " + "linked_full_hash, linked_transaction_id, height) VALUES (?, ?, ?, ?)")) { int i = 0; pstmt.setLong(++i, poll.getId()); pstmt.setBytes(++i, linkedFullHash); pstmt.setLong(++i, Convert.fullHashToId(linkedFullHash)); pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); pstmt.executeUpdate(); } } }; private static final DbKey.LongKeyFactory<PhasingPollResult> resultDbKeyFactory = new DbKey.LongKeyFactory<PhasingPollResult>("id") { @Override public DbKey newKey(PhasingPollResult phasingPollResult) { return phasingPollResult.dbKey; } }; private static final EntityDbTable<PhasingPollResult> resultTable = new EntityDbTable<PhasingPollResult>("phasing_poll_result", resultDbKeyFactory) { @Override protected PhasingPollResult load(Connection con, ResultSet rs) throws SQLException { return new PhasingPollResult(rs); } @Override protected void save(Connection con, PhasingPollResult phasingPollResult) throws SQLException { phasingPollResult.save(con); } }; public static PhasingPollResult getResult(long id) { return resultTable.get(resultDbKeyFactory.newKey(id)); } public static DbIterator<PhasingPollResult> getApproved(int height) { return resultTable.getManyBy(new DbClause.IntClause("height", height).and(new DbClause.BooleanClause("approved", true)), 0, -1, " ORDER BY db_id ASC "); } public static PhasingPoll getPoll(long id) { return phasingPollTable.get(phasingPollDbKeyFactory.newKey(id)); } static DbIterator<TransactionImpl> getFinishingTransactions(int height) { Connection con = null; try { con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT transaction.* FROM transaction, phasing_poll " + "WHERE phasing_poll.id = transaction.id AND phasing_poll.finish_height = ? " + "ORDER BY transaction.height, transaction.transaction_index"); // ASC, not DESC pstmt.setInt(1, height); return BlockchainImpl.getInstance().getTransactions(con, pstmt); } catch (SQLException e) { DbUtils.close(con); throw new RuntimeException(e.toString(), e); } } public static DbIterator<TransactionImpl> getVoterPhasedTransactions(long voterId, int from, int to) { Connection con = null; try { con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT transaction.* " + "FROM transaction, phasing_poll_voter, phasing_poll " + "LEFT JOIN phasing_poll_result ON phasing_poll.id = phasing_poll_result.id " + "WHERE transaction.id = phasing_poll.id AND " + "phasing_poll.finish_height > ? AND " + "phasing_poll.id = phasing_poll_voter.transaction_id " + "AND phasing_poll_voter.voter_id = ? " + "AND phasing_poll_result.id IS NULL " + "ORDER BY transaction.height DESC, transaction.transaction_index DESC " + DbUtils.limitsClause(from, to)); int i = 0; pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); pstmt.setLong(++i, voterId); DbUtils.setLimits(++i, pstmt, from, to); return BlockchainImpl.getInstance().getTransactions(con, pstmt); } catch (SQLException e) { DbUtils.close(con); throw new RuntimeException(e.toString(), e); } } public static DbIterator<TransactionImpl> getHoldingPhasedTransactions(long holdingId, VoteWeighting.VotingModel votingModel, long accountId, boolean withoutWhitelist, int from, int to) { Connection con = null; try { con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT transaction.* " + "FROM transaction, phasing_poll " + "WHERE phasing_poll.holding_id = ? " + "AND phasing_poll.voting_model = ? " + "AND phasing_poll.id = transaction.id " + "AND phasing_poll.finish_height > ? " + (accountId != 0 ? "AND phasing_poll.account_id = ? " : "") + (withoutWhitelist ? "AND phasing_poll.whitelist_size = 0 " : "") + "ORDER BY transaction.height DESC, transaction.transaction_index DESC " + DbUtils.limitsClause(from, to)); int i = 0; pstmt.setLong(++i, holdingId); pstmt.setByte(++i, votingModel.getCode()); pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); if (accountId != 0) { pstmt.setLong(++i, accountId); } DbUtils.setLimits(++i, pstmt, from, to); return BlockchainImpl.getInstance().getTransactions(con, pstmt); } catch (SQLException e) { DbUtils.close(con); throw new RuntimeException(e.toString(), e); } } public static DbIterator<TransactionImpl> getAccountPhasedTransactions(long accountId, int from, int to) { Connection con = null; try { con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT transaction.* FROM transaction, phasing_poll " + " LEFT JOIN phasing_poll_result ON phasing_poll.id = phasing_poll_result.id " + " WHERE phasing_poll.id = transaction.id AND (transaction.sender_id = ? OR transaction.recipient_id = ?) " + " AND phasing_poll_result.id IS NULL " + " AND phasing_poll.finish_height > ? ORDER BY transaction.height DESC, transaction.transaction_index DESC " + DbUtils.limitsClause(from, to)); int i = 0; pstmt.setLong(++i, accountId); pstmt.setLong(++i, accountId); pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); DbUtils.setLimits(++i, pstmt, from, to); return BlockchainImpl.getInstance().getTransactions(con, pstmt); } catch (SQLException e) { DbUtils.close(con); throw new RuntimeException(e.toString(), e); } } public static int getAccountPhasedTransactionCount(long accountId) { try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT COUNT(*) FROM transaction, phasing_poll " + " LEFT JOIN phasing_poll_result ON phasing_poll.id = phasing_poll_result.id " + " WHERE phasing_poll.id = transaction.id AND (transaction.sender_id = ? OR transaction.recipient_id = ?) " + " AND phasing_poll_result.id IS NULL " + " AND phasing_poll.finish_height > ?")) { int i = 0; pstmt.setLong(++i, accountId); pstmt.setLong(++i, accountId); pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); try (ResultSet rs = pstmt.executeQuery()) { rs.next(); return rs.getInt(1); } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } public static List<? extends Transaction> getLinkedPhasedTransactions(byte[] linkedTransactionFullHash) { try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT DISTINCT transaction_id FROM phasing_poll_linked_transaction " + "WHERE linked_transaction_id = ? AND linked_full_hash = ?")) { int i = 0; pstmt.setLong(++i, Convert.fullHashToId(linkedTransactionFullHash)); pstmt.setBytes(++i, linkedTransactionFullHash); List<TransactionImpl> transactions = new ArrayList<>(); try (ResultSet rs = pstmt.executeQuery()) { while (rs.next()) { transactions.add(TransactionDb.findTransaction(rs.getLong("transaction_id"))); } } return transactions; } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } static long getSenderPhasedTransactionFees(long accountId) { try (Connection con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT SUM(transaction.fee) AS fees FROM transaction, phasing_poll " + " LEFT JOIN phasing_poll_result ON phasing_poll.id = phasing_poll_result.id " + " WHERE phasing_poll.id = transaction.id AND transaction.sender_id = ? " + " AND phasing_poll_result.id IS NULL " + " AND phasing_poll.finish_height > ? ORDER BY transaction.height DESC, transaction.transaction_index DESC ")) { int i = 0; pstmt.setLong(++i, accountId); pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); try (ResultSet rs = pstmt.executeQuery()) { rs.next(); return rs.getLong("fees"); } } catch (SQLException e) { throw new RuntimeException(e.toString(), e); } } static void addPoll(Transaction transaction, Appendix.Phasing appendix) { PhasingPoll poll = new PhasingPoll(transaction, appendix); phasingPollTable.insert(poll); long[] voters = poll.whitelist; if (voters.length > 0) { votersTable.insert(poll, Convert.toList(voters)); } if (appendix.getLinkedFullHashes().length > 0) { List<byte[]> linkedFullHashes = new ArrayList<>(appendix.getLinkedFullHashes().length); Collections.addAll(linkedFullHashes, appendix.getLinkedFullHashes()); linkedTransactionTable.insert(poll, linkedFullHashes); } } static void init() { } private final DbKey dbKey; private final long[] whitelist; private final long quorum; private final byte[] hashedSecret; private final byte algorithm; private PhasingPoll(Transaction transaction, Appendix.Phasing appendix) { super(transaction.getId(), transaction.getSenderId(), appendix.getFinishHeight(), appendix.getVoteWeighting()); this.dbKey = phasingPollDbKeyFactory.newKey(this.id); this.quorum = appendix.getQuorum(); this.whitelist = appendix.getWhitelist(); this.hashedSecret = appendix.getHashedSecret(); this.algorithm = appendix.getAlgorithm(); } private PhasingPoll(ResultSet rs) throws SQLException { super(rs); this.dbKey = phasingPollDbKeyFactory.newKey(this.id); this.quorum = rs.getLong("quorum"); this.whitelist = rs.getByte("whitelist_size") == 0 ? Convert.EMPTY_LONG : Convert.toArray(votersTable.get(votersDbKeyFactory.newKey(this))); hashedSecret = rs.getBytes("hashed_secret"); algorithm = rs.getByte("algorithm"); } void finish(long result) { PhasingPollResult phasingPollResult = new PhasingPollResult(this, result); resultTable.insert(phasingPollResult); } public long[] getWhitelist() { return whitelist; } public long getQuorum() { return quorum; } public byte[] getFullHash() { return TransactionDb.getFullHash(this.id); } public List<byte[]> getLinkedFullHashes() { return linkedTransactionTable.get(linkedTransactionDbKeyFactory.newKey(this)); } public byte[] getHashedSecret() { return hashedSecret; } public byte getAlgorithm() { return algorithm; } public boolean verifySecret(byte[] revealedSecret) { HashFunction hashFunction = getHashFunction(algorithm); return hashFunction != null && Arrays.equals(hashedSecret, hashFunction.hash(revealedSecret)); } public long countVotes() { if (voteWeighting.getVotingModel() == VoteWeighting.VotingModel.NONE) { return 0; } int height = Math.min(this.finishHeight, Nxt.getBlockchain().getHeight()); if (voteWeighting.getVotingModel() == VoteWeighting.VotingModel.TRANSACTION) { int count = 0; for (byte[] hash : getLinkedFullHashes()) { if (TransactionDb.hasTransactionByFullHash(hash, height)) { count += 1; } } return count; } if (voteWeighting.isBalanceIndependent()) { return PhasingVote.getVoteCount(this.id); } VoteWeighting.VotingModel votingModel = voteWeighting.getVotingModel(); long cumulativeWeight = 0; try (DbIterator<PhasingVote> votes = PhasingVote.getVotes(this.id, 0, Integer.MAX_VALUE)) { for (PhasingVote vote : votes) { cumulativeWeight += votingModel.calcWeight(voteWeighting, vote.getVoterId(), height); } } return cumulativeWeight; } boolean allowEarlyFinish() { return voteWeighting.isBalanceIndependent() && (whitelist.length > 0 || voteWeighting.getVotingModel() != VoteWeighting.VotingModel.ACCOUNT); } private void save(Connection con) throws SQLException { try (PreparedStatement pstmt = con.prepareStatement("INSERT INTO phasing_poll (id, account_id, " + "finish_height, whitelist_size, voting_model, quorum, min_balance, holding_id, " + "min_balance_model, hashed_secret, algorithm, height) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) { int i = 0; pstmt.setLong(++i, id); pstmt.setLong(++i, accountId); pstmt.setInt(++i, finishHeight); pstmt.setByte(++i, (byte) whitelist.length); pstmt.setByte(++i, voteWeighting.getVotingModel().getCode()); DbUtils.setLongZeroToNull(pstmt, ++i, quorum); DbUtils.setLongZeroToNull(pstmt, ++i, voteWeighting.getMinBalance()); DbUtils.setLongZeroToNull(pstmt, ++i, voteWeighting.getHoldingId()); pstmt.setByte(++i, voteWeighting.getMinBalanceModel().getCode()); DbUtils.setBytes(pstmt, ++i, hashedSecret); pstmt.setByte(++i, algorithm); pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); pstmt.executeUpdate(); } } }