/****************************************************************************** * 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.util.Convert; import org.json.simple.JSONObject; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.HashSet; import java.util.Map; import java.util.Set; public abstract class ShufflingTransaction extends TransactionType { private static final byte SUBTYPE_SHUFFLING_CREATION = 0; private static final byte SUBTYPE_SHUFFLING_REGISTRATION = 1; private static final byte SUBTYPE_SHUFFLING_PROCESSING = 2; private static final byte SUBTYPE_SHUFFLING_RECIPIENTS = 3; private static final byte SUBTYPE_SHUFFLING_VERIFICATION = 4; private static final byte SUBTYPE_SHUFFLING_CANCELLATION = 5; static TransactionType findTransactionType(byte subtype) { switch (subtype) { case SUBTYPE_SHUFFLING_CREATION: return SHUFFLING_CREATION; case SUBTYPE_SHUFFLING_REGISTRATION: return SHUFFLING_REGISTRATION; case SUBTYPE_SHUFFLING_PROCESSING: return SHUFFLING_PROCESSING; case SUBTYPE_SHUFFLING_RECIPIENTS: return SHUFFLING_RECIPIENTS; case SUBTYPE_SHUFFLING_VERIFICATION: return SHUFFLING_VERIFICATION; case SUBTYPE_SHUFFLING_CANCELLATION: return SHUFFLING_CANCELLATION; default: return null; } } private final static Fee SHUFFLING_PROCESSING_FEE = new Fee.ConstantFee(10 * Constants.ONE_NXT); private final static Fee SHUFFLING_RECIPIENTS_FEE = new Fee.ConstantFee(11 * Constants.ONE_NXT); private ShufflingTransaction() {} @Override public final byte getType() { return TransactionType.TYPE_SHUFFLING; } @Override public final boolean canHaveRecipient() { return false; } @Override public final boolean isPhasingSafe() { return false; } public static final TransactionType SHUFFLING_CREATION = new ShufflingTransaction() { @Override public byte getSubtype() { return SUBTYPE_SHUFFLING_CREATION; } @Override public AccountLedger.LedgerEvent getLedgerEvent() { return AccountLedger.LedgerEvent.SHUFFLING_REGISTRATION; } @Override public String getName() { return "ShufflingCreation"; } @Override Attachment.AbstractAttachment parseAttachment(ByteBuffer buffer, byte transactionVersion) { return new Attachment.ShufflingCreation(buffer, transactionVersion); } @Override Attachment.AbstractAttachment parseAttachment(JSONObject attachmentData) { return new Attachment.ShufflingCreation(attachmentData); } @Override void validateAttachment(Transaction transaction) throws NxtException.ValidationException { if (Nxt.getBlockchain().getHeight() < Constants.SHUFFLING_BLOCK) { throw new NxtException.NotYetEnabledException("Shuffling not yet enabled"); } Attachment.ShufflingCreation attachment = (Attachment.ShufflingCreation) transaction.getAttachment(); HoldingType holdingType = attachment.getHoldingType(); long amount = attachment.getAmount(); if (holdingType == HoldingType.NXT) { if (amount < Constants.SHUFFLING_DEPOSIT_NQT || amount > Constants.MAX_BALANCE_NQT) { throw new NxtException.NotValidException("Invalid NQT amount " + amount + ", minimum is " + Constants.SHUFFLING_DEPOSIT_NQT); } } else if (holdingType == HoldingType.ASSET) { Asset asset = Asset.getAsset(attachment.getHoldingId()); if (asset == null) { throw new NxtException.NotCurrentlyValidException("Unknown asset " + Long.toUnsignedString(attachment.getHoldingId())); } if (amount <= 0 || amount > asset.getInitialQuantityQNT()) { throw new NxtException.NotValidException("Invalid asset quantity " + amount); } } else if (holdingType == HoldingType.CURRENCY) { Currency currency = Currency.getCurrency(attachment.getHoldingId()); CurrencyType.validate(currency, transaction); if (!currency.isActive()) { throw new NxtException.NotCurrentlyValidException("Currency is not active: " + currency.getCode()); } if (amount <= 0 || amount > Constants.MAX_CURRENCY_TOTAL_SUPPLY) { throw new NxtException.NotValidException("Invalid currency amount " + amount); } } else { throw new RuntimeException("Unsupported holding type " + holdingType); } if (attachment.getParticipantCount() < Constants.MIN_NUMBER_OF_SHUFFLING_PARTICIPANTS || attachment.getParticipantCount() > Constants.MAX_NUMBER_OF_SHUFFLING_PARTICIPANTS) { throw new NxtException.NotValidException(String.format("Number of participants %d is not between %d and %d", attachment.getParticipantCount(), Constants.MIN_NUMBER_OF_SHUFFLING_PARTICIPANTS, Constants.MAX_NUMBER_OF_SHUFFLING_PARTICIPANTS)); } if (attachment.getRegistrationPeriod() < 1 || attachment.getRegistrationPeriod() > Constants.MAX_SHUFFLING_REGISTRATION_PERIOD) { throw new NxtException.NotValidException("Invalid registration period: " + attachment.getRegistrationPeriod()); } } @Override boolean applyAttachmentUnconfirmed(Transaction transaction, Account senderAccount) { Attachment.ShufflingCreation attachment = (Attachment.ShufflingCreation) transaction.getAttachment(); HoldingType holdingType = attachment.getHoldingType(); if (holdingType != HoldingType.NXT) { if (holdingType.getUnconfirmedBalance(senderAccount, attachment.getHoldingId()) >= attachment.getAmount() && senderAccount.getUnconfirmedBalanceNQT() >= Constants.SHUFFLING_DEPOSIT_NQT) { holdingType.addToUnconfirmedBalance(senderAccount, getLedgerEvent(), transaction.getId(), attachment.getHoldingId(), -attachment.getAmount()); senderAccount.addToUnconfirmedBalanceNQT(getLedgerEvent(), transaction.getId(), -Constants.SHUFFLING_DEPOSIT_NQT); return true; } } else { if (senderAccount.getUnconfirmedBalanceNQT() >= attachment.getAmount()) { senderAccount.addToUnconfirmedBalanceNQT(getLedgerEvent(), transaction.getId(), -attachment.getAmount()); return true; } } return false; } @Override void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) { Attachment.ShufflingCreation attachment = (Attachment.ShufflingCreation) transaction.getAttachment(); Shuffling.addShuffling(transaction, attachment); } @Override void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) { Attachment.ShufflingCreation attachment = (Attachment.ShufflingCreation) transaction.getAttachment(); HoldingType holdingType = attachment.getHoldingType(); if (holdingType != HoldingType.NXT) { holdingType.addToUnconfirmedBalance(senderAccount, getLedgerEvent(), transaction.getId(), attachment.getHoldingId(), attachment.getAmount()); senderAccount.addToUnconfirmedBalanceNQT(getLedgerEvent(), transaction.getId(), Constants.SHUFFLING_DEPOSIT_NQT); } else { senderAccount.addToUnconfirmedBalanceNQT(getLedgerEvent(), transaction.getId(), attachment.getAmount()); } } @Override boolean isDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) { Attachment.ShufflingCreation attachment = (Attachment.ShufflingCreation) transaction.getAttachment(); if (attachment.getHoldingType() != HoldingType.CURRENCY) { return false; } Currency currency = Currency.getCurrency(attachment.getHoldingId()); String nameLower = currency.getName().toLowerCase(); String codeLower = currency.getCode().toLowerCase(); boolean isDuplicate = TransactionType.isDuplicate(MonetarySystem.CURRENCY_ISSUANCE, nameLower, duplicates, false); if (! nameLower.equals(codeLower)) { isDuplicate = isDuplicate || TransactionType.isDuplicate(MonetarySystem.CURRENCY_ISSUANCE, codeLower, duplicates, false); } return isDuplicate; } }; public static final TransactionType SHUFFLING_REGISTRATION = new ShufflingTransaction() { @Override public byte getSubtype() { return SUBTYPE_SHUFFLING_REGISTRATION; } @Override public AccountLedger.LedgerEvent getLedgerEvent() { return AccountLedger.LedgerEvent.SHUFFLING_REGISTRATION; } @Override public String getName() { return "ShufflingRegistration"; } @Override Attachment.AbstractAttachment parseAttachment(ByteBuffer buffer, byte transactionVersion) { return new Attachment.ShufflingRegistration(buffer, transactionVersion); } @Override Attachment.AbstractAttachment parseAttachment(JSONObject attachmentData) { return new Attachment.ShufflingRegistration(attachmentData); } @Override void validateAttachment(Transaction transaction) throws NxtException.ValidationException { if (Nxt.getBlockchain().getHeight() < Constants.SHUFFLING_BLOCK) { throw new NxtException.NotYetEnabledException("Shuffling not yet enabled"); } Attachment.ShufflingRegistration attachment = (Attachment.ShufflingRegistration) transaction.getAttachment(); Shuffling shuffling = Shuffling.getShuffling(attachment.getShufflingId()); if (shuffling == null) { throw new NxtException.NotCurrentlyValidException("Shuffling not found: " + Long.toUnsignedString(attachment.getShufflingId())); } byte[] shufflingStateHash = shuffling.getStateHash(); if (shufflingStateHash == null || !Arrays.equals(shufflingStateHash, attachment.getShufflingStateHash())) { throw new NxtException.NotCurrentlyValidException("Shuffling state hash doesn't match"); } if (shuffling.getStage() != Shuffling.Stage.REGISTRATION) { throw new NxtException.NotCurrentlyValidException("Shuffling registration has ended for " + Long.toUnsignedString(attachment.getShufflingId())); } if (shuffling.getParticipant(transaction.getSenderId()) != null) { throw new NxtException.NotCurrentlyValidException(String.format("Account %s is already registered for shuffling %s", Long.toUnsignedString(transaction.getSenderId()), Long.toUnsignedString(shuffling.getId()))); } if (Nxt.getBlockchain().getHeight() + shuffling.getBlocksRemaining() <= attachment.getFinishValidationHeight(transaction)) { throw new NxtException.NotCurrentlyValidException("Shuffling registration finishes in " + shuffling.getBlocksRemaining() + " blocks"); } } @Override boolean isDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) { Attachment.ShufflingRegistration attachment = (Attachment.ShufflingRegistration) transaction.getAttachment(); Shuffling shuffling = Shuffling.getShuffling(attachment.getShufflingId()); return TransactionType.isDuplicate(SHUFFLING_REGISTRATION, Long.toUnsignedString(shuffling.getId()) + "." + Long.toUnsignedString(transaction.getSenderId()), duplicates, true) || TransactionType.isDuplicate(SHUFFLING_REGISTRATION, Long.toUnsignedString(shuffling.getId()), duplicates, shuffling.getParticipantCount() - shuffling.getRegistrantCount()); } @Override boolean applyAttachmentUnconfirmed(Transaction transaction, Account senderAccount) { Attachment.ShufflingRegistration attachment = (Attachment.ShufflingRegistration) transaction.getAttachment(); Shuffling shuffling = Shuffling.getShuffling(attachment.getShufflingId()); HoldingType holdingType = shuffling.getHoldingType(); if (holdingType != HoldingType.NXT) { if (holdingType.getUnconfirmedBalance(senderAccount, shuffling.getHoldingId()) >= shuffling.getAmount() && senderAccount.getUnconfirmedBalanceNQT() >= Constants.SHUFFLING_DEPOSIT_NQT) { holdingType.addToUnconfirmedBalance(senderAccount, getLedgerEvent(), transaction.getId(), shuffling.getHoldingId(), -shuffling.getAmount()); senderAccount.addToUnconfirmedBalanceNQT(getLedgerEvent(), transaction.getId(), -Constants.SHUFFLING_DEPOSIT_NQT); return true; } } else { if (senderAccount.getUnconfirmedBalanceNQT() >= shuffling.getAmount()) { senderAccount.addToUnconfirmedBalanceNQT(getLedgerEvent(), transaction.getId(), -shuffling.getAmount()); return true; } } return false; } @Override void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) { Attachment.ShufflingRegistration attachment = (Attachment.ShufflingRegistration) transaction.getAttachment(); Shuffling shuffling = Shuffling.getShuffling(attachment.getShufflingId()); shuffling.addParticipant(transaction.getSenderId()); } @Override void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) { Attachment.ShufflingRegistration attachment = (Attachment.ShufflingRegistration) transaction.getAttachment(); Shuffling shuffling = Shuffling.getShuffling(attachment.getShufflingId()); HoldingType holdingType = shuffling.getHoldingType(); if (holdingType != HoldingType.NXT) { holdingType.addToUnconfirmedBalance(senderAccount, getLedgerEvent(), transaction.getId(), shuffling.getHoldingId(), shuffling.getAmount()); senderAccount.addToUnconfirmedBalanceNQT(getLedgerEvent(), transaction.getId(), Constants.SHUFFLING_DEPOSIT_NQT); } else { senderAccount.addToUnconfirmedBalanceNQT(getLedgerEvent(), transaction.getId(), shuffling.getAmount()); } } }; public static final TransactionType SHUFFLING_PROCESSING = new ShufflingTransaction() { @Override public byte getSubtype() { return SUBTYPE_SHUFFLING_PROCESSING; } @Override public AccountLedger.LedgerEvent getLedgerEvent() { return AccountLedger.LedgerEvent.SHUFFLING_PROCESSING; } @Override public String getName() { return "ShufflingProcessing"; } @Override Fee getBaselineFee(Transaction transaction) { return SHUFFLING_PROCESSING_FEE; } @Override Attachment.AbstractAttachment parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException { return new Attachment.ShufflingProcessing(buffer, transactionVersion); } @Override Attachment.AbstractAttachment parseAttachment(JSONObject attachmentData) throws NxtException.NotValidException { return new Attachment.ShufflingProcessing(attachmentData); } @Override void validateAttachment(Transaction transaction) throws NxtException.ValidationException { if (Nxt.getBlockchain().getHeight() < Constants.SHUFFLING_BLOCK) { throw new NxtException.NotYetEnabledException("Shuffling not yet enabled"); } Attachment.ShufflingProcessing attachment = (Attachment.ShufflingProcessing)transaction.getAttachment(); Shuffling shuffling = Shuffling.getShuffling(attachment.getShufflingId()); if (shuffling == null) { throw new NxtException.NotCurrentlyValidException("Shuffling not found: " + Long.toUnsignedString(attachment.getShufflingId())); } if (shuffling.getStage() != Shuffling.Stage.PROCESSING) { throw new NxtException.NotCurrentlyValidException(String.format("Shuffling %s is not in processing stage", Long.toUnsignedString(attachment.getShufflingId()))); } ShufflingParticipant participant = shuffling.getParticipant(transaction.getSenderId()); if (participant == null) { throw new NxtException.NotCurrentlyValidException(String.format("Account %s is not registered for shuffling %s", Long.toUnsignedString(transaction.getSenderId()), Long.toUnsignedString(shuffling.getId()))); } if (!participant.getState().canBecome(ShufflingParticipant.State.PROCESSED)) { throw new NxtException.NotCurrentlyValidException(String.format("Participant %s processing already complete", Long.toUnsignedString(transaction.getSenderId()))); } if (participant.getAccountId() != shuffling.getAssigneeAccountId()) { throw new NxtException.NotCurrentlyValidException(String.format("Participant %s is not currently assigned to process shuffling %s", Long.toUnsignedString(participant.getAccountId()), Long.toUnsignedString(shuffling.getId()))); } if (participant.getNextAccountId() == 0) { throw new NxtException.NotValidException(String.format("Participant %s is last in shuffle", Long.toUnsignedString(transaction.getSenderId()))); } byte[] shufflingStateHash = shuffling.getStateHash(); if (shufflingStateHash == null || !Arrays.equals(shufflingStateHash, attachment.getShufflingStateHash())) { throw new NxtException.NotCurrentlyValidException("Shuffling state hash doesn't match"); } byte[][] data = attachment.getData(); if (data == null && Nxt.getEpochTime() - transaction.getTimestamp() < Constants.MIN_PRUNABLE_LIFETIME) { throw new NxtException.NotCurrentlyValidException("Data has been pruned prematurely"); } if (data != null) { if (data.length != participant.getIndex() + 1 && data.length != 0) { throw new NxtException.NotValidException(String.format("Invalid number of encrypted data %d for participant number %d", data.length, participant.getIndex())); } byte[] previous = null; for (byte[] bytes : data) { if (bytes.length != 32 + 64 * (shuffling.getParticipantCount() - participant.getIndex() - 1)) { throw new NxtException.NotValidException("Invalid encrypted data length " + bytes.length); } if (previous != null && Convert.byteArrayComparator.compare(previous, bytes) >= 0) { throw new NxtException.NotValidException("Duplicate or unsorted encrypted data"); } previous = bytes; } } } @Override boolean isDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) { Attachment.ShufflingProcessing attachment = (Attachment.ShufflingProcessing) transaction.getAttachment(); Shuffling shuffling = Shuffling.getShuffling(attachment.getShufflingId()); return TransactionType.isDuplicate(SHUFFLING_PROCESSING, Long.toUnsignedString(shuffling.getId()), duplicates, true); } @Override boolean applyAttachmentUnconfirmed(Transaction transaction, Account senderAccount) { return true; } @Override void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) { Attachment.ShufflingProcessing attachment = (Attachment.ShufflingProcessing)transaction.getAttachment(); Shuffling shuffling = Shuffling.getShuffling(attachment.getShufflingId()); shuffling.updateParticipantData(transaction, attachment); } @Override void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {} @Override public boolean isPhasable() { return false; } @Override boolean isPruned(long transactionId) { Transaction transaction = TransactionDb.findTransaction(transactionId); Attachment.ShufflingProcessing attachment = (Attachment.ShufflingProcessing)transaction.getAttachment(); return ShufflingParticipant.getData(attachment.getShufflingId(), transaction.getSenderId()) == null; } }; public static final TransactionType SHUFFLING_RECIPIENTS = new ShufflingTransaction() { @Override public byte getSubtype() { return SUBTYPE_SHUFFLING_RECIPIENTS; } @Override public AccountLedger.LedgerEvent getLedgerEvent() { return AccountLedger.LedgerEvent.SHUFFLING_PROCESSING; } @Override public String getName() { return "ShufflingRecipients"; } @Override Fee getBaselineFee(Transaction transaction) { return SHUFFLING_RECIPIENTS_FEE; } @Override Attachment.AbstractAttachment parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException { return new Attachment.ShufflingRecipients(buffer, transactionVersion); } @Override Attachment.AbstractAttachment parseAttachment(JSONObject attachmentData) { return new Attachment.ShufflingRecipients(attachmentData); } @Override void validateAttachment(Transaction transaction) throws NxtException.ValidationException { if (Nxt.getBlockchain().getHeight() < Constants.SHUFFLING_BLOCK) { throw new NxtException.NotYetEnabledException("Shuffling not yet enabled"); } Attachment.ShufflingRecipients attachment = (Attachment.ShufflingRecipients)transaction.getAttachment(); Shuffling shuffling = Shuffling.getShuffling(attachment.getShufflingId()); if (shuffling == null) { throw new NxtException.NotCurrentlyValidException("Shuffling not found: " + Long.toUnsignedString(attachment.getShufflingId())); } if (shuffling.getStage() != Shuffling.Stage.PROCESSING) { throw new NxtException.NotCurrentlyValidException(String.format("Shuffling %s is not in processing stage", Long.toUnsignedString(attachment.getShufflingId()))); } ShufflingParticipant participant = shuffling.getParticipant(transaction.getSenderId()); if (participant == null) { throw new NxtException.NotCurrentlyValidException(String.format("Account %s is not registered for shuffling %s", Long.toUnsignedString(transaction.getSenderId()), Long.toUnsignedString(shuffling.getId()))); } if (participant.getNextAccountId() != 0) { throw new NxtException.NotValidException(String.format("Participant %s is not last in shuffle", Long.toUnsignedString(transaction.getSenderId()))); } if (!participant.getState().canBecome(ShufflingParticipant.State.PROCESSED)) { throw new NxtException.NotCurrentlyValidException(String.format("Participant %s processing already complete", Long.toUnsignedString(transaction.getSenderId()))); } if (participant.getAccountId() != shuffling.getAssigneeAccountId()) { throw new NxtException.NotCurrentlyValidException(String.format("Participant %s is not currently assigned to process shuffling %s", Long.toUnsignedString(participant.getAccountId()), Long.toUnsignedString(shuffling.getId()))); } byte[] shufflingStateHash = shuffling.getStateHash(); if (shufflingStateHash == null || !Arrays.equals(shufflingStateHash, attachment.getShufflingStateHash())) { throw new NxtException.NotCurrentlyValidException("Shuffling state hash doesn't match"); } byte[][] recipientPublicKeys = attachment.getRecipientPublicKeys(); if (recipientPublicKeys.length != shuffling.getParticipantCount() && recipientPublicKeys.length != 0) { throw new NxtException.NotValidException(String.format("Invalid number of recipient public keys %d", recipientPublicKeys.length)); } Set<Long> recipientAccounts = new HashSet<>(recipientPublicKeys.length); for (byte[] recipientPublicKey : recipientPublicKeys) { if (!Crypto.isCanonicalPublicKey(recipientPublicKey)) { throw new NxtException.NotValidException("Invalid recipient public key " + Convert.toHexString(recipientPublicKey)); } if (!recipientAccounts.add(Account.getId(recipientPublicKey))) { throw new NxtException.NotValidException("Duplicate recipient accounts"); } } } @Override boolean isDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) { Attachment.ShufflingRecipients attachment = (Attachment.ShufflingRecipients) transaction.getAttachment(); Shuffling shuffling = Shuffling.getShuffling(attachment.getShufflingId()); return TransactionType.isDuplicate(SHUFFLING_PROCESSING, Long.toUnsignedString(shuffling.getId()), duplicates, true); } @Override boolean applyAttachmentUnconfirmed(Transaction transaction, Account senderAccount) { return true; } @Override void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) { Attachment.ShufflingRecipients attachment = (Attachment.ShufflingRecipients)transaction.getAttachment(); Shuffling shuffling = Shuffling.getShuffling(attachment.getShufflingId()); shuffling.updateRecipients(transaction, attachment); } @Override void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {} @Override public boolean isPhasable() { return false; } }; public static final TransactionType SHUFFLING_VERIFICATION = new ShufflingTransaction() { @Override public byte getSubtype() { return SUBTYPE_SHUFFLING_VERIFICATION; } @Override public AccountLedger.LedgerEvent getLedgerEvent() { return AccountLedger.LedgerEvent.SHUFFLING_PROCESSING; } @Override public String getName() { return "ShufflingVerification"; } @Override Attachment.AbstractAttachment parseAttachment(ByteBuffer buffer, byte transactionVersion) { return new Attachment.ShufflingVerification(buffer, transactionVersion); } @Override Attachment.AbstractAttachment parseAttachment(JSONObject attachmentData) { return new Attachment.ShufflingVerification(attachmentData); } @Override void validateAttachment(Transaction transaction) throws NxtException.ValidationException { if (Nxt.getBlockchain().getHeight() < Constants.SHUFFLING_BLOCK) { throw new NxtException.NotYetEnabledException("Shuffling not yet enabled"); } Attachment.ShufflingVerification attachment = (Attachment.ShufflingVerification) transaction.getAttachment(); Shuffling shuffling = Shuffling.getShuffling(attachment.getShufflingId()); if (shuffling == null) { throw new NxtException.NotCurrentlyValidException("Shuffling not found: " + Long.toUnsignedString(attachment.getShufflingId())); } if (shuffling.getStage() != Shuffling.Stage.VERIFICATION) { throw new NxtException.NotCurrentlyValidException("Shuffling not in verification stage: " + Long.toUnsignedString(attachment.getShufflingId())); } ShufflingParticipant participant = shuffling.getParticipant(transaction.getSenderId()); if (participant == null) { throw new NxtException.NotCurrentlyValidException(String.format("Account %s is not registered for shuffling %s", Long.toUnsignedString(transaction.getSenderId()), Long.toUnsignedString(shuffling.getId()))); } if (!participant.getState().canBecome(ShufflingParticipant.State.VERIFIED)) { throw new NxtException.NotCurrentlyValidException(String.format("Shuffling participant %s in state %s cannot become verified", Long.toUnsignedString(attachment.getShufflingId()), participant.getState())); } if (participant.getIndex() == shuffling.getParticipantCount() - 1) { throw new NxtException.NotValidException("Last participant cannot submit verification transaction"); } byte[] shufflingStateHash = shuffling.getStateHash(); if (shufflingStateHash == null || !Arrays.equals(shufflingStateHash, attachment.getShufflingStateHash())) { throw new NxtException.NotCurrentlyValidException("Shuffling state hash doesn't match"); } } @Override boolean isDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) { Attachment.ShufflingVerification attachment = (Attachment.ShufflingVerification) transaction.getAttachment(); Shuffling shuffling = Shuffling.getShuffling(attachment.getShufflingId()); return TransactionType.isDuplicate(SHUFFLING_VERIFICATION, Long.toUnsignedString(shuffling.getId()) + "." + Long.toUnsignedString(transaction.getSenderId()), duplicates, true); } @Override boolean applyAttachmentUnconfirmed(Transaction transaction, Account senderAccount) { return true; } @Override void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) { Attachment.ShufflingVerification attachment = (Attachment.ShufflingVerification) transaction.getAttachment(); Shuffling shuffling = Shuffling.getShuffling(attachment.getShufflingId()); shuffling.verify(transaction.getSenderId()); } @Override void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) { } @Override public boolean isPhasable() { return false; } }; public static final TransactionType SHUFFLING_CANCELLATION = new ShufflingTransaction() { @Override public byte getSubtype() { return SUBTYPE_SHUFFLING_CANCELLATION; } @Override public AccountLedger.LedgerEvent getLedgerEvent() { return AccountLedger.LedgerEvent.SHUFFLING_PROCESSING; } @Override public String getName() { return "ShufflingCancellation"; } @Override Fee getBaselineFee(Transaction transaction) { return SHUFFLING_PROCESSING_FEE; } @Override Attachment.AbstractAttachment parseAttachment(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException { return new Attachment.ShufflingCancellation(buffer, transactionVersion); } @Override Attachment.AbstractAttachment parseAttachment(JSONObject attachmentData) { return new Attachment.ShufflingCancellation(attachmentData); } @Override void validateAttachment(Transaction transaction) throws NxtException.ValidationException { if (Nxt.getBlockchain().getHeight() < Constants.SHUFFLING_BLOCK) { throw new NxtException.NotYetEnabledException("Shuffling not yet enabled"); } Attachment.ShufflingCancellation attachment = (Attachment.ShufflingCancellation) transaction.getAttachment(); Shuffling shuffling = Shuffling.getShuffling(attachment.getShufflingId()); if (shuffling == null) { throw new NxtException.NotCurrentlyValidException("Shuffling not found: " + Long.toUnsignedString(attachment.getShufflingId())); } long cancellingAccountId = attachment.getCancellingAccountId(); if (cancellingAccountId == 0 && !shuffling.getStage().canBecome(Shuffling.Stage.BLAME)) { throw new NxtException.NotCurrentlyValidException(String.format("Shuffling in state %s cannot be cancelled", shuffling.getStage())); } if (cancellingAccountId != 0 && cancellingAccountId != shuffling.getAssigneeAccountId()) { throw new NxtException.NotCurrentlyValidException(String.format("Shuffling %s is not currently being cancelled by account %s", Long.toUnsignedString(shuffling.getId()), Long.toUnsignedString(cancellingAccountId))); } ShufflingParticipant participant = shuffling.getParticipant(transaction.getSenderId()); if (participant == null) { throw new NxtException.NotCurrentlyValidException(String.format("Account %s is not registered for shuffling %s", Long.toUnsignedString(transaction.getSenderId()), Long.toUnsignedString(shuffling.getId()))); } if (!participant.getState().canBecome(ShufflingParticipant.State.CANCELLED)) { throw new NxtException.NotCurrentlyValidException(String.format("Shuffling participant %s in state %s cannot submit cancellation", Long.toUnsignedString(attachment.getShufflingId()), participant.getState())); } if (participant.getIndex() == shuffling.getParticipantCount() - 1) { throw new NxtException.NotValidException("Last participant cannot submit cancellation transaction"); } byte[] shufflingStateHash = shuffling.getStateHash(); if (shufflingStateHash == null || !Arrays.equals(shufflingStateHash, attachment.getShufflingStateHash())) { throw new NxtException.NotCurrentlyValidException("Shuffling state hash doesn't match"); } Transaction dataProcessingTransaction = TransactionDb.findTransactionByFullHash(participant.getDataTransactionFullHash(), Nxt.getBlockchain().getHeight()); if (dataProcessingTransaction == null) { throw new NxtException.NotCurrentlyValidException("Invalid data transaction full hash"); } Attachment.ShufflingProcessing shufflingProcessing = (Attachment.ShufflingProcessing) dataProcessingTransaction.getAttachment(); if (!Arrays.equals(shufflingProcessing.getHash(), attachment.getHash())) { throw new NxtException.NotValidException("Blame data hash doesn't match processing data hash"); } byte[][] keySeeds = attachment.getKeySeeds(); if (keySeeds.length != shuffling.getParticipantCount() - participant.getIndex() - 1) { throw new NxtException.NotValidException("Invalid number of revealed keySeeds: " + keySeeds.length); } for (byte[] keySeed : keySeeds) { if (keySeed.length != 32) { throw new NxtException.NotValidException("Invalid keySeed: " + Convert.toHexString(keySeed)); } } } @Override boolean isDuplicate(Transaction transaction, Map<TransactionType, Map<String, Integer>> duplicates) { Attachment.ShufflingCancellation attachment = (Attachment.ShufflingCancellation) transaction.getAttachment(); Shuffling shuffling = Shuffling.getShuffling(attachment.getShufflingId()); return TransactionType.isDuplicate(SHUFFLING_VERIFICATION, // use VERIFICATION for unique type Long.toUnsignedString(shuffling.getId()) + "." + Long.toUnsignedString(transaction.getSenderId()), duplicates, true); } @Override boolean applyAttachmentUnconfirmed(Transaction transaction, Account senderAccount) { return true; } @Override void applyAttachment(Transaction transaction, Account senderAccount, Account recipientAccount) { Attachment.ShufflingCancellation attachment = (Attachment.ShufflingCancellation) transaction.getAttachment(); Shuffling shuffling = Shuffling.getShuffling(attachment.getShufflingId()); ShufflingParticipant participant = ShufflingParticipant.getParticipant(shuffling.getId(), senderAccount.getId()); shuffling.cancelBy(participant, attachment.getBlameData(), attachment.getKeySeeds()); } @Override void undoAttachmentUnconfirmed(Transaction transaction, Account senderAccount) {} @Override public boolean isPhasable() { return false; } }; }