/******************************************************************************
* 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.Crypto;
import nxt.db.DbIterator;
import nxt.util.Convert;
import nxt.util.Logger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public final class Shuffler {
private static final int MAX_SHUFFLERS = Nxt.getIntProperty("nxt.maxNumberOfShufflers");
private static final Map<String, Map<Long, Shuffler>> shufflingsMap = new HashMap<>();
private static final Map<Integer, Set<String>> expirations = new HashMap<>();
public static Shuffler addOrGetShuffler(String secretPhrase, byte[] recipientPublicKey, byte[] shufflingFullHash) throws ShufflerException {
String hash = Convert.toHexString(shufflingFullHash);
long accountId = Account.getId(Crypto.getPublicKey(secretPhrase));
BlockchainImpl.getInstance().writeLock();
try {
Map<Long, Shuffler> map = shufflingsMap.get(hash);
if (map == null) {
map = new HashMap<>();
shufflingsMap.put(hash, map);
}
Shuffler shuffler = map.get(accountId);
if (recipientPublicKey == null) {
return shuffler;
}
if (shufflingsMap.size() > MAX_SHUFFLERS) {
throw new ShufflerLimitException("Cannot run more than " + MAX_SHUFFLERS + " shufflers on the same node");
}
if (shuffler == null) {
Shuffling shuffling = Shuffling.getShuffling(shufflingFullHash);
if (shuffling == null && Account.getAccount(recipientPublicKey) != null) {
throw new InvalidRecipientException("Existing account cannot be used as shuffling recipient");
}
if (getRecipientShuffler(Account.getId(recipientPublicKey)) != null) {
throw new InvalidRecipientException("Another shuffler with the same recipient account already running");
}
if (map.size() >= (shuffling == null ? Constants.MAX_NUMBER_OF_SHUFFLING_PARTICIPANTS : shuffling.getParticipantCount())) {
throw new ShufflerLimitException("Cannot run shufflers for more than " + map.size() + " accounts for this shuffling");
}
Account account = Account.getAccount(accountId);
if (account != null && account.getControls().contains(Account.ControlType.PHASING_ONLY)) {
throw new ControlledAccountException("Cannot run a shuffler for an account under phasing only control");
}
shuffler = new Shuffler(secretPhrase, recipientPublicKey, shufflingFullHash);
if (shuffling != null) {
shuffler.init(shuffling);
clearExpiration(shuffling);
}
map.put(accountId, shuffler);
Logger.logMessage(String.format("Started shuffler for account %s, shuffling %s",
Long.toUnsignedString(accountId), Long.toUnsignedString(Convert.fullHashToId(shufflingFullHash))));
} else if (!Arrays.equals(shuffler.recipientPublicKey, recipientPublicKey)) {
throw new DuplicateShufflerException("A shuffler with different recipientPublicKey already started");
} else if (!Arrays.equals(shuffler.shufflingFullHash, shufflingFullHash)) {
throw new DuplicateShufflerException("A shuffler with different shufflingFullHash already started");
} else {
Logger.logMessage("Shuffler already started");
}
return shuffler;
} finally {
BlockchainImpl.getInstance().writeUnlock();
}
}
public static List<Shuffler> getAllShufflers() {
List<Shuffler> shufflers = new ArrayList<>();
BlockchainImpl.getInstance().readLock();
try {
shufflingsMap.values().forEach(shufflerMap -> shufflers.addAll(shufflerMap.values()));
} finally {
BlockchainImpl.getInstance().readUnlock();
}
return shufflers;
}
public static List<Shuffler> getShufflingShufflers(byte[] shufflingFullHash) {
List<Shuffler> shufflers = new ArrayList<>();
BlockchainImpl.getInstance().readLock();
try {
Map<Long, Shuffler> shufflerMap = shufflingsMap.get(Convert.toHexString(shufflingFullHash));
if (shufflerMap != null) {
shufflers.addAll(shufflerMap.values());
}
} finally {
BlockchainImpl.getInstance().readUnlock();
}
return shufflers;
}
public static List<Shuffler> getAccountShufflers(long accountId) {
List<Shuffler> shufflers = new ArrayList<>();
BlockchainImpl.getInstance().readLock();
try {
shufflingsMap.values().forEach(shufflerMap -> {
Shuffler shuffler = shufflerMap.get(accountId);
if (shuffler != null) {
shufflers.add(shuffler);
}
});
} finally {
BlockchainImpl.getInstance().readUnlock();
}
return shufflers;
}
public static Shuffler getShuffler(long accountId, byte[] shufflingFullHash) {
BlockchainImpl.getInstance().readLock();
try {
Map<Long, Shuffler> shufflerMap = shufflingsMap.get(Convert.toHexString(shufflingFullHash));
if (shufflerMap != null) {
return shufflerMap.get(accountId);
}
} finally {
BlockchainImpl.getInstance().readUnlock();
}
return null;
}
public static Shuffler stopShuffler(long accountId, byte[] shufflingFullHash) {
BlockchainImpl.getInstance().writeLock();
try {
Map<Long, Shuffler> shufflerMap = shufflingsMap.get(Convert.toHexString(shufflingFullHash));
if (shufflerMap != null) {
return shufflerMap.remove(accountId);
}
} finally {
BlockchainImpl.getInstance().writeUnlock();
}
return null;
}
public static void stopAllShufflers() {
BlockchainImpl.getInstance().writeLock();
try {
shufflingsMap.clear();
} finally {
BlockchainImpl.getInstance().writeUnlock();
}
}
private static Shuffler getRecipientShuffler(long recipientId) {
BlockchainImpl.getInstance().readLock();
try {
for (Map<Long,Shuffler> shufflerMap : shufflingsMap.values()) {
for (Shuffler shuffler : shufflerMap.values()) {
if (Account.getId(shuffler.recipientPublicKey) == recipientId) {
return shuffler;
}
}
}
return null;
} finally {
BlockchainImpl.getInstance().readUnlock();
}
}
static {
Shuffling.addListener(shuffling -> {
Map<Long, Shuffler> shufflerMap = getShufflers(shuffling);
if (shufflerMap != null) {
shufflerMap.values().forEach(shuffler -> {
if (shuffler.accountId != shuffling.getIssuerId()) {
try {
shuffler.submitRegister(shuffling);
} catch (RuntimeException e) {
Logger.logErrorMessage(e.toString(), e);
}
}
});
clearExpiration(shuffling);
}
}, Shuffling.Event.SHUFFLING_CREATED);
Shuffling.addListener(shuffling -> {
Map<Long, Shuffler> shufflerMap = getShufflers(shuffling);
if (shufflerMap != null) {
Shuffler shuffler = shufflerMap.get(shuffling.getAssigneeAccountId());
if (shuffler != null) {
try {
shuffler.submitProcess(shuffling);
} catch (RuntimeException e) {
Logger.logErrorMessage(e.toString(), e);
}
}
clearExpiration(shuffling);
}
}, Shuffling.Event.SHUFFLING_PROCESSING_ASSIGNED);
Shuffling.addListener(shuffling -> {
Map<Long, Shuffler> shufflerMap = getShufflers(shuffling);
if (shufflerMap != null) {
shufflerMap.values().forEach(shuffler -> {
try {
shuffler.verify(shuffling);
} catch (RuntimeException e) {
Logger.logErrorMessage(e.toString(), e);
}
});
clearExpiration(shuffling);
}
}, Shuffling.Event.SHUFFLING_PROCESSING_FINISHED);
Shuffling.addListener(shuffling -> {
Map<Long, Shuffler> shufflerMap = getShufflers(shuffling);
if (shufflerMap != null) {
shufflerMap.values().forEach(shuffler -> {
try {
shuffler.cancel(shuffling);
} catch (RuntimeException e) {
Logger.logErrorMessage(e.toString(), e);
}
});
clearExpiration(shuffling);
}
}, Shuffling.Event.SHUFFLING_BLAME_STARTED);
Shuffling.addListener(Shuffler::scheduleExpiration, Shuffling.Event.SHUFFLING_DONE);
Shuffling.addListener(Shuffler::scheduleExpiration, Shuffling.Event.SHUFFLING_CANCELLED);
BlockchainProcessorImpl.getInstance().addListener(block -> {
Set<String> expired = expirations.get(block.getHeight());
if (expired != null) {
expired.forEach(shufflingsMap::remove);
expirations.remove(block.getHeight());
}
}, BlockchainProcessor.Event.AFTER_BLOCK_APPLY);
BlockchainProcessorImpl.getInstance().addListener(block -> shufflingsMap.values().forEach(shufflerMap -> shufflerMap.values().forEach(shuffler -> {
if (shuffler.failedTransaction != null) {
try {
TransactionProcessorImpl.getInstance().broadcast(shuffler.failedTransaction);
shuffler.failedTransaction = null;
shuffler.failureCause = null;
} catch (NxtException.ValidationException ignore) {
}
}
})), BlockchainProcessor.Event.AFTER_BLOCK_ACCEPT);
BlockchainProcessorImpl.getInstance().addListener(block -> stopAllShufflers(), BlockchainProcessor.Event.RESCAN_BEGIN);
}
private static Map<Long, Shuffler> getShufflers(Shuffling shuffling) {
return shufflingsMap.get(Convert.toHexString(shuffling.getFullHash()));
}
private static void scheduleExpiration(Shuffling shuffling) {
int expirationHeight = Nxt.getBlockchain().getHeight() + 720;
Set<String> shufflingIds = expirations.get(expirationHeight);
if (shufflingIds == null) {
shufflingIds = new HashSet<>();
expirations.put(expirationHeight, shufflingIds);
}
shufflingIds.add(Convert.toHexString(shuffling.getFullHash()));
}
private static void clearExpiration(Shuffling shuffling) {
for (Set shufflingIds : expirations.values()) {
if (shufflingIds.remove(shuffling.getId())) {
return;
}
}
}
private final long accountId;
private final String secretPhrase;
private final byte[] recipientPublicKey;
private final byte[] shufflingFullHash;
private volatile Transaction failedTransaction;
private volatile NxtException.NotCurrentlyValidException failureCause;
private Shuffler(String secretPhrase, byte[] recipientPublicKey, byte[] shufflingFullHash) {
this.secretPhrase = secretPhrase;
this.accountId = Account.getId(Crypto.getPublicKey(secretPhrase));
this.recipientPublicKey = recipientPublicKey;
this.shufflingFullHash = shufflingFullHash;
}
public long getAccountId() {
return accountId;
}
public byte[] getRecipientPublicKey() {
return recipientPublicKey;
}
public byte[] getShufflingFullHash() {
return shufflingFullHash;
}
public Transaction getFailedTransaction() {
return failedTransaction;
}
public NxtException.NotCurrentlyValidException getFailureCause() {
return failureCause;
}
private void init(Shuffling shuffling) throws ShufflerException {
ShufflingParticipant shufflingParticipant = shuffling.getParticipant(accountId);
switch (shuffling.getStage()) {
case REGISTRATION:
if (Account.getAccount(recipientPublicKey) != null) {
throw new InvalidRecipientException("Existing account cannot be used as shuffling recipient");
}
if (shufflingParticipant == null) {
submitRegister(shuffling);
}
break;
case PROCESSING:
if (shufflingParticipant == null) {
throw new InvalidStageException("Account has not registered for this shuffling");
}
if (Account.getAccount(recipientPublicKey) != null) {
throw new InvalidRecipientException("Existing account cannot be used as shuffling recipient");
}
if (accountId == shuffling.getAssigneeAccountId()) {
submitProcess(shuffling);
}
break;
case VERIFICATION:
if (shufflingParticipant == null) {
throw new InvalidStageException("Account has not registered for this shuffling");
}
if (shufflingParticipant.getState() == ShufflingParticipant.State.PROCESSED) {
verify(shuffling);
}
break;
case BLAME:
if (shufflingParticipant == null) {
throw new InvalidStageException("Account has not registered for this shuffling");
}
if (shufflingParticipant.getState() != ShufflingParticipant.State.CANCELLED) {
cancel(shuffling);
}
break;
case DONE:
case CANCELLED:
scheduleExpiration(shuffling);
break;
default:
throw new RuntimeException("Unsupported shuffling stage " + shuffling.getStage());
}
if (failureCause != null) {
throw new ShufflerException(failureCause.getMessage(), failureCause);
}
}
private void verify(Shuffling shuffling) {
if (shuffling.getParticipant(accountId).getIndex() != shuffling.getParticipantCount() - 1) {
boolean found = false;
for (byte[] key : shuffling.getRecipientPublicKeys()) {
if (Arrays.equals(key, recipientPublicKey)) {
found = true;
break;
}
}
if (found) {
submitVerify(shuffling);
} else {
submitCancel(shuffling);
}
}
}
private void cancel(Shuffling shuffling) {
if (accountId == shuffling.getAssigneeAccountId()) {
return;
}
if (shuffling.getParticipant(accountId).getIndex() == shuffling.getParticipantCount() - 1) {
return;
}
if (ShufflingParticipant.getData(shuffling.getId(), accountId) == null) {
return;
}
submitCancel(shuffling);
}
private void submitRegister(Shuffling shuffling) {
Logger.logDebugMessage("Account %s registering for shuffling %s", Long.toUnsignedString(accountId), Long.toUnsignedString(shuffling.getId()));
Attachment.ShufflingRegistration attachment = new Attachment.ShufflingRegistration(shufflingFullHash);
submitTransaction(attachment);
}
private void submitProcess(Shuffling shuffling) {
Logger.logDebugMessage("Account %s processing shuffling %s", Long.toUnsignedString(accountId), Long.toUnsignedString(shuffling.getId()));
Attachment.ShufflingAttachment attachment = shuffling.process(accountId, secretPhrase, recipientPublicKey);
submitTransaction(attachment);
}
private void submitVerify(Shuffling shuffling) {
Logger.logDebugMessage("Account %s verifying shuffling %s", Long.toUnsignedString(accountId), Long.toUnsignedString(shuffling.getId()));
Attachment.ShufflingVerification attachment = new Attachment.ShufflingVerification(shuffling.getId(), shuffling.getStateHash());
submitTransaction(attachment);
}
private void submitCancel(Shuffling shuffling) {
Logger.logDebugMessage("Account %s cancelling shuffling %s", Long.toUnsignedString(accountId), Long.toUnsignedString(shuffling.getId()));
Attachment.ShufflingCancellation attachment = shuffling.revealKeySeeds(secretPhrase, shuffling.getAssigneeAccountId(), shuffling.getStateHash());
submitTransaction(attachment);
}
private void submitTransaction(Attachment.ShufflingAttachment attachment) {
if (BlockchainProcessorImpl.getInstance().isProcessingBlock()) {
if (hasUnconfirmedTransaction(attachment, TransactionProcessorImpl.getInstance().getWaitingTransactions())) {
Logger.logDebugMessage("Transaction already submitted");
return;
}
} else {
try (DbIterator<UnconfirmedTransaction> unconfirmedTransactions = TransactionProcessorImpl.getInstance().getAllUnconfirmedTransactions()) {
if (hasUnconfirmedTransaction(attachment, unconfirmedTransactions)) {
Logger.logDebugMessage("Transaction already submitted");
return;
}
}
}
try {
Transaction.Builder builder = Nxt.newTransactionBuilder(Crypto.getPublicKey(secretPhrase), 0, 0,
(short) 1440, attachment);
Transaction transaction = builder.build(secretPhrase);
failedTransaction = null;
failureCause = null;
Account participantAccount = Account.getAccount(this.accountId);
if (participantAccount == null || transaction.getFeeNQT() > participantAccount.getUnconfirmedBalanceNQT()) {
failedTransaction = transaction;
failureCause = new NxtException.NotCurrentlyValidException("Insufficient balance");
Logger.logDebugMessage("Error submitting shuffler transaction", failureCause);
}
try {
TransactionProcessorImpl.getInstance().broadcast(transaction);
} catch (NxtException.NotCurrentlyValidException e) {
failedTransaction = transaction;
failureCause = e;
Logger.logDebugMessage("Error submitting shuffler transaction", e);
}
} catch (NxtException.ValidationException e) {
Logger.logErrorMessage("Fatal error submitting shuffler transaction", e);
}
}
private boolean hasUnconfirmedTransaction(Attachment.ShufflingAttachment shufflingAttachment, Iterable<UnconfirmedTransaction> unconfirmedTransactions) {
for (UnconfirmedTransaction unconfirmedTransaction : unconfirmedTransactions) {
if (unconfirmedTransaction.getSenderId() != accountId) {
continue;
}
Attachment attachment = unconfirmedTransaction.getAttachment();
if (!attachment.getClass().equals(shufflingAttachment.getClass())) {
continue;
}
if (Arrays.equals(shufflingAttachment.getShufflingStateHash(), ((Attachment.ShufflingAttachment)attachment).getShufflingStateHash())) {
return true;
}
}
return false;
}
public static class ShufflerException extends NxtException {
private ShufflerException(String message) {
super(message);
}
private ShufflerException(String message, Throwable cause) {
super(message, cause);
}
}
public static final class ShufflerLimitException extends ShufflerException {
private ShufflerLimitException(String message) {
super(message);
}
}
public static final class DuplicateShufflerException extends ShufflerException {
private DuplicateShufflerException(String message) {
super(message);
}
}
public static final class InvalidRecipientException extends ShufflerException {
private InvalidRecipientException(String message) {
super(message);
}
}
public static final class ControlledAccountException extends ShufflerException {
private ControlledAccountException(String message) {
super(message);
}
}
public static final class InvalidStageException extends ShufflerException {
private InvalidStageException(String message) {
super(message);
}
}
}