/****************************************************************************** * 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.DbKey; import nxt.util.Convert; import nxt.util.Filter; import nxt.util.Logger; import org.json.simple.JSONObject; import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; final class TransactionImpl implements Transaction { static final class BuilderImpl implements Builder { private final short deadline; private final byte[] senderPublicKey; private final long amountNQT; private final long feeNQT; private final TransactionType type; private final byte version; private Attachment.AbstractAttachment attachment; private long recipientId; private byte[] referencedTransactionFullHash; private byte[] signature; private Appendix.Message message; private Appendix.EncryptedMessage encryptedMessage; private Appendix.EncryptToSelfMessage encryptToSelfMessage; private Appendix.PublicKeyAnnouncement publicKeyAnnouncement; private Appendix.Phasing phasing; private Appendix.PrunablePlainMessage prunablePlainMessage; private Appendix.PrunableEncryptedMessage prunableEncryptedMessage; private long blockId; private int height = Integer.MAX_VALUE; private long id; private long senderId; private int timestamp = Integer.MAX_VALUE; private int blockTimestamp = -1; private byte[] fullHash; private boolean ecBlockSet = false; private int ecBlockHeight; private long ecBlockId; private short index = -1; BuilderImpl(byte version, byte[] senderPublicKey, long amountNQT, long feeNQT, short deadline, Attachment.AbstractAttachment attachment) { this.version = version; this.deadline = deadline; this.senderPublicKey = senderPublicKey; this.amountNQT = amountNQT; this.feeNQT = feeNQT; this.attachment = attachment; this.type = attachment.getTransactionType(); } @Override public TransactionImpl build(String secretPhrase) throws NxtException.NotValidException { if (timestamp == Integer.MAX_VALUE) { timestamp = Nxt.getEpochTime(); } if (!ecBlockSet) { Block ecBlock = EconomicClustering.getECBlock(timestamp); this.ecBlockHeight = ecBlock.getHeight(); this.ecBlockId = ecBlock.getId(); } return new TransactionImpl(this, secretPhrase); } @Override public TransactionImpl build() throws NxtException.NotValidException { return build(null); } public BuilderImpl recipientId(long recipientId) { this.recipientId = recipientId; return this; } @Override public BuilderImpl referencedTransactionFullHash(String referencedTransactionFullHash) { this.referencedTransactionFullHash = Convert.parseHexString(referencedTransactionFullHash); return this; } BuilderImpl referencedTransactionFullHash(byte[] referencedTransactionFullHash) { this.referencedTransactionFullHash = referencedTransactionFullHash; return this; } BuilderImpl appendix(Attachment.AbstractAttachment attachment) { this.attachment = attachment; return this; } @Override public BuilderImpl appendix(Appendix.Message message) { this.message = message; return this; } @Override public BuilderImpl appendix(Appendix.EncryptedMessage encryptedMessage) { this.encryptedMessage = encryptedMessage; return this; } @Override public BuilderImpl appendix(Appendix.EncryptToSelfMessage encryptToSelfMessage) { this.encryptToSelfMessage = encryptToSelfMessage; return this; } @Override public BuilderImpl appendix(Appendix.PublicKeyAnnouncement publicKeyAnnouncement) { this.publicKeyAnnouncement = publicKeyAnnouncement; return this; } @Override public BuilderImpl appendix(Appendix.PrunablePlainMessage prunablePlainMessage) { this.prunablePlainMessage = prunablePlainMessage; return this; } @Override public BuilderImpl appendix(Appendix.PrunableEncryptedMessage prunableEncryptedMessage) { this.prunableEncryptedMessage = prunableEncryptedMessage; return this; } @Override public BuilderImpl appendix(Appendix.Phasing phasing) { this.phasing = phasing; return this; } @Override public BuilderImpl timestamp(int timestamp) { this.timestamp = timestamp; return this; } @Override public BuilderImpl ecBlockHeight(int height) { this.ecBlockHeight = height; this.ecBlockSet = true; return this; } @Override public BuilderImpl ecBlockId(long blockId) { this.ecBlockId = blockId; this.ecBlockSet = true; return this; } BuilderImpl id(long id) { this.id = id; return this; } BuilderImpl signature(byte[] signature) { this.signature = signature; return this; } BuilderImpl blockId(long blockId) { this.blockId = blockId; return this; } BuilderImpl height(int height) { this.height = height; return this; } BuilderImpl senderId(long senderId) { this.senderId = senderId; return this; } BuilderImpl fullHash(byte[] fullHash) { this.fullHash = fullHash; return this; } BuilderImpl blockTimestamp(int blockTimestamp) { this.blockTimestamp = blockTimestamp; return this; } BuilderImpl index(short index) { this.index = index; return this; } } private final short deadline; private volatile byte[] senderPublicKey; private final long recipientId; private final long amountNQT; private final long feeNQT; private final byte[] referencedTransactionFullHash; private final TransactionType type; private final int ecBlockHeight; private final long ecBlockId; private final byte version; private final int timestamp; private final byte[] signature; private final Attachment.AbstractAttachment attachment; private final Appendix.Message message; private final Appendix.EncryptedMessage encryptedMessage; private final Appendix.EncryptToSelfMessage encryptToSelfMessage; private final Appendix.PublicKeyAnnouncement publicKeyAnnouncement; private final Appendix.Phasing phasing; private final Appendix.PrunablePlainMessage prunablePlainMessage; private final Appendix.PrunableEncryptedMessage prunableEncryptedMessage; private final List<Appendix.AbstractAppendix> appendages; private final int appendagesSize; private volatile int height = Integer.MAX_VALUE; private volatile long blockId; private volatile BlockImpl block; private volatile int blockTimestamp = -1; private volatile short index = -1; private volatile long id; private volatile String stringId; private volatile long senderId; private volatile byte[] fullHash; private volatile DbKey dbKey; private volatile byte[] bytes = null; private TransactionImpl(BuilderImpl builder, String secretPhrase) throws NxtException.NotValidException { this.timestamp = builder.timestamp; this.deadline = builder.deadline; this.senderPublicKey = builder.senderPublicKey; this.recipientId = builder.recipientId; this.amountNQT = builder.amountNQT; this.referencedTransactionFullHash = builder.referencedTransactionFullHash; this.type = builder.type; this.version = builder.version; this.blockId = builder.blockId; this.height = builder.height; this.index = builder.index; this.id = builder.id; this.senderId = builder.senderId; this.blockTimestamp = builder.blockTimestamp; this.fullHash = builder.fullHash; this.ecBlockHeight = builder.ecBlockHeight; this.ecBlockId = builder.ecBlockId; List<Appendix.AbstractAppendix> list = new ArrayList<>(); if ((this.attachment = builder.attachment) != null) { list.add(this.attachment); } if ((this.message = builder.message) != null) { list.add(this.message); } if ((this.encryptedMessage = builder.encryptedMessage) != null) { list.add(this.encryptedMessage); } if ((this.publicKeyAnnouncement = builder.publicKeyAnnouncement) != null) { list.add(this.publicKeyAnnouncement); } if ((this.encryptToSelfMessage = builder.encryptToSelfMessage) != null) { list.add(this.encryptToSelfMessage); } if ((this.phasing = builder.phasing) != null) { list.add(this.phasing); } if ((this.prunablePlainMessage = builder.prunablePlainMessage) != null) { list.add(this.prunablePlainMessage); } if ((this.prunableEncryptedMessage = builder.prunableEncryptedMessage) != null) { list.add(this.prunableEncryptedMessage); } this.appendages = Collections.unmodifiableList(list); int appendagesSize = 0; for (Appendix appendage : appendages) { if (secretPhrase != null && appendage instanceof Appendix.Encryptable) { ((Appendix.Encryptable)appendage).encrypt(secretPhrase); } appendagesSize += appendage.getSize(); } this.appendagesSize = appendagesSize; if (builder.feeNQT <= 0 || (Constants.correctInvalidFees && builder.signature == null)) { int effectiveHeight = (height < Integer.MAX_VALUE ? height : Nxt.getBlockchain().getHeight()); long minFee = getMinimumFeeNQT(effectiveHeight); feeNQT = Math.max(minFee, builder.feeNQT); } else { feeNQT = builder.feeNQT; } if (builder.signature != null && secretPhrase != null) { throw new NxtException.NotValidException("Transaction is already signed"); } else if (builder.signature != null) { this.signature = builder.signature; } else if (secretPhrase != null) { if (getSenderPublicKey() != null && ! Arrays.equals(senderPublicKey, Crypto.getPublicKey(secretPhrase))) { throw new NxtException.NotValidException("Secret phrase doesn't match transaction sender public key"); } signature = Crypto.sign(bytes(), secretPhrase); bytes = null; } else { signature = null; } } @Override public short getDeadline() { return deadline; } @Override public byte[] getSenderPublicKey() { if (senderPublicKey == null) { senderPublicKey = Account.getPublicKey(senderId); } return senderPublicKey; } @Override public long getRecipientId() { return recipientId; } @Override public long getAmountNQT() { return amountNQT; } @Override public long getFeeNQT() { return feeNQT; } long[] getBackFees() { return type.getBackFees(this); } @Override public String getReferencedTransactionFullHash() { return Convert.toHexString(referencedTransactionFullHash); } byte[] referencedTransactionFullHash() { return referencedTransactionFullHash; } @Override public int getHeight() { return height; } void setHeight(int height) { this.height = height; } @Override public byte[] getSignature() { return signature; } @Override public TransactionType getType() { return type; } @Override public byte getVersion() { return version; } @Override public long getBlockId() { return blockId; } @Override public BlockImpl getBlock() { if (block == null && blockId != 0) { block = BlockchainImpl.getInstance().getBlock(blockId); } return block; } void setBlock(BlockImpl block) { this.block = block; this.blockId = block.getId(); this.height = block.getHeight(); this.blockTimestamp = block.getTimestamp(); } void unsetBlock() { this.block = null; this.blockId = 0; this.blockTimestamp = -1; this.index = -1; // must keep the height set, as transactions already having been included in a popped-off block before // get priority when sorted for inclusion in a new block } @Override public short getIndex() { if (index == -1) { throw new IllegalStateException("Transaction index has not been set"); } return index; } void setIndex(int index) { this.index = (short)index; } @Override public int getTimestamp() { return timestamp; } @Override public int getBlockTimestamp() { return blockTimestamp; } @Override public int getExpiration() { return timestamp + deadline * 60; } @Override public Attachment.AbstractAttachment getAttachment() { attachment.loadPrunable(this); return attachment; } @Override public List<Appendix.AbstractAppendix> getAppendages() { return getAppendages(false); } @Override public List<Appendix.AbstractAppendix> getAppendages(boolean includeExpiredPrunable) { for (Appendix.AbstractAppendix appendage : appendages) { appendage.loadPrunable(this, includeExpiredPrunable); } return appendages; } @Override public List<Appendix> getAppendages(Filter<Appendix> filter, boolean includeExpiredPrunable) { List<Appendix> result = new ArrayList<>(); appendages.forEach(appendix -> { if (filter.ok(appendix)) { appendix.loadPrunable(this, includeExpiredPrunable); result.add(appendix); } }); return result; } @Override public long getId() { if (id == 0) { if (signature == null) { throw new IllegalStateException("Transaction is not signed yet"); } if (useNQT()) { byte[] data = zeroSignature(getBytes()); byte[] signatureHash = Crypto.sha256().digest(signature); MessageDigest digest = Crypto.sha256(); digest.update(data); fullHash = digest.digest(signatureHash); } else { fullHash = Crypto.sha256().digest(bytes()); } BigInteger bigInteger = new BigInteger(1, new byte[] {fullHash[7], fullHash[6], fullHash[5], fullHash[4], fullHash[3], fullHash[2], fullHash[1], fullHash[0]}); id = bigInteger.longValue(); stringId = bigInteger.toString(); } return id; } @Override public String getStringId() { if (stringId == null) { getId(); if (stringId == null) { stringId = Long.toUnsignedString(id); } } return stringId; } @Override public String getFullHash() { return Convert.toHexString(fullHash()); } byte[] fullHash() { if (fullHash == null) { getId(); } return fullHash; } @Override public long getSenderId() { if (senderId == 0) { senderId = Account.getId(getSenderPublicKey()); } return senderId; } DbKey getDbKey() { if (dbKey == null) { dbKey = TransactionProcessorImpl.getInstance().unconfirmedTransactionDbKeyFactory.newKey(getId()); } return dbKey; } @Override public Appendix.Message getMessage() { return message; } @Override public Appendix.EncryptedMessage getEncryptedMessage() { return encryptedMessage; } @Override public Appendix.EncryptToSelfMessage getEncryptToSelfMessage() { return encryptToSelfMessage; } @Override public Appendix.Phasing getPhasing() { return phasing; } boolean attachmentIsPhased() { return attachment.isPhased(this); } Appendix.PublicKeyAnnouncement getPublicKeyAnnouncement() { return publicKeyAnnouncement; } @Override public Appendix.PrunablePlainMessage getPrunablePlainMessage() { if (prunablePlainMessage != null) { prunablePlainMessage.loadPrunable(this); } return prunablePlainMessage; } boolean hasPrunablePlainMessage() { return prunablePlainMessage != null; } @Override public Appendix.PrunableEncryptedMessage getPrunableEncryptedMessage() { if (prunableEncryptedMessage != null) { prunableEncryptedMessage.loadPrunable(this); } return prunableEncryptedMessage; } boolean hasPrunableEncryptedMessage() { return prunableEncryptedMessage != null; } public byte[] getBytes() { return Arrays.copyOf(bytes(), bytes.length); } byte[] bytes() { if (bytes == null) { try { ByteBuffer buffer = ByteBuffer.allocate(getSize()); buffer.order(ByteOrder.LITTLE_ENDIAN); buffer.put(type.getType()); buffer.put((byte) ((version << 4) | type.getSubtype())); buffer.putInt(timestamp); buffer.putShort(deadline); buffer.put(getSenderPublicKey()); buffer.putLong(type.canHaveRecipient() ? recipientId : Genesis.CREATOR_ID); if (useNQT()) { buffer.putLong(amountNQT); buffer.putLong(feeNQT); if (referencedTransactionFullHash != null) { buffer.put(referencedTransactionFullHash); } else { buffer.put(new byte[32]); } } else { buffer.putInt((int) (amountNQT / Constants.ONE_NXT)); buffer.putInt((int) (feeNQT / Constants.ONE_NXT)); if (referencedTransactionFullHash != null) { buffer.putLong(Convert.fullHashToId(referencedTransactionFullHash)); } else { buffer.putLong(0L); } } buffer.put(signature != null ? signature : new byte[64]); if (version > 0) { buffer.putInt(getFlags()); buffer.putInt(ecBlockHeight); buffer.putLong(ecBlockId); } for (Appendix appendage : appendages) { appendage.putBytes(buffer); } bytes = buffer.array(); } catch (RuntimeException e) { if (signature != null) { Logger.logDebugMessage("Failed to get transaction bytes for transaction: " + getJSONObject().toJSONString()); } throw e; } } return bytes; } static TransactionImpl.BuilderImpl newTransactionBuilder(byte[] bytes) throws NxtException.NotValidException { try { ByteBuffer buffer = ByteBuffer.wrap(bytes); buffer.order(ByteOrder.LITTLE_ENDIAN); byte type = buffer.get(); byte subtype = buffer.get(); byte version = (byte) ((subtype & 0xF0) >> 4); subtype = (byte) (subtype & 0x0F); int timestamp = buffer.getInt(); short deadline = buffer.getShort(); byte[] senderPublicKey = new byte[32]; buffer.get(senderPublicKey); long recipientId = buffer.getLong(); long amountNQT = buffer.getLong(); long feeNQT = buffer.getLong(); byte[] referencedTransactionFullHash = new byte[32]; buffer.get(referencedTransactionFullHash); referencedTransactionFullHash = Convert.emptyToNull(referencedTransactionFullHash); byte[] signature = new byte[64]; buffer.get(signature); signature = Convert.emptyToNull(signature); int flags = 0; int ecBlockHeight = 0; long ecBlockId = 0; if (version > 0) { flags = buffer.getInt(); ecBlockHeight = buffer.getInt(); ecBlockId = buffer.getLong(); } TransactionType transactionType = TransactionType.findTransactionType(type, subtype); TransactionImpl.BuilderImpl builder = new BuilderImpl(version, senderPublicKey, amountNQT, feeNQT, deadline, transactionType.parseAttachment(buffer, version)) .timestamp(timestamp) .referencedTransactionFullHash(referencedTransactionFullHash) .signature(signature) .ecBlockHeight(ecBlockHeight) .ecBlockId(ecBlockId); if (transactionType.canHaveRecipient()) { builder.recipientId(recipientId); } int position = 1; if ((flags & position) != 0 || (version == 0 && transactionType == TransactionType.Messaging.ARBITRARY_MESSAGE)) { builder.appendix(new Appendix.Message(buffer, version)); } position <<= 1; if ((flags & position) != 0) { builder.appendix(new Appendix.EncryptedMessage(buffer, version)); } position <<= 1; if ((flags & position) != 0) { builder.appendix(new Appendix.PublicKeyAnnouncement(buffer, version)); } position <<= 1; if ((flags & position) != 0) { builder.appendix(new Appendix.EncryptToSelfMessage(buffer, version)); } position <<= 1; if ((flags & position) != 0) { builder.appendix(new Appendix.Phasing(buffer, version)); } position <<= 1; if ((flags & position) != 0) { builder.appendix(new Appendix.PrunablePlainMessage(buffer, version)); } position <<= 1; if ((flags & position) != 0) { builder.appendix(new Appendix.PrunableEncryptedMessage(buffer, version)); } if (buffer.hasRemaining()) { throw new NxtException.NotValidException("Transaction bytes too long, " + buffer.remaining() + " extra bytes"); } return builder; } catch (NxtException.NotValidException|RuntimeException e) { Logger.logDebugMessage("Failed to parse transaction bytes: " + Convert.toHexString(bytes)); throw e; } } static TransactionImpl.BuilderImpl newTransactionBuilder(byte[] bytes, JSONObject prunableAttachments) throws NxtException.NotValidException { BuilderImpl builder = newTransactionBuilder(bytes); if (prunableAttachments != null) { Attachment.ShufflingProcessing shufflingProcessing = Attachment.ShufflingProcessing.parse(prunableAttachments); if (shufflingProcessing != null) { builder.appendix(shufflingProcessing); } Attachment.TaggedDataUpload taggedDataUpload = Attachment.TaggedDataUpload.parse(prunableAttachments); if (taggedDataUpload != null) { builder.appendix(taggedDataUpload); } Attachment.TaggedDataExtend taggedDataExtend = Attachment.TaggedDataExtend.parse(prunableAttachments); if (taggedDataExtend != null) { builder.appendix(taggedDataExtend); } Appendix.PrunablePlainMessage prunablePlainMessage = Appendix.PrunablePlainMessage.parse(prunableAttachments); if (prunablePlainMessage != null) { builder.appendix(prunablePlainMessage); } Appendix.PrunableEncryptedMessage prunableEncryptedMessage = Appendix.PrunableEncryptedMessage.parse(prunableAttachments); if (prunableEncryptedMessage != null) { builder.appendix(prunableEncryptedMessage); } } return builder; } public byte[] getUnsignedBytes() { return zeroSignature(getBytes()); } @Override public JSONObject getJSONObject() { JSONObject json = new JSONObject(); json.put("type", type.getType()); json.put("subtype", type.getSubtype()); json.put("timestamp", timestamp); json.put("deadline", deadline); json.put("senderPublicKey", Convert.toHexString(getSenderPublicKey())); if (type.canHaveRecipient()) { json.put("recipient", Long.toUnsignedString(recipientId)); } json.put("amountNQT", amountNQT); json.put("feeNQT", feeNQT); if (referencedTransactionFullHash != null) { json.put("referencedTransactionFullHash", Convert.toHexString(referencedTransactionFullHash)); } json.put("ecBlockHeight", ecBlockHeight); json.put("ecBlockId", Long.toUnsignedString(ecBlockId)); json.put("signature", Convert.toHexString(signature)); JSONObject attachmentJSON = new JSONObject(); for (Appendix.AbstractAppendix appendage : appendages) { appendage.loadPrunable(this); attachmentJSON.putAll(appendage.getJSONObject()); } if (! attachmentJSON.isEmpty()) { json.put("attachment", attachmentJSON); } json.put("version", version); return json; } @Override public JSONObject getPrunableAttachmentJSON() { JSONObject prunableJSON = null; for (Appendix.AbstractAppendix appendage : appendages) { if (appendage instanceof Appendix.Prunable) { appendage.loadPrunable(this); if (prunableJSON == null) { prunableJSON = appendage.getJSONObject(); } else { prunableJSON.putAll(appendage.getJSONObject()); } } } return prunableJSON; } static TransactionImpl parseTransaction(JSONObject transactionData) throws NxtException.NotValidException { TransactionImpl transaction = newTransactionBuilder(transactionData).build(); if (transaction.getSignature() != null && !transaction.checkSignature()) { throw new NxtException.NotValidException("Invalid transaction signature for transaction " + transaction.getJSONObject().toJSONString()); } return transaction; } static TransactionImpl.BuilderImpl newTransactionBuilder(JSONObject transactionData) throws NxtException.NotValidException { try { byte type = ((Long) transactionData.get("type")).byteValue(); byte subtype = ((Long) transactionData.get("subtype")).byteValue(); int timestamp = ((Long) transactionData.get("timestamp")).intValue(); short deadline = ((Long) transactionData.get("deadline")).shortValue(); byte[] senderPublicKey = Convert.parseHexString((String) transactionData.get("senderPublicKey")); long amountNQT = Convert.parseLong(transactionData.get("amountNQT")); long feeNQT = Convert.parseLong(transactionData.get("feeNQT")); String referencedTransactionFullHash = (String) transactionData.get("referencedTransactionFullHash"); byte[] signature = Convert.parseHexString((String) transactionData.get("signature")); Long versionValue = (Long) transactionData.get("version"); byte version = versionValue == null ? 0 : versionValue.byteValue(); JSONObject attachmentData = (JSONObject) transactionData.get("attachment"); int ecBlockHeight = 0; long ecBlockId = 0; if (version > 0) { ecBlockHeight = ((Long) transactionData.get("ecBlockHeight")).intValue(); ecBlockId = Convert.parseUnsignedLong((String) transactionData.get("ecBlockId")); } TransactionType transactionType = TransactionType.findTransactionType(type, subtype); if (transactionType == null) { throw new NxtException.NotValidException("Invalid transaction type: " + type + ", " + subtype); } TransactionImpl.BuilderImpl builder = new BuilderImpl(version, senderPublicKey, amountNQT, feeNQT, deadline, transactionType.parseAttachment(attachmentData)) .timestamp(timestamp) .referencedTransactionFullHash(referencedTransactionFullHash) .signature(signature) .ecBlockHeight(ecBlockHeight) .ecBlockId(ecBlockId); if (transactionType.canHaveRecipient()) { long recipientId = Convert.parseUnsignedLong((String) transactionData.get("recipient")); builder.recipientId(recipientId); } if (attachmentData != null) { builder.appendix(Appendix.Message.parse(attachmentData)); builder.appendix(Appendix.EncryptedMessage.parse(attachmentData)); builder.appendix((Appendix.PublicKeyAnnouncement.parse(attachmentData))); builder.appendix(Appendix.EncryptToSelfMessage.parse(attachmentData)); builder.appendix(Appendix.Phasing.parse(attachmentData)); builder.appendix(Appendix.PrunablePlainMessage.parse(attachmentData)); builder.appendix(Appendix.PrunableEncryptedMessage.parse(attachmentData)); } return builder; } catch (NxtException.NotValidException|RuntimeException e) { Logger.logDebugMessage("Failed to parse transaction: " + transactionData.toJSONString()); throw e; } } @Override public int getECBlockHeight() { return ecBlockHeight; } @Override public long getECBlockId() { return ecBlockId; } @Override public boolean equals(Object o) { return o instanceof TransactionImpl && this.getId() == ((Transaction)o).getId(); } @Override public int hashCode() { return (int)(getId() ^ (getId() >>> 32)); } public boolean verifySignature() { return checkSignature() && Account.setOrVerify(getSenderId(), getSenderPublicKey()); } private volatile boolean hasValidSignature = false; private boolean checkSignature() { if (!hasValidSignature) { hasValidSignature = signature != null && Crypto.verify(signature, zeroSignature(getBytes()), getSenderPublicKey(), useNQT()); } return hasValidSignature; } private int getSize() { return signatureOffset() + 64 + (version > 0 ? 4 + 4 + 8 : 0) + appendagesSize; } @Override public int getFullSize() { int fullSize = getSize() - appendagesSize; for (Appendix.AbstractAppendix appendage : getAppendages()) { fullSize += appendage.getFullSize(); } return fullSize; } private int signatureOffset() { return 1 + 1 + 4 + 2 + 32 + 8 + (useNQT() ? 8 + 8 + 32 : 4 + 4 + 8); } private boolean useNQT() { return this.height > Constants.NQT_BLOCK && (this.timestamp > (Constants.isTestnet ? 12908200 : 14271000) || Nxt.getBlockchain().getHeight() >= Constants.NQT_BLOCK); } private byte[] zeroSignature(byte[] data) { int start = signatureOffset(); for (int i = start; i < start + 64; i++) { data[i] = 0; } return data; } private int getFlags() { int flags = 0; int position = 1; if (message != null) { flags |= position; } position <<= 1; if (encryptedMessage != null) { flags |= position; } position <<= 1; if (publicKeyAnnouncement != null) { flags |= position; } position <<= 1; if (encryptToSelfMessage != null) { flags |= position; } position <<= 1; if (phasing != null) { flags |= position; } position <<= 1; if (prunablePlainMessage != null) { flags |= position; } position <<= 1; if (prunableEncryptedMessage != null) { flags |= position; } return flags; } @Override public void validate() throws NxtException.ValidationException { if (timestamp == 0 ? (deadline != 0 || feeNQT != 0) : (deadline < 1 || feeNQT <= 0) || feeNQT > Constants.MAX_BALANCE_NQT || amountNQT < 0 || amountNQT > Constants.MAX_BALANCE_NQT || type == null) { throw new NxtException.NotValidException("Invalid transaction parameters:\n type: " + type + ", timestamp: " + timestamp + ", deadline: " + deadline + ", fee: " + feeNQT + ", amount: " + amountNQT); } if (referencedTransactionFullHash != null && referencedTransactionFullHash.length != 32) { throw new NxtException.NotValidException("Invalid referenced transaction full hash " + Convert.toHexString(referencedTransactionFullHash)); } if (attachment == null || type != attachment.getTransactionType()) { throw new NxtException.NotValidException("Invalid attachment " + attachment + " for transaction of type " + type); } if (! type.canHaveRecipient()) { if (recipientId != 0 || getAmountNQT() != 0) { throw new NxtException.NotValidException("Transactions of this type must have recipient == 0, amount == 0"); } } if (type.mustHaveRecipient() && version > 0) { if (recipientId == 0) { throw new NxtException.NotValidException("Transactions of this type must have a valid recipient"); } } boolean validatingAtFinish = phasing != null && getSignature() != null && PhasingPoll.getPoll(getId()) != null; for (Appendix.AbstractAppendix appendage : appendages) { appendage.loadPrunable(this); if (! appendage.verifyVersion(this.version)) { throw new NxtException.NotValidException("Invalid attachment version " + appendage.getVersion() + " for transaction version " + this.version); } if (validatingAtFinish) { appendage.validateAtFinish(this); } else { appendage.validate(this); } } if (getFullSize() > Constants.MAX_PAYLOAD_LENGTH) { throw new NxtException.NotValidException("Transaction size " + getFullSize() + " exceeds maximum payload size"); } if (!validatingAtFinish) { long minimumFeeNQT = getMinimumFeeNQT(Nxt.getBlockchain().getHeight()); if (feeNQT < minimumFeeNQT) { throw new NxtException.NotCurrentlyValidException(String.format("Transaction fee %f NXT less than minimum fee %f NXT at height %d", ((double) feeNQT) / Constants.ONE_NXT, ((double) minimumFeeNQT) / Constants.ONE_NXT, Nxt.getBlockchain().getHeight())); } } AccountRestrictions.checkTransaction(this, validatingAtFinish); } // returns false iff double spending boolean applyUnconfirmed() { Account senderAccount = Account.getAccount(getSenderId()); return senderAccount != null && type.applyUnconfirmed(this, senderAccount); } void apply() { Account senderAccount = Account.getAccount(getSenderId()); senderAccount.apply(getSenderPublicKey()); Account recipientAccount = null; if (recipientId != 0) { recipientAccount = Account.getAccount(recipientId); if (recipientAccount == null) { recipientAccount = Account.addOrGetAccount(recipientId); } } if (referencedTransactionFullHash != null && timestamp > Constants.REFERENCED_TRANSACTION_FULL_HASH_BLOCK_TIMESTAMP) { senderAccount.addToUnconfirmedBalanceNQT(getType().getLedgerEvent(), getId(), 0, Constants.UNCONFIRMED_POOL_DEPOSIT_NQT); } if (attachmentIsPhased()) { senderAccount.addToBalanceNQT(getType().getLedgerEvent(), getId(), 0, -feeNQT); } for (Appendix.AbstractAppendix appendage : appendages) { if (!appendage.isPhased(this)) { appendage.loadPrunable(this); appendage.apply(this, senderAccount, recipientAccount); } } } void undoUnconfirmed() { Account senderAccount = Account.getAccount(getSenderId()); type.undoUnconfirmed(this, senderAccount); } boolean attachmentIsDuplicate(Map<TransactionType, Map<String, Integer>> duplicates, boolean atAcceptanceHeight) { if (!attachmentIsPhased() && !atAcceptanceHeight) { // can happen for phased transactions having non-phasable attachment return false; } if (atAcceptanceHeight) { if (AccountRestrictions.isBlockDuplicate(this, duplicates)) { return true; } // all are checked at acceptance height for block duplicates if (type.isBlockDuplicate(this, duplicates)) { return true; } // phased are not further checked at acceptance height if (attachmentIsPhased()) { return false; } } // non-phased at acceptance height, and phased at execution height return type.isDuplicate(this, duplicates); } boolean isUnconfirmedDuplicate(Map<TransactionType, Map<String, Integer>> duplicates) { return type.isUnconfirmedDuplicate(this, duplicates); } private long getMinimumFeeNQT(int blockchainHeight) { long totalFee = 0; for (Appendix.AbstractAppendix appendage : appendages) { appendage.loadPrunable(this); if (blockchainHeight < appendage.getBaselineFeeHeight()) { return 0; // No need to validate fees before baseline block } Fee fee = blockchainHeight >= appendage.getNextFeeHeight() ? appendage.getNextFee(this) : appendage.getBaselineFee(this); totalFee = Math.addExact(totalFee, fee.getFee(this, appendage)); } if (referencedTransactionFullHash != null && blockchainHeight > Constants.SHUFFLING_BLOCK) { totalFee = Math.addExact(totalFee, Constants.ONE_NXT); } return totalFee; } }