/****************************************************************************** * 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. * * * ******************************************************************************/ /** * Represents a single shuffling participant */ package nxt; import nxt.db.DbClause; import nxt.db.DbIterator; import nxt.db.DbKey; import nxt.db.DbUtils; import nxt.db.PrunableDbTable; import nxt.db.VersionedEntityDbTable; import nxt.util.Convert; import nxt.util.Listener; import nxt.util.Listeners; import nxt.util.Logger; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Arrays; public final class ShufflingParticipant { public enum State { REGISTERED((byte)0, new byte[]{1}), PROCESSED((byte)1, new byte[]{2,3}), VERIFIED((byte)2, new byte[]{3}), CANCELLED((byte)3, new byte[]{}); private final byte code; private final byte[] allowedNext; State(byte code, byte[] allowedNext) { this.code = code; this.allowedNext = allowedNext; } static State get(byte code) { for (State state : State.values()) { if (state.code == code) { return state; } } throw new IllegalArgumentException("No matching state for " + code); } public byte getCode() { return code; } public boolean canBecome(State nextState) { return Arrays.binarySearch(allowedNext, nextState.code) >= 0; } } public enum Event { PARTICIPANT_REGISTERED, PARTICIPANT_PROCESSED, PARTICIPANT_VERIFIED, PARTICIPANT_CANCELLED } private final static class ShufflingData { private final long shufflingId; private final long accountId; private final DbKey dbKey; private final byte[][] data; private final int transactionTimestamp; private final int height; private ShufflingData(long shufflingId, long accountId, byte[][] data, int transactionTimestamp, int height) { this.shufflingId = shufflingId; this.accountId = accountId; this.dbKey = shufflingDataDbKeyFactory.newKey(shufflingId, accountId); this.data = data; this.transactionTimestamp = transactionTimestamp; this.height = height; } private ShufflingData(ResultSet rs) throws SQLException { this.shufflingId = rs.getLong("shuffling_id"); this.accountId = rs.getLong("account_id"); this.dbKey = shufflingDataDbKeyFactory.newKey(shufflingId, accountId); this.data = DbUtils.getArray(rs, "data", byte[][].class, Convert.EMPTY_BYTES); this.transactionTimestamp = rs.getInt("transaction_timestamp"); this.height = rs.getInt("height"); } private void save(Connection con) throws SQLException { try (PreparedStatement pstmt = con.prepareStatement("INSERT INTO shuffling_data (shuffling_id, account_id, data, " + "transaction_timestamp, height) " + "VALUES (?, ?, ?, ?, ?)")) { int i = 0; pstmt.setLong(++i, this.shufflingId); pstmt.setLong(++i, this.accountId); DbUtils.setArrayEmptyToNull(pstmt, ++i, this.data); pstmt.setInt(++i, this.transactionTimestamp); pstmt.setInt(++i, this.height); pstmt.executeUpdate(); } } } private static final Listeners<ShufflingParticipant, Event> listeners = new Listeners<>(); private static final DbKey.LinkKeyFactory<ShufflingParticipant> shufflingParticipantDbKeyFactory = new DbKey.LinkKeyFactory<ShufflingParticipant>("shuffling_id", "account_id") { @Override public DbKey newKey(ShufflingParticipant participant) { return participant.dbKey; } }; private static final VersionedEntityDbTable<ShufflingParticipant> shufflingParticipantTable = new VersionedEntityDbTable<ShufflingParticipant>("shuffling_participant", shufflingParticipantDbKeyFactory) { @Override protected ShufflingParticipant load(Connection con, ResultSet rs) throws SQLException { return new ShufflingParticipant(rs); } @Override protected void save(Connection con, ShufflingParticipant participant) throws SQLException { participant.save(con); } }; private static final DbKey.LinkKeyFactory<ShufflingData> shufflingDataDbKeyFactory = new DbKey.LinkKeyFactory<ShufflingData>("shuffling_id", "account_id") { @Override public DbKey newKey(ShufflingData shufflingData) { return shufflingData.dbKey; } }; private static final PrunableDbTable<ShufflingData> shufflingDataTable = new PrunableDbTable<ShufflingData>("shuffling_data", shufflingDataDbKeyFactory) { @Override protected ShufflingData load(Connection con, ResultSet rs) throws SQLException { return new ShufflingData(rs); } @Override protected void save(Connection con, ShufflingData shufflingData) throws SQLException { shufflingData.save(con); } }; public static boolean addListener(Listener<ShufflingParticipant> listener, Event eventType) { return listeners.addListener(listener, eventType); } public static boolean removeListener(Listener<ShufflingParticipant> listener, Event eventType) { return listeners.removeListener(listener, eventType); } public static DbIterator<ShufflingParticipant> getParticipants(long shufflingId) { return shufflingParticipantTable.getManyBy(new DbClause.LongClause("shuffling_id", shufflingId), 0, -1, " ORDER BY participant_index "); } public static ShufflingParticipant getParticipant(long shufflingId, long accountId) { return shufflingParticipantTable.get(shufflingParticipantDbKeyFactory.newKey(shufflingId, accountId)); } static ShufflingParticipant getLastParticipant(long shufflingId) { return shufflingParticipantTable.getBy(new DbClause.LongClause("shuffling_id", shufflingId).and(new DbClause.NullClause("next_account_id"))); } static void addParticipant(long shufflingId, long accountId, int index) { ShufflingParticipant participant = new ShufflingParticipant(shufflingId, accountId, index); shufflingParticipantTable.insert(participant); listeners.notify(participant, Event.PARTICIPANT_REGISTERED); } static int getVerifiedCount(long shufflingId) { return shufflingParticipantTable.getCount(new DbClause.LongClause("shuffling_id", shufflingId).and( new DbClause.ByteClause("state", State.VERIFIED.getCode()))); } static void init() {} private final long shufflingId; private final long accountId; // sender account private final DbKey dbKey; private final int index; private long nextAccountId; // pointer to the next shuffling participant updated during registration private State state; // tracks the state of the participant in the process private byte[][] blameData; // encrypted data saved as intermediate result in the shuffling process private byte[][] keySeeds; // to be revealed only if shuffle is being cancelled private byte[] dataTransactionFullHash; private ShufflingParticipant(long shufflingId, long accountId, int index) { this.shufflingId = shufflingId; this.accountId = accountId; this.dbKey = shufflingParticipantDbKeyFactory.newKey(shufflingId, accountId); this.index = index; this.state = State.REGISTERED; this.blameData = Convert.EMPTY_BYTES; this.keySeeds = Convert.EMPTY_BYTES; } private ShufflingParticipant(ResultSet rs) throws SQLException { this.shufflingId = rs.getLong("shuffling_id"); this.accountId = rs.getLong("account_id"); this.dbKey = shufflingParticipantDbKeyFactory.newKey(shufflingId, accountId); this.nextAccountId = rs.getLong("next_account_id"); this.index = rs.getInt("participant_index"); this.state = State.get(rs.getByte("state")); this.blameData = DbUtils.getArray(rs, "blame_data", byte[][].class, Convert.EMPTY_BYTES); this.keySeeds = DbUtils.getArray(rs, "key_seeds", byte[][].class, Convert.EMPTY_BYTES); this.dataTransactionFullHash = rs.getBytes("data_transaction_full_hash"); } private void save(Connection con) throws SQLException { try (PreparedStatement pstmt = con.prepareStatement("MERGE INTO shuffling_participant (shuffling_id, " + "account_id, next_account_id, participant_index, state, blame_data, key_seeds, data_transaction_full_hash, height, latest) " + "KEY (shuffling_id, account_id, height) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, TRUE)")) { int i = 0; pstmt.setLong(++i, this.shufflingId); pstmt.setLong(++i, this.accountId); DbUtils.setLongZeroToNull(pstmt, ++i, this.nextAccountId); pstmt.setInt(++i, this.index); pstmt.setByte(++i, this.getState().getCode()); DbUtils.setArrayEmptyToNull(pstmt, ++i, this.blameData); DbUtils.setArrayEmptyToNull(pstmt, ++i, this.keySeeds); DbUtils.setBytes(pstmt, ++i, this.dataTransactionFullHash); pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); pstmt.executeUpdate(); } } public long getShufflingId() { return shufflingId; } public long getAccountId() { return accountId; } public long getNextAccountId() { return nextAccountId; } void setNextAccountId(long nextAccountId) { if (this.nextAccountId != 0) { throw new IllegalStateException("nextAccountId already set to " + Long.toUnsignedString(this.nextAccountId)); } this.nextAccountId = nextAccountId; shufflingParticipantTable.insert(this); } public int getIndex() { return index; } public State getState() { return state; } // caller must update database private void setState(State state) { if (!this.state.canBecome(state)) { throw new IllegalStateException(String.format("Shuffling participant in state %s cannot go to state %s", this.state, state)); } this.state = state; Logger.logDebugMessage("Shuffling participant %s changed state to %s", Long.toUnsignedString(accountId), this.state); } public byte[][] getData() { return getData(shufflingId, accountId); } static byte[][] getData(long shufflingId, long accountId) { ShufflingData shufflingData = shufflingDataTable.get(shufflingDataDbKeyFactory.newKey(shufflingId, accountId)); return shufflingData != null ? shufflingData.data : null; } void setData(byte[][] data, int timestamp) { if (data != null && Nxt.getEpochTime() - timestamp < Constants.MAX_PRUNABLE_LIFETIME && getData() == null) { shufflingDataTable.insert(new ShufflingData(shufflingId, accountId, data, timestamp, Nxt.getBlockchain().getHeight())); } } static void restoreData(long shufflingId, long accountId, byte[][] data, int timestamp, int height) { if (data != null && getData(shufflingId, accountId) == null) { shufflingDataTable.insert(new ShufflingData(shufflingId, accountId, data, timestamp, height)); } } public byte[][] getBlameData() { return blameData; } public byte[][] getKeySeeds() { return keySeeds; } void cancel(byte[][] blameData, byte[][] keySeeds) { if (this.keySeeds.length > 0) { throw new IllegalStateException("keySeeds already set"); } this.blameData = blameData; this.keySeeds = keySeeds; setState(State.CANCELLED); shufflingParticipantTable.insert(this); listeners.notify(this, Event.PARTICIPANT_CANCELLED); } public byte[] getDataTransactionFullHash() { return dataTransactionFullHash; } void setProcessed(byte[] dataTransactionFullHash) { if (this.dataTransactionFullHash != null) { throw new IllegalStateException("dataTransactionFullHash already set"); } setState(State.PROCESSED); this.dataTransactionFullHash = dataTransactionFullHash; shufflingParticipantTable.insert(this); listeners.notify(this, Event.PARTICIPANT_PROCESSED); } public ShufflingParticipant getPreviousParticipant() { if (index == 0) { return null; } return shufflingParticipantTable.getBy(new DbClause.LongClause("shuffling_id", shufflingId).and(new DbClause.IntClause("participant_index", index - 1))); } void verify() { setState(State.VERIFIED); shufflingParticipantTable.insert(this); listeners.notify(this, Event.PARTICIPANT_VERIFIED); } void delete() { shufflingParticipantTable.delete(this); } }