/****************************************************************************** * 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.Account.ControlType; import nxt.NxtException.AccountControlException; import nxt.VoteWeighting.VotingModel; import nxt.db.DbIterator; import nxt.db.DbKey; import nxt.db.DbUtils; import nxt.db.VersionedEntityDbTable; import nxt.util.Convert; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Map; public final class AccountRestrictions { public static final class PhasingOnly { public static PhasingOnly get(long accountId) { return phasingControlTable.get(phasingControlDbKeyFactory.newKey(accountId)); } public static int getCount() { return phasingControlTable.getCount(); } public static DbIterator<PhasingOnly> getAll(int from, int to) { return phasingControlTable.getAll(from, to); } static void set(Account senderAccount, Attachment.SetPhasingOnly attachment) { PhasingParams phasingParams = attachment.getPhasingParams(); if (phasingParams.getVoteWeighting().getVotingModel() == VotingModel.NONE) { //no voting - remove the control senderAccount.removeControl(ControlType.PHASING_ONLY); PhasingOnly phasingOnly = get(senderAccount.getId()); phasingControlTable.delete(phasingOnly); } else { senderAccount.addControl(ControlType.PHASING_ONLY); PhasingOnly phasingOnly = get(senderAccount.getId()); if (phasingOnly == null) { phasingOnly = new PhasingOnly(senderAccount.getId(), phasingParams, attachment.getMaxFees(), attachment.getMinDuration(), attachment.getMaxDuration()); } else { phasingOnly.phasingParams = phasingParams; phasingOnly.maxFees = attachment.getMaxFees(); phasingOnly.minDuration = attachment.getMinDuration(); phasingOnly.maxDuration = attachment.getMaxDuration(); } phasingControlTable.insert(phasingOnly); } } private final DbKey dbKey; private final long accountId; private PhasingParams phasingParams; private long maxFees; private short minDuration; private short maxDuration; private PhasingOnly(long accountId, PhasingParams params, long maxFees, short minDuration, short maxDuration) { this.accountId = accountId; dbKey = phasingControlDbKeyFactory.newKey(this.accountId); phasingParams = params; this.maxFees = maxFees; this.minDuration = minDuration; this.maxDuration = maxDuration; } private PhasingOnly(ResultSet rs) throws SQLException { accountId = rs.getLong("account_id"); dbKey = phasingControlDbKeyFactory.newKey(this.accountId); Long[] whitelist = DbUtils.getArray(rs, "whitelist", Long[].class); phasingParams = new PhasingParams(rs.getByte("voting_model"), rs.getLong("holding_id"), rs.getLong("quorum"), rs.getLong("min_balance"), rs.getByte("min_balance_model"), whitelist == null ? Convert.EMPTY_LONG : Convert.toArray(whitelist)); this.maxFees = rs.getLong("max_fees"); this.minDuration = rs.getShort("min_duration"); this.maxDuration = rs.getShort("max_duration"); } public long getAccountId() { return accountId; } public PhasingParams getPhasingParams() { return phasingParams; } public long getMaxFees() { return maxFees; } public short getMinDuration() { return minDuration; } public short getMaxDuration() { return maxDuration; } private void checkTransaction(Transaction transaction, boolean validatingAtFinish) throws AccountControlException { if (!validatingAtFinish && maxFees > 0 && Math.addExact(transaction.getFeeNQT(), PhasingPoll.getSenderPhasedTransactionFees(transaction.getSenderId())) > maxFees) { throw new AccountControlException(String.format("Maximum total fees limit of %f NXT exceeded", ((double)maxFees)/Constants.ONE_NXT)); } if (transaction.getType() == TransactionType.Messaging.PHASING_VOTE_CASTING) { return; } Appendix.Phasing phasingAppendix = transaction.getPhasing(); if (phasingAppendix == null) { throw new AccountControlException("Non-phased transaction when phasing account control is enabled"); } if (!phasingParams.equals(phasingAppendix.getParams())) { throw new AccountControlException("Phasing parameters mismatch phasing account control. Expected: " + phasingParams.toString() + " . Actual: " + phasingAppendix.getParams().toString()); } if (!validatingAtFinish) { int duration = phasingAppendix.getFinishHeight() - Nxt.getBlockchain().getHeight(); if ((maxDuration > 0 && duration > maxDuration) || (minDuration > 0 && duration < minDuration)) { throw new AccountControlException("Invalid phasing duration " + duration); } } } private void save(Connection con) throws SQLException { try (PreparedStatement pstmt = con.prepareStatement("INSERT INTO account_control_phasing " + "(account_id, whitelist, voting_model, quorum, min_balance, holding_id, min_balance_model, " + "max_fees, min_duration, max_duration, height, latest) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, TRUE)")) { int i = 0; pstmt.setLong(++i, this.accountId); DbUtils.setArrayEmptyToNull(pstmt, ++i, Convert.toArray(phasingParams.getWhitelist())); pstmt.setByte(++i, phasingParams.getVoteWeighting().getVotingModel().getCode()); DbUtils.setLongZeroToNull(pstmt, ++i, phasingParams.getQuorum()); DbUtils.setLongZeroToNull(pstmt, ++i, phasingParams.getVoteWeighting().getMinBalance()); DbUtils.setLongZeroToNull(pstmt, ++i, phasingParams.getVoteWeighting().getHoldingId()); pstmt.setByte(++i, phasingParams.getVoteWeighting().getMinBalanceModel().getCode()); pstmt.setLong(++i, this.maxFees); pstmt.setShort(++i, this.minDuration); pstmt.setShort(++i, this.maxDuration); pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); pstmt.executeUpdate(); } } } private static final DbKey.LongKeyFactory<PhasingOnly> phasingControlDbKeyFactory = new DbKey.LongKeyFactory<PhasingOnly>("account_id") { @Override public DbKey newKey(PhasingOnly rule) { return rule.dbKey; } }; private static final VersionedEntityDbTable<PhasingOnly> phasingControlTable = new VersionedEntityDbTable<PhasingOnly>("account_control_phasing", phasingControlDbKeyFactory) { @Override protected PhasingOnly load(Connection con, ResultSet rs) throws SQLException { return new PhasingOnly(rs); } @Override protected void save(Connection con, PhasingOnly phasingOnly) throws SQLException { phasingOnly.save(con); } }; static void init() { } static void checkTransaction(Transaction transaction, boolean validatingAtFinish) throws NxtException.NotCurrentlyValidException { Account senderAccount = Account.getAccount(transaction.getSenderId()); if (senderAccount == null) { throw new NxtException.NotCurrentlyValidException("Account " + Long.toUnsignedString(transaction.getSenderId()) + " does not exist yet"); } if (senderAccount.getControls().contains(Account.ControlType.PHASING_ONLY)) { PhasingOnly phasingOnly = PhasingOnly.get(transaction.getSenderId()); phasingOnly.checkTransaction(transaction, validatingAtFinish); } } static boolean isBlockDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) { Account senderAccount = Account.getAccount(transaction.getSenderId()); if (!senderAccount.getControls().contains(Account.ControlType.PHASING_ONLY)) { return false; } if (PhasingOnly.get(transaction.getSenderId()).getMaxFees() == 0) { return false; } return transaction.getType() != TransactionType.AccountControl.SET_PHASING_ONLY && TransactionType.isDuplicate(TransactionType.AccountControl.SET_PHASING_ONLY, Long.toUnsignedString(senderAccount.getId()), duplicates, true); } }