/****************************************************************************** * 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.AccountLedger.LedgerEvent; import nxt.crypto.Crypto; import nxt.crypto.EncryptedData; import nxt.util.Convert; import nxt.util.Logger; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; public interface Appendix { int getSize(); int getFullSize(); void putBytes(ByteBuffer buffer); JSONObject getJSONObject(); byte getVersion(); int getBaselineFeeHeight(); Fee getBaselineFee(Transaction transaction); int getNextFeeHeight(); Fee getNextFee(Transaction transaction); boolean isPhased(Transaction transaction); interface Prunable { byte[] getHash(); boolean hasPrunableData(); void restorePrunableData(Transaction transaction, int blockTimestamp, int height); default boolean shouldLoadPrunable(Transaction transaction, boolean includeExpiredPrunable) { return Nxt.getEpochTime() - transaction.getTimestamp() < (includeExpiredPrunable && Constants.INCLUDE_EXPIRED_PRUNABLE ? Constants.MAX_PRUNABLE_LIFETIME : Constants.MIN_PRUNABLE_LIFETIME); } } interface Encryptable { void encrypt(String secretPhrase); } abstract class AbstractAppendix implements Appendix { private final byte version; AbstractAppendix(JSONObject attachmentData) { Long l = (Long) attachmentData.get("version." + getAppendixName()); version = (byte) (l == null ? 0 : l); } AbstractAppendix(ByteBuffer buffer, byte transactionVersion) { if (transactionVersion == 0) { version = 0; } else { version = buffer.get(); } } AbstractAppendix(int version) { this.version = (byte) version; } AbstractAppendix() { this.version = 1; } abstract String getAppendixName(); @Override public final int getSize() { return getMySize() + (version > 0 ? 1 : 0); } @Override public final int getFullSize() { return getMyFullSize() + (version > 0 ? 1 : 0); } abstract int getMySize(); int getMyFullSize() { return getMySize(); } @Override public final void putBytes(ByteBuffer buffer) { if (version > 0) { buffer.put(version); } putMyBytes(buffer); } abstract void putMyBytes(ByteBuffer buffer); @Override public final JSONObject getJSONObject() { JSONObject json = new JSONObject(); json.put("version." + getAppendixName(), version); putMyJSON(json); return json; } abstract void putMyJSON(JSONObject json); @Override public final byte getVersion() { return version; } boolean verifyVersion(byte transactionVersion) { return transactionVersion == 0 ? version == 0 : version > 0; } @Override public int getBaselineFeeHeight() { return 1; } @Override public Fee getBaselineFee(Transaction transaction) { return Fee.NONE; } @Override public int getNextFeeHeight() { return Integer.MAX_VALUE; } @Override public Fee getNextFee(Transaction transaction) { return getBaselineFee(transaction); } abstract void validate(Transaction transaction) throws NxtException.ValidationException; void validateAtFinish(Transaction transaction) throws NxtException.ValidationException { if (!isPhased(transaction)) { return; } validate(transaction); } abstract void apply(Transaction transaction, Account senderAccount, Account recipientAccount); final void loadPrunable(Transaction transaction) { loadPrunable(transaction, false); } void loadPrunable(Transaction transaction, boolean includeExpiredPrunable) {} abstract boolean isPhasable(); @Override public final boolean isPhased(Transaction transaction) { return isPhasable() && transaction.getPhasing() != null; } } static boolean hasAppendix(String appendixName, JSONObject attachmentData) { return attachmentData.get("version." + appendixName) != null; } class Message extends AbstractAppendix { private static final String appendixName = "Message"; static Message parse(JSONObject attachmentData) { if (!hasAppendix(appendixName, attachmentData)) { return null; } return new Message(attachmentData); } private static final Fee MESSAGE_FEE = new Fee.SizeBasedFee(0, Constants.ONE_NXT, 32) { @Override public int getSize(TransactionImpl transaction, Appendix appendage) { return ((Message)appendage).getMessage().length; } }; private final byte[] message; private final boolean isText; Message(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException { super(buffer, transactionVersion); int messageLength = buffer.getInt(); this.isText = messageLength < 0; // ugly hack if (messageLength < 0) { messageLength &= Integer.MAX_VALUE; } if (messageLength > Constants.MAX_ARBITRARY_MESSAGE_LENGTH) { throw new NxtException.NotValidException("Invalid arbitrary message length: " + messageLength); } this.message = new byte[messageLength]; buffer.get(this.message); if (isText && !Arrays.equals(message, Convert.toBytes(Convert.toString(message)))) { throw new NxtException.NotValidException("Message is not UTF-8 text"); } } Message(JSONObject attachmentData) { super(attachmentData); String messageString = (String)attachmentData.get("message"); this.isText = Boolean.TRUE.equals(attachmentData.get("messageIsText")); this.message = isText ? Convert.toBytes(messageString) : Convert.parseHexString(messageString); } public Message(byte[] message) { this(message, false); } public Message(String string) { this(Convert.toBytes(string), true); } public Message(String string, boolean isText) { this(isText ? Convert.toBytes(string) : Convert.parseHexString(string), isText); } private Message(byte[] message, boolean isText) { this.message = message; this.isText = isText; } @Override String getAppendixName() { return appendixName; } @Override int getMySize() { return 4 + message.length; } @Override void putMyBytes(ByteBuffer buffer) { buffer.putInt(isText ? (message.length | Integer.MIN_VALUE) : message.length); buffer.put(message); } @Override void putMyJSON(JSONObject json) { json.put("message", Convert.toString(message, isText)); json.put("messageIsText", isText); } @Override public Fee getNextFee(Transaction transaction) { return MESSAGE_FEE; } @Override public int getNextFeeHeight() { return Constants.SHUFFLING_BLOCK; } @Override void validate(Transaction transaction) throws NxtException.ValidationException { if (message.length > (Nxt.getBlockchain().getHeight() > Constants.SHUFFLING_BLOCK ? Constants.MAX_ARBITRARY_MESSAGE_LENGTH_2 : Constants.MAX_ARBITRARY_MESSAGE_LENGTH)) { throw new NxtException.NotValidException("Invalid arbitrary message length: " + message.length); } } @Override void apply(Transaction transaction, Account senderAccount, Account recipientAccount) {} public byte[] getMessage() { return message; } public boolean isText() { return isText; } @Override boolean isPhasable() { return false; } } class PrunablePlainMessage extends Appendix.AbstractAppendix implements Prunable { private static final String appendixName = "PrunablePlainMessage"; private static final Fee PRUNABLE_MESSAGE_FEE = new Fee.SizeBasedFee(Constants.ONE_NXT/10) { @Override public int getSize(TransactionImpl transaction, Appendix appendix) { return appendix.getFullSize(); } }; static PrunablePlainMessage parse(JSONObject attachmentData) { if (!hasAppendix(appendixName, attachmentData)) { return null; } return new PrunablePlainMessage(attachmentData); } private final byte[] hash; private final byte[] message; private final boolean isText; private volatile PrunableMessage prunableMessage; PrunablePlainMessage(ByteBuffer buffer, byte transactionVersion) { super(buffer, transactionVersion); this.hash = new byte[32]; buffer.get(this.hash); this.message = null; this.isText = false; } private PrunablePlainMessage(JSONObject attachmentData) { super(attachmentData); String hashString = Convert.emptyToNull((String) attachmentData.get("messageHash")); String messageString = Convert.emptyToNull((String) attachmentData.get("message")); if (hashString != null && messageString == null) { this.hash = Convert.parseHexString(hashString); this.message = null; this.isText = false; } else { this.hash = null; this.isText = Boolean.TRUE.equals(attachmentData.get("messageIsText")); this.message = Convert.toBytes(messageString, isText); } } public PrunablePlainMessage(byte[] message) { this(message, false); } public PrunablePlainMessage(String string) { this(Convert.toBytes(string), true); } public PrunablePlainMessage(String string, boolean isText) { this(Convert.toBytes(string, isText), isText); } private PrunablePlainMessage(byte[] message, boolean isText) { this.message = message; this.isText = isText; this.hash = null; } @Override String getAppendixName() { return appendixName; } @Override public Fee getBaselineFee(Transaction transaction) { return PRUNABLE_MESSAGE_FEE; } @Override int getMySize() { return 32; } @Override int getMyFullSize() { return getMessage() == null ? 0 : getMessage().length; } @Override void putMyBytes(ByteBuffer buffer) { buffer.put(getHash()); } @Override void putMyJSON(JSONObject json) { if (prunableMessage != null) { json.put("message", Convert.toString(prunableMessage.getMessage(), prunableMessage.messageIsText())); json.put("messageIsText", prunableMessage.messageIsText()); } else if (message != null) { json.put("message", Convert.toString(message, isText)); json.put("messageIsText", isText); } json.put("messageHash", Convert.toHexString(getHash())); } @Override void validate(Transaction transaction) throws NxtException.ValidationException { if (transaction.getMessage() != null) { throw new NxtException.NotValidException("Cannot have both message and prunable message attachments"); } byte[] msg = getMessage(); if (msg != null && msg.length > Constants.MAX_PRUNABLE_MESSAGE_LENGTH) { throw new NxtException.NotValidException("Invalid prunable message length: " + msg.length); } if (msg == null && Nxt.getEpochTime() - transaction.getTimestamp() < Constants.MIN_PRUNABLE_LIFETIME) { throw new NxtException.NotCurrentlyValidException("Message has been pruned prematurely"); } } @Override void apply(Transaction transaction, Account senderAccount, Account recipientAccount) { if (Nxt.getEpochTime() - transaction.getTimestamp() < Constants.MAX_PRUNABLE_LIFETIME) { PrunableMessage.add(transaction, this); } } public byte[] getMessage() { if (prunableMessage != null) { return prunableMessage.getMessage(); } return message; } public boolean isText() { if (prunableMessage != null) { return prunableMessage.messageIsText(); } return isText; } @Override public byte[] getHash() { if (hash != null) { return hash; } MessageDigest digest = Crypto.sha256(); digest.update((byte)(isText ? 1 : 0)); digest.update(message); return digest.digest(); } @Override final void loadPrunable(Transaction transaction, boolean includeExpiredPrunable) { if (!hasPrunableData() && shouldLoadPrunable(transaction, includeExpiredPrunable)) { PrunableMessage prunableMessage = PrunableMessage.getPrunableMessage(transaction.getId()); if (prunableMessage != null && prunableMessage.getMessage() != null) { this.prunableMessage = prunableMessage; } } } @Override boolean isPhasable() { return false; } @Override public final boolean hasPrunableData() { return (prunableMessage != null || message != null); } @Override public void restorePrunableData(Transaction transaction, int blockTimestamp, int height) { PrunableMessage.add(transaction, this, blockTimestamp, height); } } abstract class AbstractEncryptedMessage extends AbstractAppendix { private static final Fee ENCRYPTED_MESSAGE_FEE = new Fee.SizeBasedFee(Constants.ONE_NXT, Constants.ONE_NXT, 32) { @Override public int getSize(TransactionImpl transaction, Appendix appendage) { return ((AbstractEncryptedMessage)appendage).getEncryptedDataLength() - 16; } }; private EncryptedData encryptedData; private final boolean isText; private final boolean isCompressed; private AbstractEncryptedMessage(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException { super(buffer, transactionVersion); int length = buffer.getInt(); this.isText = length < 0; if (length < 0) { length &= Integer.MAX_VALUE; } this.encryptedData = EncryptedData.readEncryptedData(buffer, length, Constants.MAX_ENCRYPTED_MESSAGE_LENGTH); this.isCompressed = getVersion() != 2; } private AbstractEncryptedMessage(JSONObject attachmentJSON, JSONObject encryptedMessageJSON) { super(attachmentJSON); byte[] data = Convert.parseHexString((String)encryptedMessageJSON.get("data")); byte[] nonce = Convert.parseHexString((String) encryptedMessageJSON.get("nonce")); this.encryptedData = new EncryptedData(data, nonce); this.isText = Boolean.TRUE.equals(encryptedMessageJSON.get("isText")); Object isCompressed = encryptedMessageJSON.get("isCompressed"); this.isCompressed = isCompressed == null || Boolean.TRUE.equals(isCompressed); } private AbstractEncryptedMessage(EncryptedData encryptedData, boolean isText, boolean isCompressed) { super(isCompressed ? 1 : 2); this.encryptedData = encryptedData; this.isText = isText; this.isCompressed = isCompressed; } @Override int getMySize() { return 4 + encryptedData.getSize(); } @Override void putMyBytes(ByteBuffer buffer) { buffer.putInt(isText ? (encryptedData.getData().length | Integer.MIN_VALUE) : encryptedData.getData().length); buffer.put(encryptedData.getData()); buffer.put(encryptedData.getNonce()); } @Override void putMyJSON(JSONObject json) { json.put("data", Convert.toHexString(encryptedData.getData())); json.put("nonce", Convert.toHexString(encryptedData.getNonce())); json.put("isText", isText); json.put("isCompressed", isCompressed); } @Override public Fee getNextFee(Transaction transaction) { return ENCRYPTED_MESSAGE_FEE; } @Override public int getNextFeeHeight() { return Constants.SHUFFLING_BLOCK; } @Override void validate(Transaction transaction) throws NxtException.ValidationException { if (getEncryptedDataLength() > (Nxt.getBlockchain().getHeight() > Constants.SHUFFLING_BLOCK ? Constants.MAX_ENCRYPTED_MESSAGE_LENGTH_2 : Constants.MAX_ENCRYPTED_MESSAGE_LENGTH)) { throw new NxtException.NotValidException("Max encrypted message length exceeded"); } if (encryptedData != null) { if ((encryptedData.getNonce().length != 32 && encryptedData.getData().length > 0) || (encryptedData.getNonce().length != 0 && encryptedData.getData().length == 0)) { throw new NxtException.NotValidException("Invalid nonce length " + encryptedData.getNonce().length); } } if ((getVersion() != 2 && !isCompressed) || (getVersion() == 2 && isCompressed)) { throw new NxtException.NotValidException("Version mismatch - version " + getVersion() + ", isCompressed " + isCompressed); } } @Override void apply(Transaction transaction, Account senderAccount, Account recipientAccount) {} public final EncryptedData getEncryptedData() { return encryptedData; } final void setEncryptedData(EncryptedData encryptedData) { this.encryptedData = encryptedData; } int getEncryptedDataLength() { return encryptedData.getData().length; } public final boolean isText() { return isText; } public final boolean isCompressed() { return isCompressed; } @Override final boolean isPhasable() { return false; } } class PrunableEncryptedMessage extends AbstractAppendix implements Prunable { private static final String appendixName = "PrunableEncryptedMessage"; private static final Fee PRUNABLE_ENCRYPTED_DATA_FEE = new Fee.SizeBasedFee(Constants.ONE_NXT/10) { @Override public int getSize(TransactionImpl transaction, Appendix appendix) { return appendix.getFullSize(); } }; static PrunableEncryptedMessage parse(JSONObject attachmentData) { if (!hasAppendix(appendixName, attachmentData)) { return null; } JSONObject encryptedMessageJSON = (JSONObject)attachmentData.get("encryptedMessage"); if (encryptedMessageJSON != null && encryptedMessageJSON.get("data") == null) { return new UnencryptedPrunableEncryptedMessage(attachmentData); } return new PrunableEncryptedMessage(attachmentData); } private final byte[] hash; private EncryptedData encryptedData; private final boolean isText; private final boolean isCompressed; private volatile PrunableMessage prunableMessage; PrunableEncryptedMessage(ByteBuffer buffer, byte transactionVersion) { super(buffer, transactionVersion); this.hash = new byte[32]; buffer.get(this.hash); this.encryptedData = null; this.isText = false; this.isCompressed = false; } private PrunableEncryptedMessage(JSONObject attachmentJSON) { super(attachmentJSON); String hashString = Convert.emptyToNull((String) attachmentJSON.get("encryptedMessageHash")); JSONObject encryptedMessageJSON = (JSONObject) attachmentJSON.get("encryptedMessage"); if (hashString != null && encryptedMessageJSON == null) { this.hash = Convert.parseHexString(hashString); this.encryptedData = null; this.isText = false; this.isCompressed = false; } else { this.hash = null; byte[] data = Convert.parseHexString((String) encryptedMessageJSON.get("data")); byte[] nonce = Convert.parseHexString((String) encryptedMessageJSON.get("nonce")); this.encryptedData = new EncryptedData(data, nonce); this.isText = Boolean.TRUE.equals(encryptedMessageJSON.get("isText")); this.isCompressed = Boolean.TRUE.equals(encryptedMessageJSON.get("isCompressed")); } } public PrunableEncryptedMessage(EncryptedData encryptedData, boolean isText, boolean isCompressed) { this.encryptedData = encryptedData; this.isText = isText; this.isCompressed = isCompressed; this.hash = null; } @Override public final Fee getBaselineFee(Transaction transaction) { return PRUNABLE_ENCRYPTED_DATA_FEE; } @Override final int getMySize() { return 32; } @Override final int getMyFullSize() { return getEncryptedDataLength(); } @Override void putMyBytes(ByteBuffer buffer) { buffer.put(getHash()); } @Override void putMyJSON(JSONObject json) { if (prunableMessage != null) { JSONObject encryptedMessageJSON = new JSONObject(); json.put("encryptedMessage", encryptedMessageJSON); encryptedMessageJSON.put("data", Convert.toHexString(prunableMessage.getEncryptedData().getData())); encryptedMessageJSON.put("nonce", Convert.toHexString(prunableMessage.getEncryptedData().getNonce())); encryptedMessageJSON.put("isText", prunableMessage.encryptedMessageIsText()); encryptedMessageJSON.put("isCompressed", prunableMessage.isCompressed()); } else if (encryptedData != null) { JSONObject encryptedMessageJSON = new JSONObject(); json.put("encryptedMessage", encryptedMessageJSON); encryptedMessageJSON.put("data", Convert.toHexString(encryptedData.getData())); encryptedMessageJSON.put("nonce", Convert.toHexString(encryptedData.getNonce())); encryptedMessageJSON.put("isText", isText); encryptedMessageJSON.put("isCompressed", isCompressed); } json.put("encryptedMessageHash", Convert.toHexString(getHash())); } @Override final String getAppendixName() { return appendixName; } @Override void validate(Transaction transaction) throws NxtException.ValidationException { if (transaction.getEncryptedMessage() != null) { throw new NxtException.NotValidException("Cannot have both encrypted and prunable encrypted message attachments"); } if (Nxt.getBlockchain().getHeight() < Constants.SHUFFLING_BLOCK && transaction.getPrunablePlainMessage() != null) { throw new NxtException.NotYetEnabledException("Cannot have both plain and encrypted prunable message attachments yet"); } EncryptedData ed = getEncryptedData(); if (ed == null && Nxt.getEpochTime() - transaction.getTimestamp() < Constants.MIN_PRUNABLE_LIFETIME) { throw new NxtException.NotCurrentlyValidException("Encrypted message has been pruned prematurely"); } if (ed != null) { if (ed.getData().length > Constants.MAX_PRUNABLE_ENCRYPTED_MESSAGE_LENGTH) { throw new NxtException.NotValidException(String.format("Message length %d exceeds max prunable encrypted message length %d", ed.getData().length, Constants.MAX_PRUNABLE_ENCRYPTED_MESSAGE_LENGTH)); } if ((ed.getNonce().length != 32 && ed.getData().length > 0) || (ed.getNonce().length != 0 && ed.getData().length == 0)) { throw new NxtException.NotValidException("Invalid nonce length " + ed.getNonce().length); } } if (transaction.getRecipientId() == 0) { throw new NxtException.NotValidException("Encrypted messages cannot be attached to transactions with no recipient"); } } @Override void apply(Transaction transaction, Account senderAccount, Account recipientAccount) { if (Nxt.getEpochTime() - transaction.getTimestamp() < Constants.MAX_PRUNABLE_LIFETIME) { PrunableMessage.add(transaction, this); } } public final EncryptedData getEncryptedData() { if (prunableMessage != null) { return prunableMessage.getEncryptedData(); } return encryptedData; } final void setEncryptedData(EncryptedData encryptedData) { this.encryptedData = encryptedData; } int getEncryptedDataLength() { return getEncryptedData() == null ? 0 : getEncryptedData().getData().length; } public final boolean isText() { if (prunableMessage != null) { return prunableMessage.encryptedMessageIsText(); } return isText; } public final boolean isCompressed() { if (prunableMessage != null) { return prunableMessage.isCompressed(); } return isCompressed; } @Override public final byte[] getHash() { if (hash != null) { return hash; } MessageDigest digest = Crypto.sha256(); digest.update((byte)(isText ? 1 : 0)); digest.update((byte)(isCompressed ? 1 : 0)); digest.update(encryptedData.getData()); digest.update(encryptedData.getNonce()); return digest.digest(); } @Override void loadPrunable(Transaction transaction, boolean includeExpiredPrunable) { if (!hasPrunableData() && shouldLoadPrunable(transaction, includeExpiredPrunable)) { PrunableMessage prunableMessage = PrunableMessage.getPrunableMessage(transaction.getId()); if (prunableMessage != null && prunableMessage.getEncryptedData() != null) { this.prunableMessage = prunableMessage; } } } @Override final boolean isPhasable() { return false; } @Override public final boolean hasPrunableData() { return (prunableMessage != null || encryptedData != null); } @Override public void restorePrunableData(Transaction transaction, int blockTimestamp, int height) { PrunableMessage.add(transaction, this, blockTimestamp, height); } } final class UnencryptedPrunableEncryptedMessage extends PrunableEncryptedMessage implements Encryptable { private final byte[] messageToEncrypt; private final byte[] recipientPublicKey; private UnencryptedPrunableEncryptedMessage(JSONObject attachmentJSON) { super(attachmentJSON); setEncryptedData(null); JSONObject encryptedMessageJSON = (JSONObject)attachmentJSON.get("encryptedMessage"); String messageToEncryptString = (String)encryptedMessageJSON.get("messageToEncrypt"); this.messageToEncrypt = isText() ? Convert.toBytes(messageToEncryptString) : Convert.parseHexString(messageToEncryptString); this.recipientPublicKey = Convert.parseHexString((String)attachmentJSON.get("recipientPublicKey")); } public UnencryptedPrunableEncryptedMessage(byte[] messageToEncrypt, boolean isText, boolean isCompressed, byte[] recipientPublicKey) { super(null, isText, isCompressed); this.messageToEncrypt = messageToEncrypt; this.recipientPublicKey = recipientPublicKey; } @Override void putMyBytes(ByteBuffer buffer) { if (getEncryptedData() == null) { throw new NxtException.NotYetEncryptedException("Prunable encrypted message not yet encrypted"); } super.putMyBytes(buffer); } @Override void putMyJSON(JSONObject json) { if (getEncryptedData() == null) { JSONObject encryptedMessageJSON = new JSONObject(); encryptedMessageJSON.put("messageToEncrypt", isText() ? Convert.toString(messageToEncrypt) : Convert.toHexString(messageToEncrypt)); encryptedMessageJSON.put("isText", isText()); encryptedMessageJSON.put("isCompressed", isCompressed()); json.put("recipientPublicKey", Convert.toHexString(recipientPublicKey)); json.put("encryptedMessage", encryptedMessageJSON); } else { super.putMyJSON(json); } } @Override void validate(Transaction transaction) throws NxtException.ValidationException { if (getEncryptedData() == null) { int dataLength = getEncryptedDataLength(); if (dataLength > Constants.MAX_PRUNABLE_ENCRYPTED_MESSAGE_LENGTH) { throw new NxtException.NotValidException(String.format("Message length %d exceeds max prunable encrypted message length %d", dataLength, Constants.MAX_PRUNABLE_ENCRYPTED_MESSAGE_LENGTH)); } } else { super.validate(transaction); } } @Override void apply(Transaction transaction, Account senderAccount, Account recipientAccount) { if (getEncryptedData() == null) { throw new NxtException.NotYetEncryptedException("Prunable encrypted message not yet encrypted"); } super.apply(transaction, senderAccount, recipientAccount); } @Override void loadPrunable(Transaction transaction, boolean includeExpiredPrunable) {} @Override public void encrypt(String secretPhrase) { setEncryptedData(EncryptedData.encrypt(getPlaintext(), secretPhrase, recipientPublicKey)); } @Override int getEncryptedDataLength() { return EncryptedData.getEncryptedDataLength(getPlaintext()); } private byte[] getPlaintext() { return isCompressed() && messageToEncrypt.length > 0 ? Convert.compress(messageToEncrypt) : messageToEncrypt; } } class EncryptedMessage extends AbstractEncryptedMessage { private static final String appendixName = "EncryptedMessage"; static EncryptedMessage parse(JSONObject attachmentData) { if (!hasAppendix(appendixName, attachmentData)) { return null; } if (((JSONObject)attachmentData.get("encryptedMessage")).get("data") == null) { return new UnencryptedEncryptedMessage(attachmentData); } return new EncryptedMessage(attachmentData); } EncryptedMessage(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException { super(buffer, transactionVersion); } EncryptedMessage(JSONObject attachmentData) { super(attachmentData, (JSONObject)attachmentData.get("encryptedMessage")); } public EncryptedMessage(EncryptedData encryptedData, boolean isText, boolean isCompressed) { super(encryptedData, isText, isCompressed); } @Override final String getAppendixName() { return appendixName; } @Override void putMyJSON(JSONObject json) { JSONObject encryptedMessageJSON = new JSONObject(); super.putMyJSON(encryptedMessageJSON); json.put("encryptedMessage", encryptedMessageJSON); } @Override void validate(Transaction transaction) throws NxtException.ValidationException { super.validate(transaction); if (transaction.getRecipientId() == 0) { throw new NxtException.NotValidException("Encrypted messages cannot be attached to transactions with no recipient"); } } } final class UnencryptedEncryptedMessage extends EncryptedMessage implements Encryptable { private final byte[] messageToEncrypt; private final byte[] recipientPublicKey; UnencryptedEncryptedMessage(JSONObject attachmentData) { super(attachmentData); setEncryptedData(null); JSONObject encryptedMessageJSON = (JSONObject)attachmentData.get("encryptedMessage"); String messageToEncryptString = (String)encryptedMessageJSON.get("messageToEncrypt"); messageToEncrypt = isText() ? Convert.toBytes(messageToEncryptString) : Convert.parseHexString(messageToEncryptString); recipientPublicKey = Convert.parseHexString((String)attachmentData.get("recipientPublicKey")); } public UnencryptedEncryptedMessage(byte[] messageToEncrypt, boolean isText, boolean isCompressed, byte[] recipientPublicKey) { super(null, isText, isCompressed); this.messageToEncrypt = messageToEncrypt; this.recipientPublicKey = recipientPublicKey; } @Override int getMySize() { if (getEncryptedData() != null) { return super.getMySize(); } return 4 + EncryptedData.getEncryptedSize(getPlaintext()); } @Override void putMyBytes(ByteBuffer buffer) { if (getEncryptedData() == null) { throw new NxtException.NotYetEncryptedException("Message not yet encrypted"); } super.putMyBytes(buffer); } @Override void putMyJSON(JSONObject json) { if (getEncryptedData() == null) { JSONObject encryptedMessageJSON = new JSONObject(); encryptedMessageJSON.put("messageToEncrypt", isText() ? Convert.toString(messageToEncrypt) : Convert.toHexString(messageToEncrypt)); encryptedMessageJSON.put("isText", isText()); encryptedMessageJSON.put("isCompressed", isCompressed()); json.put("encryptedMessage", encryptedMessageJSON); json.put("recipientPublicKey", Convert.toHexString(recipientPublicKey)); } else { super.putMyJSON(json); } } @Override void apply(Transaction transaction, Account senderAccount, Account recipientAccount) { if (getEncryptedData() == null) { throw new NxtException.NotYetEncryptedException("Message not yet encrypted"); } super.apply(transaction, senderAccount, recipientAccount); } @Override public void encrypt(String secretPhrase) { setEncryptedData(EncryptedData.encrypt(getPlaintext(), secretPhrase, recipientPublicKey)); } private byte[] getPlaintext() { return isCompressed() && messageToEncrypt.length > 0 ? Convert.compress(messageToEncrypt) : messageToEncrypt; } @Override int getEncryptedDataLength() { return EncryptedData.getEncryptedDataLength(getPlaintext()); } } class EncryptToSelfMessage extends AbstractEncryptedMessage { private static final String appendixName = "EncryptToSelfMessage"; static EncryptToSelfMessage parse(JSONObject attachmentData) { if (!hasAppendix(appendixName, attachmentData)) { return null; } if (((JSONObject)attachmentData.get("encryptToSelfMessage")).get("data") == null) { return new UnencryptedEncryptToSelfMessage(attachmentData); } return new EncryptToSelfMessage(attachmentData); } EncryptToSelfMessage(ByteBuffer buffer, byte transactionVersion) throws NxtException.NotValidException { super(buffer, transactionVersion); } EncryptToSelfMessage(JSONObject attachmentData) { super(attachmentData, (JSONObject)attachmentData.get("encryptToSelfMessage")); } public EncryptToSelfMessage(EncryptedData encryptedData, boolean isText, boolean isCompressed) { super(encryptedData, isText, isCompressed); } @Override final String getAppendixName() { return appendixName; } @Override void putMyJSON(JSONObject json) { JSONObject encryptToSelfMessageJSON = new JSONObject(); super.putMyJSON(encryptToSelfMessageJSON); json.put("encryptToSelfMessage", encryptToSelfMessageJSON); } } final class UnencryptedEncryptToSelfMessage extends EncryptToSelfMessage implements Encryptable { private final byte[] messageToEncrypt; UnencryptedEncryptToSelfMessage(JSONObject attachmentData) { super(attachmentData); setEncryptedData(null); JSONObject encryptedMessageJSON = (JSONObject)attachmentData.get("encryptToSelfMessage"); String messageToEncryptString = (String)encryptedMessageJSON.get("messageToEncrypt"); messageToEncrypt = isText() ? Convert.toBytes(messageToEncryptString) : Convert.parseHexString(messageToEncryptString); } public UnencryptedEncryptToSelfMessage(byte[] messageToEncrypt, boolean isText, boolean isCompressed) { super(null, isText, isCompressed); this.messageToEncrypt = messageToEncrypt; } @Override int getMySize() { if (getEncryptedData() != null) { return super.getMySize(); } return 4 + EncryptedData.getEncryptedSize(getPlaintext()); } @Override void putMyBytes(ByteBuffer buffer) { if (getEncryptedData() == null) { throw new NxtException.NotYetEncryptedException("Message not yet encrypted"); } super.putMyBytes(buffer); } @Override void putMyJSON(JSONObject json) { if (getEncryptedData() == null) { JSONObject encryptedMessageJSON = new JSONObject(); encryptedMessageJSON.put("messageToEncrypt", isText() ? Convert.toString(messageToEncrypt) : Convert.toHexString(messageToEncrypt)); encryptedMessageJSON.put("isText", isText()); encryptedMessageJSON.put("isCompressed", isCompressed()); json.put("encryptToSelfMessage", encryptedMessageJSON); } else { super.putMyJSON(json); } } @Override void apply(Transaction transaction, Account senderAccount, Account recipientAccount) { if (getEncryptedData() == null) { throw new NxtException.NotYetEncryptedException("Message not yet encrypted"); } super.apply(transaction, senderAccount, recipientAccount); } @Override public void encrypt(String secretPhrase) { setEncryptedData(EncryptedData.encrypt(getPlaintext(), secretPhrase, Crypto.getPublicKey(secretPhrase))); } @Override int getEncryptedDataLength() { return EncryptedData.getEncryptedDataLength(getPlaintext()); } private byte[] getPlaintext() { return isCompressed() && messageToEncrypt.length > 0 ? Convert.compress(messageToEncrypt) : messageToEncrypt; } } final class PublicKeyAnnouncement extends AbstractAppendix { private static final String appendixName = "PublicKeyAnnouncement"; static PublicKeyAnnouncement parse(JSONObject attachmentData) { if (!hasAppendix(appendixName, attachmentData)) { return null; } return new PublicKeyAnnouncement(attachmentData); } private final byte[] publicKey; PublicKeyAnnouncement(ByteBuffer buffer, byte transactionVersion) { super(buffer, transactionVersion); this.publicKey = new byte[32]; buffer.get(this.publicKey); } PublicKeyAnnouncement(JSONObject attachmentData) { super(attachmentData); this.publicKey = Convert.parseHexString((String)attachmentData.get("recipientPublicKey")); } public PublicKeyAnnouncement(byte[] publicKey) { this.publicKey = publicKey; } @Override String getAppendixName() { return appendixName; } @Override int getMySize() { return 32; } @Override void putMyBytes(ByteBuffer buffer) { buffer.put(publicKey); } @Override void putMyJSON(JSONObject json) { json.put("recipientPublicKey", Convert.toHexString(publicKey)); } @Override void validate(Transaction transaction) throws NxtException.ValidationException { if (transaction.getRecipientId() == 0) { throw new NxtException.NotValidException("PublicKeyAnnouncement cannot be attached to transactions with no recipient"); } if (publicKey.length != 32) { throw new NxtException.NotValidException("Invalid recipient public key length: " + Convert.toHexString(publicKey)); } if (Nxt.getBlockchain().getHeight() > Constants.SHUFFLING_BLOCK && !Crypto.isCanonicalPublicKey(publicKey)) { throw new NxtException.NotValidException("Invalid recipient public key: " + Convert.toHexString(publicKey)); } long recipientId = transaction.getRecipientId(); if (Account.getId(this.publicKey) != recipientId) { throw new NxtException.NotValidException("Announced public key does not match recipient accountId"); } byte[] recipientPublicKey = Account.getPublicKey(recipientId); if (recipientPublicKey != null && ! Arrays.equals(publicKey, recipientPublicKey)) { throw new NxtException.NotCurrentlyValidException("A different public key for this account has already been announced"); } } @Override void apply(Transaction transaction, Account senderAccount, Account recipientAccount) { if (Account.setOrVerify(recipientAccount.getId(), publicKey)) { recipientAccount.apply(this.publicKey); } } @Override boolean isPhasable() { return false; } public byte[] getPublicKey() { return publicKey; } } final class Phasing extends AbstractAppendix { private static final String appendixName = "Phasing"; private static final Fee PHASING_FEE = new Fee.ConstantFee(20 * Constants.ONE_NXT); private static final Fee PHASING_FEE_2 = new Fee() { @Override public long getFee(TransactionImpl transaction, Appendix appendage) { long fee = 0; Phasing phasing = (Phasing)appendage; if (!phasing.params.getVoteWeighting().isBalanceIndependent()) { fee += 20 * Constants.ONE_NXT; } else { fee += Constants.ONE_NXT; } if (phasing.hashedSecret.length > 0) { fee += (1 + (phasing.hashedSecret.length - 1) / 32) * Constants.ONE_NXT; } fee += Constants.ONE_NXT * phasing.linkedFullHashes.length; return fee; } }; static Phasing parse(JSONObject attachmentData) { if (!hasAppendix(appendixName, attachmentData)) { return null; } return new Phasing(attachmentData); } private final int finishHeight; private final PhasingParams params; private final byte[][] linkedFullHashes; private final byte[] hashedSecret; private final byte algorithm; Phasing(ByteBuffer buffer, byte transactionVersion) { super(buffer, transactionVersion); finishHeight = buffer.getInt(); params = new PhasingParams(buffer); byte linkedFullHashesSize = buffer.get(); if (linkedFullHashesSize > 0) { linkedFullHashes = new byte[linkedFullHashesSize][]; for (int i = 0; i < linkedFullHashesSize; i++) { linkedFullHashes[i] = new byte[32]; buffer.get(linkedFullHashes[i]); } } else { linkedFullHashes = Convert.EMPTY_BYTES; } byte hashedSecretLength = buffer.get(); if (hashedSecretLength > 0) { hashedSecret = new byte[hashedSecretLength]; buffer.get(hashedSecret); } else { hashedSecret = Convert.EMPTY_BYTE; } algorithm = buffer.get(); } Phasing(JSONObject attachmentData) { super(attachmentData); finishHeight = ((Long) attachmentData.get("phasingFinishHeight")).intValue(); params = new PhasingParams(attachmentData); JSONArray linkedFullHashesJson = (JSONArray) attachmentData.get("phasingLinkedFullHashes"); if (linkedFullHashesJson != null && linkedFullHashesJson.size() > 0) { linkedFullHashes = new byte[linkedFullHashesJson.size()][]; for (int i = 0; i < linkedFullHashes.length; i++) { linkedFullHashes[i] = Convert.parseHexString((String) linkedFullHashesJson.get(i)); } } else { linkedFullHashes = Convert.EMPTY_BYTES; } String hashedSecret = Convert.emptyToNull((String)attachmentData.get("phasingHashedSecret")); if (hashedSecret != null) { this.hashedSecret = Convert.parseHexString(hashedSecret); this.algorithm = ((Long) attachmentData.get("phasingHashedSecretAlgorithm")).byteValue(); } else { this.hashedSecret = Convert.EMPTY_BYTE; this.algorithm = 0; } } public Phasing(int finishHeight, PhasingParams phasingParams, byte[][] linkedFullHashes, byte[] hashedSecret, byte algorithm) { this.finishHeight = finishHeight; this.params = phasingParams; this.linkedFullHashes = Convert.nullToEmpty(linkedFullHashes); this.hashedSecret = hashedSecret != null ? hashedSecret : Convert.EMPTY_BYTE; this.algorithm = algorithm; } @Override String getAppendixName() { return appendixName; } @Override int getMySize() { return 4 + params.getMySize() + 1 + 32 * linkedFullHashes.length + 1 + hashedSecret.length + 1; } @Override void putMyBytes(ByteBuffer buffer) { buffer.putInt(finishHeight); params.putMyBytes(buffer); buffer.put((byte) linkedFullHashes.length); for (byte[] hash : linkedFullHashes) { buffer.put(hash); } buffer.put((byte)hashedSecret.length); buffer.put(hashedSecret); buffer.put(algorithm); } @Override void putMyJSON(JSONObject json) { json.put("phasingFinishHeight", finishHeight); params.putMyJSON(json); if (linkedFullHashes.length > 0) { JSONArray linkedFullHashesJson = new JSONArray(); for (byte[] hash : linkedFullHashes) { linkedFullHashesJson.add(Convert.toHexString(hash)); } json.put("phasingLinkedFullHashes", linkedFullHashesJson); } if (hashedSecret.length > 0) { json.put("phasingHashedSecret", Convert.toHexString(hashedSecret)); json.put("phasingHashedSecretAlgorithm", algorithm); } } @Override void validate(Transaction transaction) throws NxtException.ValidationException { params.validate(); int currentHeight = Nxt.getBlockchain().getHeight(); if (params.getVoteWeighting().getVotingModel() == VoteWeighting.VotingModel.TRANSACTION) { if (linkedFullHashes.length == 0 || linkedFullHashes.length > Constants.MAX_PHASING_LINKED_TRANSACTIONS) { throw new NxtException.NotValidException("Invalid number of linkedFullHashes " + linkedFullHashes.length); } Set<Long> linkedTransactionIds = new HashSet<>(linkedFullHashes.length); for (byte[] hash : linkedFullHashes) { if (Convert.emptyToNull(hash) == null || hash.length != 32) { throw new NxtException.NotValidException("Invalid linkedFullHash " + Convert.toHexString(hash)); } if (Nxt.getBlockchain().getHeight() > Constants.SHUFFLING_BLOCK) { if (!linkedTransactionIds.add(Convert.fullHashToId(hash))) { throw new NxtException.NotValidException("Duplicate linked transaction ids"); } } TransactionImpl linkedTransaction = TransactionDb.findTransactionByFullHash(hash, currentHeight); if (linkedTransaction != null) { if (transaction.getTimestamp() - linkedTransaction.getTimestamp() > Constants.MAX_REFERENCED_TRANSACTION_TIMESPAN) { throw new NxtException.NotValidException("Linked transaction cannot be more than 60 days older than the phased transaction"); } if (linkedTransaction.getPhasing() != null) { throw new NxtException.NotCurrentlyValidException("Cannot link to an already existing phased transaction"); } } } if (params.getQuorum() > linkedFullHashes.length) { throw new NxtException.NotValidException("Quorum of " + params.getQuorum() + " cannot be achieved in by-transaction voting with " + linkedFullHashes.length + " linked full hashes only"); } } else { if (linkedFullHashes.length != 0) { throw new NxtException.NotValidException("LinkedFullHashes can only be used with VotingModel.TRANSACTION"); } } if (params.getVoteWeighting().getVotingModel() == VoteWeighting.VotingModel.HASH) { if (params.getQuorum() != 1) { throw new NxtException.NotValidException("Quorum must be 1 for by-hash voting"); } if (hashedSecret.length == 0 || hashedSecret.length > Byte.MAX_VALUE) { throw new NxtException.NotValidException("Invalid hashedSecret " + Convert.toHexString(hashedSecret)); } if (PhasingPoll.getHashFunction(algorithm) == null) { throw new NxtException.NotValidException("Invalid hashedSecretAlgorithm " + algorithm); } } else { if (hashedSecret.length != 0) { throw new NxtException.NotValidException("HashedSecret can only be used with VotingModel.HASH"); } if (algorithm != 0) { throw new NxtException.NotValidException("HashedSecretAlgorithm can only be used with VotingModel.HASH"); } } if (finishHeight <= currentHeight + (params.getVoteWeighting().acceptsVotes() ? 2 : 1) || finishHeight >= currentHeight + Constants.MAX_PHASING_DURATION) { throw new NxtException.NotCurrentlyValidException("Invalid finish height " + finishHeight); } } @Override void validateAtFinish(Transaction transaction) throws NxtException.ValidationException { params.getVoteWeighting().validate(); } @Override void apply(Transaction transaction, Account senderAccount, Account recipientAccount) { PhasingPoll.addPoll(transaction, this); } @Override boolean isPhasable() { return false; } @Override public Fee getBaselineFee(Transaction transaction) { if (params.getVoteWeighting().isBalanceIndependent()) { return Fee.DEFAULT_FEE; } return PHASING_FEE; } @Override public Fee getNextFee(Transaction transaction) { return PHASING_FEE_2; } @Override public int getNextFeeHeight() { return Constants.SHUFFLING_BLOCK; } private void release(TransactionImpl transaction) { Account senderAccount = Account.getAccount(transaction.getSenderId()); Account recipientAccount = transaction.getRecipientId() == 0 ? null : Account.getAccount(transaction.getRecipientId()); transaction.getAppendages().forEach(appendage -> { if (appendage.isPhasable()) { appendage.apply(transaction, senderAccount, recipientAccount); } }); TransactionProcessorImpl.getInstance().notifyListeners(Collections.singletonList(transaction), TransactionProcessor.Event.RELEASE_PHASED_TRANSACTION); Logger.logDebugMessage("Transaction " + transaction.getStringId() + " has been released"); } void reject(TransactionImpl transaction) { Account senderAccount = Account.getAccount(transaction.getSenderId()); transaction.getType().undoAttachmentUnconfirmed(transaction, senderAccount); senderAccount.addToUnconfirmedBalanceNQT(LedgerEvent.REJECT_PHASED_TRANSACTION, transaction.getId(), transaction.getAmountNQT()); TransactionProcessorImpl.getInstance() .notifyListeners(Collections.singletonList(transaction), TransactionProcessor.Event.REJECT_PHASED_TRANSACTION); Logger.logDebugMessage("Transaction " + transaction.getStringId() + " has been rejected"); } void countVotes(TransactionImpl transaction) { if (Nxt.getBlockchain().getHeight() > Constants.SHUFFLING_BLOCK && PhasingPoll.getResult(transaction.getId()) != null) { return; } PhasingPoll poll = PhasingPoll.getPoll(transaction.getId()); long result = poll.countVotes(); poll.finish(result); if (result >= poll.getQuorum()) { try { release(transaction); } catch (RuntimeException e) { Logger.logErrorMessage("Failed to release phased transaction " + transaction.getJSONObject().toJSONString(), e); reject(transaction); } } else { reject(transaction); } } void tryCountVotes(TransactionImpl transaction, Map<TransactionType, Map<String, Integer>> duplicates) { PhasingPoll poll = PhasingPoll.getPoll(transaction.getId()); long result = poll.countVotes(); if (result >= poll.getQuorum()) { if (!transaction.attachmentIsDuplicate(duplicates, false)) { try { release(transaction); poll.finish(result); Logger.logDebugMessage("Early finish of transaction " + transaction.getStringId() + " at height " + Nxt.getBlockchain().getHeight()); } catch (RuntimeException e) { Logger.logErrorMessage("Failed to release phased transaction " + transaction.getJSONObject().toJSONString(), e); } } else { Logger.logDebugMessage("At height " + Nxt.getBlockchain().getHeight() + " phased transaction " + transaction.getStringId() + " is duplicate, cannot finish early"); } } else { Logger.logDebugMessage("At height " + Nxt.getBlockchain().getHeight() + " phased transaction " + transaction.getStringId() + " does not yet meet quorum, cannot finish early"); } } public int getFinishHeight() { return finishHeight; } public long getQuorum() { return params.getQuorum(); } public long[] getWhitelist() { return params.getWhitelist(); } public VoteWeighting getVoteWeighting() { return params.getVoteWeighting(); } public byte[][] getLinkedFullHashes() { return linkedFullHashes; } public byte[] getHashedSecret() { return hashedSecret; } public byte getAlgorithm() { return algorithm; } public PhasingParams getParams() { return params; } } }