/****************************************************************************** * 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.AnonymouslyEncryptedData; import nxt.crypto.Crypto; import nxt.db.DbClause; import nxt.db.DbIterator; import nxt.db.DbKey; import nxt.db.DbUtils; import nxt.db.VersionedEntityDbTable; import nxt.util.Convert; import nxt.util.Listener; import nxt.util.Listeners; import nxt.util.Logger; import java.security.MessageDigest; 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.HashSet; import java.util.List; import java.util.Set; public final class Shuffling { public enum Event { SHUFFLING_CREATED, SHUFFLING_PROCESSING_ASSIGNED, SHUFFLING_PROCESSING_FINISHED, SHUFFLING_BLAME_STARTED, SHUFFLING_CANCELLED, SHUFFLING_DONE } public enum Stage { REGISTRATION((byte)0, new byte[]{1,4}) { @Override byte[] getHash(Shuffling shuffling) { return shuffling.getFullHash(); } }, PROCESSING((byte)1, new byte[]{2,3,4}) { @Override byte[] getHash(Shuffling shuffling) { if (shuffling.assigneeAccountId == shuffling.issuerId) { try (DbIterator<ShufflingParticipant> participants = ShufflingParticipant.getParticipants(shuffling.id)) { return getParticipantsHash(participants); } } else { ShufflingParticipant participant = shuffling.getParticipant(shuffling.assigneeAccountId); return participant.getPreviousParticipant().getDataTransactionFullHash(); } } }, VERIFICATION((byte)2, new byte[]{3,4,5}) { @Override byte[] getHash(Shuffling shuffling) { return shuffling.getLastParticipant().getDataTransactionFullHash(); } }, BLAME((byte)3, new byte[]{4}) { @Override byte[] getHash(Shuffling shuffling) { return shuffling.getParticipant(shuffling.assigneeAccountId).getDataTransactionFullHash(); } }, CANCELLED((byte)4, new byte[]{}) { @Override byte[] getHash(Shuffling shuffling) { byte[] hash = shuffling.getLastParticipant().getDataTransactionFullHash(); if (hash != null && hash.length > 0) { return hash; } try (DbIterator<ShufflingParticipant> participants = ShufflingParticipant.getParticipants(shuffling.id)) { return getParticipantsHash(participants); } } }, DONE((byte)5, new byte[]{}) { @Override byte[] getHash(Shuffling shuffling) { return shuffling.getLastParticipant().getDataTransactionFullHash(); } }; private final byte code; private final byte[] allowedNext; Stage(byte code, byte[] allowedNext) { this.code = code; this.allowedNext = allowedNext; } public static Stage get(byte code) { for (Stage stage : Stage.values()) { if (stage.code == code) { return stage; } } throw new IllegalArgumentException("No matching stage for " + code); } public byte getCode() { return code; } public boolean canBecome(Stage nextStage) { return Arrays.binarySearch(allowedNext, nextStage.code) >= 0; } abstract byte[] getHash(Shuffling shuffling); } private static final boolean deleteFinished = Nxt.getBooleanProperty("nxt.deleteFinishedShufflings"); private static final Listeners<Shuffling, Event> listeners = new Listeners<>(); private static final DbKey.LongKeyFactory<Shuffling> shufflingDbKeyFactory = new DbKey.LongKeyFactory<Shuffling>("id") { @Override public DbKey newKey(Shuffling transfer) { return transfer.dbKey; } }; private static final VersionedEntityDbTable<Shuffling> shufflingTable = new VersionedEntityDbTable<Shuffling>("shuffling", shufflingDbKeyFactory) { @Override protected Shuffling load(Connection con, ResultSet rs) throws SQLException { return new Shuffling(rs); } @Override protected void save(Connection con, Shuffling shuffling) throws SQLException { shuffling.save(con); } }; static { Nxt.getBlockchainProcessor().addListener(block -> { if (block.getHeight() < Constants.SHUFFLING_BLOCK || block.getTransactions().size() == Constants.MAX_NUMBER_OF_TRANSACTIONS || block.getPayloadLength() > Constants.MAX_PAYLOAD_LENGTH - Constants.MIN_TRANSACTION_SIZE) { return; } List<Shuffling> shufflings = new ArrayList<>(); try (DbIterator<Shuffling> iterator = getActiveShufflings(0, -1)) { for (Shuffling shuffling : iterator) { if (!shuffling.isFull(block)) { shufflings.add(shuffling); } } } shufflings.forEach(shuffling -> { if (--shuffling.blocksRemaining <= 0) { shuffling.cancel(block); } else { shufflingTable.insert(shuffling); } }); }, BlockchainProcessor.Event.AFTER_BLOCK_APPLY); } public static boolean addListener(Listener<Shuffling> listener, Event eventType) { return listeners.addListener(listener, eventType); } public static boolean removeListener(Listener<Shuffling> listener, Event eventType) { return listeners.removeListener(listener, eventType); } public static int getCount() { return shufflingTable.getCount(); } public static int getActiveCount() { return shufflingTable.getCount(new DbClause.NotNullClause("blocks_remaining")); } public static DbIterator<Shuffling> getAll(int from, int to) { return shufflingTable.getAll(from, to, " ORDER BY blocks_remaining NULLS LAST, height DESC "); } public static DbIterator<Shuffling> getActiveShufflings(int from, int to) { return shufflingTable.getManyBy(new DbClause.NotNullClause("blocks_remaining"), from, to, " ORDER BY blocks_remaining, height DESC "); } public static Shuffling getShuffling(long shufflingId) { return shufflingTable.get(shufflingDbKeyFactory.newKey(shufflingId)); } public static Shuffling getShuffling(byte[] fullHash) { long shufflingId = Convert.fullHashToId(fullHash); Shuffling shuffling = shufflingTable.get(shufflingDbKeyFactory.newKey(shufflingId)); if (shuffling != null && !Arrays.equals(shuffling.getFullHash(), fullHash)) { Logger.logDebugMessage("Shuffling with different hash %s but same id found for hash %s", Convert.toHexString(shuffling.getFullHash()), Convert.toHexString(fullHash)); return null; } return shuffling; } public static int getHoldingShufflingCount(long holdingId, boolean includeFinished) { DbClause clause = holdingId != 0 ? new DbClause.LongClause("holding_id", holdingId) : new DbClause.NullClause("holding_id"); if (!includeFinished) { clause = clause.and(new DbClause.NotNullClause("blocks_remaining")); } return shufflingTable.getCount(clause); } public static DbIterator<Shuffling> getHoldingShufflings(long holdingId, Stage stage, boolean includeFinished, int from, int to) { DbClause clause = holdingId != 0 ? new DbClause.LongClause("holding_id", holdingId) : new DbClause.NullClause("holding_id"); if (!includeFinished) { clause = clause.and(new DbClause.NotNullClause("blocks_remaining")); } if (stage != null) { clause = clause.and(new DbClause.ByteClause("stage", stage.getCode())); } return shufflingTable.getManyBy(clause, from, to, " ORDER BY blocks_remaining NULLS LAST, height DESC "); } public static DbIterator<Shuffling> getAccountShufflings(long accountId, boolean includeFinished, int from, int to) { Connection con = null; try { con = Db.db.getConnection(); PreparedStatement pstmt = con.prepareStatement("SELECT shuffling.* FROM shuffling, shuffling_participant WHERE " + "shuffling_participant.account_id = ? AND shuffling.id = shuffling_participant.shuffling_id " + (includeFinished ? "" : "AND shuffling.blocks_remaining IS NOT NULL ") + "AND shuffling.latest = TRUE AND shuffling_participant.latest = TRUE ORDER BY blocks_remaining NULLS LAST, height DESC " + DbUtils.limitsClause(from, to)); int i = 0; pstmt.setLong(++i, accountId); DbUtils.setLimits(++i, pstmt, from, to); return shufflingTable.getManyBy(con, pstmt, false); } catch (SQLException e) { DbUtils.close(con); throw new RuntimeException(e.toString(), e); } } public static DbIterator<Shuffling> getAssignedShufflings(long assigneeAccountId, int from, int to) { return shufflingTable.getManyBy(new DbClause.LongClause("assignee_account_id", assigneeAccountId) .and(new DbClause.ByteClause("stage", Stage.PROCESSING.getCode())), from, to, " ORDER BY blocks_remaining NULLS LAST, height DESC "); } static void addShuffling(Transaction transaction, Attachment.ShufflingCreation attachment) { Shuffling shuffling = new Shuffling(transaction, attachment); shufflingTable.insert(shuffling); ShufflingParticipant.addParticipant(shuffling.getId(), transaction.getSenderId(), 0); listeners.notify(shuffling, Event.SHUFFLING_CREATED); } static void init() {} private final long id; private final DbKey dbKey; private final long holdingId; private final HoldingType holdingType; private final long issuerId; private final long amount; private final byte participantCount; private short blocksRemaining; private byte registrantCount; private Stage stage; private long assigneeAccountId; private byte[][] recipientPublicKeys; private Shuffling(Transaction transaction, Attachment.ShufflingCreation attachment) { this.id = transaction.getId(); this.dbKey = shufflingDbKeyFactory.newKey(this.id); this.holdingId = attachment.getHoldingId(); this.holdingType = attachment.getHoldingType(); this.issuerId = transaction.getSenderId(); this.amount = attachment.getAmount(); this.participantCount = attachment.getParticipantCount(); this.blocksRemaining = attachment.getRegistrationPeriod(); this.stage = Stage.REGISTRATION; this.assigneeAccountId = issuerId; this.recipientPublicKeys = Convert.EMPTY_BYTES; this.registrantCount = 1; } private Shuffling(ResultSet rs) throws SQLException { this.id = rs.getLong("id"); this.dbKey = shufflingDbKeyFactory.newKey(this.id); this.holdingId = rs.getLong("holding_id"); this.holdingType = HoldingType.get(rs.getByte("holding_type")); this.issuerId = rs.getLong("issuer_id"); this.amount = rs.getLong("amount"); this.participantCount = rs.getByte("participant_count"); this.blocksRemaining = rs.getShort("blocks_remaining"); this.stage = Stage.get(rs.getByte("stage")); this.assigneeAccountId = rs.getLong("assignee_account_id"); this.recipientPublicKeys = DbUtils.getArray(rs, "recipient_public_keys", byte[][].class, Convert.EMPTY_BYTES); this.registrantCount = rs.getByte("registrant_count"); } private void save(Connection con) throws SQLException { try (PreparedStatement pstmt = con.prepareStatement("MERGE INTO shuffling (id, holding_id, holding_type, " + "issuer_id, amount, participant_count, blocks_remaining, stage, assignee_account_id, " + "recipient_public_keys, registrant_count, height, latest) " + "KEY (id, height) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, TRUE)")) { int i = 0; pstmt.setLong(++i, this.id); DbUtils.setLongZeroToNull(pstmt, ++i, this.holdingId); pstmt.setByte(++i, this.holdingType.getCode()); pstmt.setLong(++i, this.issuerId); pstmt.setLong(++i, this.amount); pstmt.setByte(++i, this.participantCount); DbUtils.setShortZeroToNull(pstmt, ++i, this.blocksRemaining); pstmt.setByte(++i, this.getStage().getCode()); DbUtils.setLongZeroToNull(pstmt, ++i, this.assigneeAccountId); DbUtils.setArrayEmptyToNull(pstmt, ++i, this.recipientPublicKeys); pstmt.setByte(++i, this.registrantCount); pstmt.setInt(++i, Nxt.getBlockchain().getHeight()); pstmt.executeUpdate(); } } public long getId() { return id; } public long getHoldingId() { return holdingId; } public HoldingType getHoldingType() { return holdingType; } public long getIssuerId() { return issuerId; } public long getAmount() { return amount; } public byte getParticipantCount() { return participantCount; } public byte getRegistrantCount() { return registrantCount; } public short getBlocksRemaining() { return blocksRemaining; } public Stage getStage() { return stage; } // caller must update database private void setStage(Stage stage, long assigneeAccountId, short blocksRemaining) { if (!this.stage.canBecome(stage)) { throw new IllegalStateException(String.format("Shuffling in stage %s cannot go to stage %s", this.stage, stage)); } if ((stage == Stage.VERIFICATION || stage == Stage.DONE) && assigneeAccountId != 0) { throw new IllegalArgumentException(String.format("Invalid assigneeAccountId %s for stage %s", Long.toUnsignedString(assigneeAccountId), stage)); } if ((stage == Stage.REGISTRATION || stage == Stage.PROCESSING || stage == Stage.BLAME) && assigneeAccountId == 0) { throw new IllegalArgumentException(String.format("In stage %s assigneeAccountId cannot be 0", stage)); } if ((stage == Stage.DONE || stage == Stage.CANCELLED) && blocksRemaining != 0) { throw new IllegalArgumentException(String.format("For stage %s remaining blocks cannot be %s", stage, blocksRemaining)); } this.stage = stage; this.assigneeAccountId = assigneeAccountId; this.blocksRemaining = blocksRemaining; Logger.logDebugMessage("Shuffling %s entered stage %s, assignee %s, remaining blocks %s", Long.toUnsignedString(id), this.stage, Long.toUnsignedString(this.assigneeAccountId), this.blocksRemaining); } /* * Meaning of assigneeAccountId in each shuffling stage: * REGISTRATION: last currently registered participant * PROCESSING: next participant in turn to submit processing data * VERIFICATION: 0, not assigned to anyone * BLAME: the participant who initiated the blame phase * CANCELLED: the participant who got blamed for the shuffling failure, if any * DONE: 0, not assigned to anyone */ public long getAssigneeAccountId() { return assigneeAccountId; } public byte[][] getRecipientPublicKeys() { return recipientPublicKeys; } public ShufflingParticipant getParticipant(long accountId) { return ShufflingParticipant.getParticipant(id, accountId); } public ShufflingParticipant getLastParticipant() { return ShufflingParticipant.getLastParticipant(id); } public byte[] getStateHash() { return stage.getHash(this); } public byte[] getFullHash() { return TransactionDb.getFullHash(id); } public Attachment.ShufflingAttachment process(final long accountId, final String secretPhrase, final byte[] recipientPublicKey) { byte[][] data = Convert.EMPTY_BYTES; byte[] shufflingStateHash = null; int participantIndex = 0; List<ShufflingParticipant> shufflingParticipants = new ArrayList<>(); Nxt.getBlockchain().readLock(); // Read the participant list for the shuffling try (DbIterator<ShufflingParticipant> participants = ShufflingParticipant.getParticipants(id)) { for (ShufflingParticipant participant : participants) { shufflingParticipants.add(participant); if (participant.getNextAccountId() == accountId) { data = participant.getData(); shufflingStateHash = participant.getDataTransactionFullHash(); participantIndex = shufflingParticipants.size(); } } if (shufflingStateHash == null) { shufflingStateHash = getParticipantsHash(shufflingParticipants); } } finally { Nxt.getBlockchain().readUnlock(); } boolean isLast = participantIndex == participantCount - 1; // decrypt the tokens bundled in the current data List<byte[]> outputDataList = new ArrayList<>(); for (byte[] bytes : data) { AnonymouslyEncryptedData encryptedData = AnonymouslyEncryptedData.readEncryptedData(bytes); try { byte[] decrypted = encryptedData.decrypt(secretPhrase); outputDataList.add(decrypted); } catch (Exception e) { Logger.logMessage("Decryption failed", e); return isLast ? new Attachment.ShufflingRecipients(this.id, Convert.EMPTY_BYTES, shufflingStateHash) : new Attachment.ShufflingProcessing(this.id, Convert.EMPTY_BYTES, shufflingStateHash); } } // Calculate the token for the current sender by iteratively encrypting it using the public key of all the participants // which did not perform shuffle processing yet byte[] bytesToEncrypt = recipientPublicKey; byte[] nonce = Convert.toBytes(this.id); for (int i = shufflingParticipants.size() - 1; i > participantIndex; i--) { ShufflingParticipant participant = shufflingParticipants.get(i); byte[] participantPublicKey = Account.getPublicKey(participant.getAccountId()); AnonymouslyEncryptedData encryptedData = AnonymouslyEncryptedData.encrypt(bytesToEncrypt, secretPhrase, participantPublicKey, nonce); bytesToEncrypt = encryptedData.getBytes(); } outputDataList.add(bytesToEncrypt); // Shuffle the tokens and save the shuffled tokens as the participant data Collections.sort(outputDataList, Convert.byteArrayComparator); if (isLast) { Set<Long> recipientAccounts = new HashSet<>(participantCount); for (byte[] publicKey : outputDataList) { if (!Crypto.isCanonicalPublicKey(publicKey) || !recipientAccounts.add(Account.getId(publicKey))) { // duplicate or invalid recipient public key Logger.logDebugMessage("Invalid recipient public key " + Convert.toHexString(publicKey)); return new Attachment.ShufflingRecipients(this.id, Convert.EMPTY_BYTES, shufflingStateHash); } } // last participant prepares ShufflingRecipients transaction instead of ShufflingProcessing return new Attachment.ShufflingRecipients(this.id, outputDataList.toArray(new byte[outputDataList.size()][]), shufflingStateHash); } else { byte[] previous = null; for (byte[] decrypted : outputDataList) { if (previous != null && Arrays.equals(decrypted, previous)) { Logger.logDebugMessage("Duplicate decrypted data"); return new Attachment.ShufflingProcessing(this.id, Convert.EMPTY_BYTES, shufflingStateHash); } if (decrypted.length != 32 + 64 * (participantCount - participantIndex - 1)) { Logger.logDebugMessage("Invalid encrypted data length in process " + decrypted.length); return new Attachment.ShufflingProcessing(this.id, Convert.EMPTY_BYTES, shufflingStateHash); } previous = decrypted; } return new Attachment.ShufflingProcessing(this.id, outputDataList.toArray(new byte[outputDataList.size()][]), shufflingStateHash); } } public Attachment.ShufflingCancellation revealKeySeeds(final String secretPhrase, long cancellingAccountId, byte[] shufflingStateHash) { Nxt.getBlockchain().readLock(); try (DbIterator<ShufflingParticipant> participants = ShufflingParticipant.getParticipants(id)) { if (cancellingAccountId != this.assigneeAccountId) { throw new RuntimeException(String.format("Current shuffling cancellingAccountId %s does not match %s", Long.toUnsignedString(this.assigneeAccountId), Long.toUnsignedString(cancellingAccountId))); } if (shufflingStateHash == null || !Arrays.equals(shufflingStateHash, getStateHash())) { throw new RuntimeException("Current shuffling state hash does not match"); } long accountId = Account.getId(Crypto.getPublicKey(secretPhrase)); byte[][] data = null; while (participants.hasNext()) { ShufflingParticipant participant = participants.next(); if (participant.getAccountId() == accountId) { data = participant.getData(); break; } } if (!participants.hasNext()) { throw new RuntimeException("Last participant cannot have keySeeds to reveal"); } if (data == null) { throw new RuntimeException("Account " + Long.toUnsignedString(accountId) + " has not submitted data"); } final byte[] nonce = Convert.toBytes(this.id); final List<byte[]> keySeeds = new ArrayList<>(); byte[] nextParticipantPublicKey = Account.getPublicKey(participants.next().getAccountId()); byte[] keySeed = Crypto.getKeySeed(secretPhrase, nextParticipantPublicKey, nonce); keySeeds.add(keySeed); byte[] publicKey = Crypto.getPublicKey(keySeed); byte[] decryptedBytes = null; // find the data that we encrypted for (byte[] bytes : data) { AnonymouslyEncryptedData encryptedData = AnonymouslyEncryptedData.readEncryptedData(bytes); if (Arrays.equals(encryptedData.getPublicKey(), publicKey)) { try { decryptedBytes = encryptedData.decrypt(keySeed, nextParticipantPublicKey); break; } catch (Exception ignore) {} } } if (decryptedBytes == null) { throw new RuntimeException("None of the encrypted data could be decrypted"); } // decrypt all iteratively, adding the key seeds to the result while (participants.hasNext()) { nextParticipantPublicKey = Account.getPublicKey(participants.next().getAccountId()); keySeed = Crypto.getKeySeed(secretPhrase, nextParticipantPublicKey, nonce); keySeeds.add(keySeed); AnonymouslyEncryptedData encryptedData = AnonymouslyEncryptedData.readEncryptedData(decryptedBytes); decryptedBytes = encryptedData.decrypt(keySeed, nextParticipantPublicKey); } return new Attachment.ShufflingCancellation(this.id, data, keySeeds.toArray(new byte[keySeeds.size()][]), shufflingStateHash, cancellingAccountId); } finally { Nxt.getBlockchain().readUnlock(); } } void addParticipant(long participantId) { // Update the shuffling assignee to point to the new participant and update the next pointer of the existing participant // to the new participant ShufflingParticipant lastParticipant = ShufflingParticipant.getParticipant(this.id, this.assigneeAccountId); lastParticipant.setNextAccountId(participantId); ShufflingParticipant.addParticipant(this.id, participantId, this.registrantCount); this.registrantCount += 1; // Check if participant registration is complete and if so update the shuffling if (this.registrantCount == this.participantCount) { setStage(Stage.PROCESSING, this.issuerId, Constants.SHUFFLING_PROCESSING_DEADLINE); } else { this.assigneeAccountId = participantId; } shufflingTable.insert(this); if (stage == Stage.PROCESSING) { listeners.notify(this, Event.SHUFFLING_PROCESSING_ASSIGNED); } } void updateParticipantData(Transaction transaction, Attachment.ShufflingProcessing attachment) { long participantId = transaction.getSenderId(); byte[][] data = attachment.getData(); ShufflingParticipant participant = ShufflingParticipant.getParticipant(this.id, participantId); participant.setData(data, transaction.getTimestamp()); participant.setProcessed(((TransactionImpl) transaction).fullHash()); if (data != null && data.length == 0) { // couldn't decrypt all data from previous participants cancelBy(participant); return; } this.assigneeAccountId = participant.getNextAccountId(); this.blocksRemaining = Constants.SHUFFLING_PROCESSING_DEADLINE; shufflingTable.insert(this); listeners.notify(this, Event.SHUFFLING_PROCESSING_ASSIGNED); } void updateRecipients(Transaction transaction, Attachment.ShufflingRecipients attachment) { long participantId = transaction.getSenderId(); this.recipientPublicKeys = attachment.getRecipientPublicKeys(); ShufflingParticipant participant = ShufflingParticipant.getParticipant(this.id, participantId); participant.setProcessed(((TransactionImpl) transaction).fullHash()); if (recipientPublicKeys.length == 0) { // couldn't decrypt all data from previous participants cancelBy(participant); return; } participant.verify(); // last participant announces all valid recipient public keys for (byte[] recipientPublicKey : recipientPublicKeys) { long recipientId = Account.getId(recipientPublicKey); if (Account.setOrVerify(recipientId, recipientPublicKey)) { Account.addOrGetAccount(recipientId).apply(recipientPublicKey); } } setStage(Stage.VERIFICATION, 0, (short)(Constants.SHUFFLING_PROCESSING_DEADLINE + participantCount)); shufflingTable.insert(this); listeners.notify(this, Event.SHUFFLING_PROCESSING_FINISHED); } void verify(long accountId) { ShufflingParticipant.getParticipant(id, accountId).verify(); if (ShufflingParticipant.getVerifiedCount(id) == participantCount) { distribute(); } } void cancelBy(ShufflingParticipant participant, byte[][] blameData, byte[][] keySeeds) { participant.cancel(blameData, keySeeds); boolean startingBlame = this.stage != Stage.BLAME; if (startingBlame) { setStage(Stage.BLAME, participant.getAccountId(), (short) (Constants.SHUFFLING_PROCESSING_DEADLINE + participantCount)); } shufflingTable.insert(this); if (startingBlame) { listeners.notify(this, Event.SHUFFLING_BLAME_STARTED); } } private void cancelBy(ShufflingParticipant participant) { cancelBy(participant, Convert.EMPTY_BYTES, Convert.EMPTY_BYTES); } private void distribute() { if (recipientPublicKeys.length != participantCount) { cancelBy(getLastParticipant()); return; } for (byte[] recipientPublicKey : recipientPublicKeys) { byte[] publicKey = Account.getPublicKey(Account.getId(recipientPublicKey)); if (publicKey != null && !Arrays.equals(publicKey, recipientPublicKey)) { // distribution not possible, do a cancellation on behalf of last participant instead cancelBy(getLastParticipant()); return; } } AccountLedger.LedgerEvent event = AccountLedger.LedgerEvent.SHUFFLING_DISTRIBUTION; try (DbIterator<ShufflingParticipant> participants = ShufflingParticipant.getParticipants(id)) { for (ShufflingParticipant participant : participants) { Account participantAccount = Account.getAccount(participant.getAccountId()); holdingType.addToBalance(participantAccount, event, this.id, this.holdingId, -amount); if (holdingType != HoldingType.NXT) { participantAccount.addToBalanceNQT(event, this.id, -Constants.SHUFFLING_DEPOSIT_NQT); } } } for (byte[] recipientPublicKey : recipientPublicKeys) { long recipientId = Account.getId(recipientPublicKey); Account recipientAccount = Account.addOrGetAccount(recipientId); recipientAccount.apply(recipientPublicKey); holdingType.addToBalanceAndUnconfirmedBalance(recipientAccount, event, this.id, this.holdingId, amount); if (holdingType != HoldingType.NXT) { recipientAccount.addToBalanceAndUnconfirmedBalanceNQT(event, this.id, Constants.SHUFFLING_DEPOSIT_NQT); } } setStage(Stage.DONE, 0, (short)0); shufflingTable.insert(this); listeners.notify(this, Event.SHUFFLING_DONE); if (deleteFinished) { delete(); } Logger.logDebugMessage("Shuffling %s was distributed", Long.toUnsignedString(id)); } private void cancel(Block block) { AccountLedger.LedgerEvent event = AccountLedger.LedgerEvent.SHUFFLING_CANCELLATION; long blamedAccountId = blame(); try (DbIterator<ShufflingParticipant> participants = ShufflingParticipant.getParticipants(id)) { for (ShufflingParticipant participant : participants) { Account participantAccount = Account.getAccount(participant.getAccountId()); holdingType.addToUnconfirmedBalance(participantAccount, event, this.id, this.holdingId, this.amount); if (participantAccount.getId() != blamedAccountId) { if (holdingType != HoldingType.NXT) { participantAccount.addToUnconfirmedBalanceNQT(event, this.id, Constants.SHUFFLING_DEPOSIT_NQT); } } else { if (holdingType == HoldingType.NXT) { participantAccount.addToUnconfirmedBalanceNQT(event, this.id, -Constants.SHUFFLING_DEPOSIT_NQT); } participantAccount.addToBalanceNQT(event, this.id, -Constants.SHUFFLING_DEPOSIT_NQT); } } } if (blamedAccountId != 0) { // as a penalty the deposit goes to the generators of the finish block and previous 3 blocks long fee = Constants.SHUFFLING_DEPOSIT_NQT / 4; for (int i = 0; i < 3; i++) { Account previousGeneratorAccount = Account.getAccount(BlockDb.findBlockAtHeight(block.getHeight() - i - 1).getGeneratorId()); previousGeneratorAccount.addToBalanceAndUnconfirmedBalanceNQT(AccountLedger.LedgerEvent.BLOCK_GENERATED, block.getId(), fee); previousGeneratorAccount.addToForgedBalanceNQT(fee); Logger.logDebugMessage("Shuffling penalty %f NXT awarded to forger at height %d", ((double)fee) / Constants.ONE_NXT, block.getHeight() - i - 1); } fee = Constants.SHUFFLING_DEPOSIT_NQT - 3 * fee; Account blockGeneratorAccount = Account.getAccount(block.getGeneratorId()); blockGeneratorAccount.addToBalanceAndUnconfirmedBalanceNQT(AccountLedger.LedgerEvent.BLOCK_GENERATED, block.getId(), fee); blockGeneratorAccount.addToForgedBalanceNQT(fee); Logger.logDebugMessage("Shuffling penalty %f NXT awarded to forger at height %d", ((double)fee) / Constants.ONE_NXT, block.getHeight()); } setStage(Stage.CANCELLED, blamedAccountId, (short)0); shufflingTable.insert(this); listeners.notify(this, Event.SHUFFLING_CANCELLED); if (deleteFinished) { delete(); } Logger.logDebugMessage("Shuffling %s was cancelled, blaming account %s", Long.toUnsignedString(id), Long.toUnsignedString(blamedAccountId)); } private long blame() { // if registration never completed, no one is to blame if (stage == Stage.REGISTRATION) { Logger.logDebugMessage("Registration never completed for shuffling %s", Long.toUnsignedString(id)); return 0; } // if no one submitted cancellation, blame the first one that did not submit processing data if (stage == Stage.PROCESSING) { Logger.logDebugMessage("Participant %s did not submit processing", Long.toUnsignedString(assigneeAccountId)); return assigneeAccountId; } List<ShufflingParticipant> participants = new ArrayList<>(); try (DbIterator<ShufflingParticipant> iterator = ShufflingParticipant.getParticipants(this.id)) { while (iterator.hasNext()) { participants.add(iterator.next()); } } if (stage == Stage.VERIFICATION) { // if verification started, blame the first one who did not submit verification for (ShufflingParticipant participant : participants) { if (participant.getState() != ShufflingParticipant.State.VERIFIED) { Logger.logDebugMessage("Participant %s did not submit verification", Long.toUnsignedString(participant.getAccountId())); return participant.getAccountId(); } } throw new RuntimeException("All participants submitted data and verifications, blame phase should not have been entered"); } Set<Long> recipientAccounts = new HashSet<>(participantCount); // start from issuer and verify all data up, skipping last participant for (int i = 0; i < participantCount - 1; i++) { ShufflingParticipant participant = participants.get(i); byte[][] keySeeds = participant.getKeySeeds(); // if participant couldn't submit key seeds because he also couldn't decrypt some of the previous data, this should have been caught before if (keySeeds.length == 0) { Logger.logDebugMessage("Participant %s did not reveal keys", Long.toUnsignedString(participant.getAccountId())); return participant.getAccountId(); } byte[] publicKey = Crypto.getPublicKey(keySeeds[0]); AnonymouslyEncryptedData encryptedData = null; for (byte[] bytes : participant.getBlameData()) { encryptedData = AnonymouslyEncryptedData.readEncryptedData(bytes); if (Arrays.equals(publicKey, encryptedData.getPublicKey())) { // found the data that this participant encrypted break; } } if (encryptedData == null || !Arrays.equals(publicKey, encryptedData.getPublicKey())) { // participant lied about key seeds or data Logger.logDebugMessage("Participant %s did not submit blame data, or revealed invalid keys", Long.toUnsignedString(participant.getAccountId())); return participant.getAccountId(); } for (int k = i + 1; k < participantCount; k++) { ShufflingParticipant nextParticipant = participants.get(k); byte[] nextParticipantPublicKey = Account.getPublicKey(nextParticipant.getAccountId()); byte[] keySeed = keySeeds[k - i - 1]; byte[] participantBytes; try { participantBytes = encryptedData.decrypt(keySeed, nextParticipantPublicKey); } catch (Exception e) { // the next participant couldn't decrypt the data either, blame this one Logger.logDebugMessage("Could not decrypt data from participant %s", Long.toUnsignedString(participant.getAccountId())); return participant.getAccountId(); } boolean isLast = k == participantCount - 1; if (isLast) { // not encrypted data but plaintext recipient public key if (!Crypto.isCanonicalPublicKey(publicKey)) { // not a valid public key Logger.logDebugMessage("Participant %s submitted invalid recipient public key", Long.toUnsignedString(participant.getAccountId())); return participant.getAccountId(); } // check for collisions and assume they are intentional byte[] currentPublicKey = Account.getPublicKey(Account.getId(participantBytes)); if (currentPublicKey != null && !Arrays.equals(currentPublicKey, participantBytes)) { Logger.logDebugMessage("Participant %s submitted colliding recipient public key", Long.toUnsignedString(participant.getAccountId())); return participant.getAccountId(); } if (!recipientAccounts.add(Account.getId(participantBytes))) { Logger.logDebugMessage("Participant %s submitted duplicate recipient public key", Long.toUnsignedString(participant.getAccountId())); return participant.getAccountId(); } } if (nextParticipant.getState() == ShufflingParticipant.State.CANCELLED && nextParticipant.getBlameData().length == 0) { break; } boolean found = false; for (byte[] bytes : isLast ? recipientPublicKeys : nextParticipant.getBlameData()) { if (Arrays.equals(participantBytes, bytes)) { found = true; break; } } if (!found) { // the next participant did not include this participant's data Logger.logDebugMessage("Participant %s did not include previous data", Long.toUnsignedString(nextParticipant.getAccountId())); return nextParticipant.getAccountId(); } if (!isLast) { encryptedData = AnonymouslyEncryptedData.readEncryptedData(participantBytes); } } } return assigneeAccountId; } private void delete() { try (DbIterator<ShufflingParticipant> participants = ShufflingParticipant.getParticipants(id)) { for (ShufflingParticipant participant : participants) { participant.delete(); } } shufflingTable.delete(this); } private boolean isFull(Block block) { int transactionSize = Constants.MIN_TRANSACTION_SIZE; // min transaction size with no attachment if (stage == Stage.REGISTRATION) { transactionSize += 1 + 32; } else { // must use same for PROCESSING/VERIFICATION/BLAME transactionSize = 16384; // max observed was 15647 for 30 participants } return block.getPayloadLength() + transactionSize > Constants.MAX_PAYLOAD_LENGTH; } private static byte[] getParticipantsHash(Iterable<ShufflingParticipant> participants) { MessageDigest digest = Crypto.sha256(); participants.forEach(participant -> digest.update(Convert.toBytes(participant.getAccountId()))); return digest.digest(); } }