From 4a1c3821db9d81addd83e4a9c13b109de63b8c22 Mon Sep 17 00:00:00 2001 From: catbref Date: Fri, 15 Jun 2018 17:16:44 +0100 Subject: [PATCH] Progess on block and transaction processing + tidying up * Code added for calculating an account's generating balance. (CIYAM AT support yet to be added). * Added associated code in Block for calculating next block's timestamp, generating balance, base target, etc. * ValidationResult enum added to Block, mostly to aid debugging. * Block.isValid() now returns ValidationResult instead of boolean. * Block.isValid() now has added proof-of-stake tests. * Some blockchain-related constants, like feature release heights/timestamps, moved from Block to BlockChain. * Added better Block constructor for use when creating a new block. * Added helpful 'navigation' methods to Block to get to block's parent (or child). * Changed visibility of block's individual signature calculators to protected, in favour of public sign() method. * Added asset existence check to Payment.isValid. * All current transaction objects (qora.transaction.*) now have private subclassed transaction variable to save multiple casts in various methods. * Also added to above: * isInvolved(Account) : boolean * getRecipients() : List * getAmount(Account) : BigDecimal * Added BlockRepository.getLastBlock() to fetch highest block in blockchain. * Added diagnostics to HSQLDBRepository.close() to alert if there are any uncommitted changes during closure. (Currently under suspicion due to possible HSQLDB bug!) * Old "TransactionTests" renamed to "SerializationTests" as that's what they really are. * New "TransactionTests" added to test processing of transactions. (Currently only a PaymentTransaction). * PaymentTransformer.toBytes() detects and skips null signature. This was causing issues with Transaction.toBytesLessSignature(). Needs rolling out to other transaction types if acceptable. --- src/data/block/BlockData.java | 4 + src/qora/account/Account.java | 56 +++- src/qora/block/Block.java | 246 ++++++++++++++---- src/qora/block/BlockChain.java | 15 ++ src/qora/block/GenesisBlock.java | 8 +- src/qora/crypto/Crypto.java | 18 +- src/qora/payment/Payment.java | 6 +- .../transaction/CancelOrderTransaction.java | 31 ++- .../transaction/CreateOrderTransaction.java | 31 ++- src/qora/transaction/GenesisTransaction.java | 45 +++- .../transaction/IssueAssetTransaction.java | 59 ++++- src/qora/transaction/MessageTransaction.java | 68 ++++- .../transaction/MultiPaymentTransaction.java | 74 +++++- src/qora/transaction/PaymentTransaction.java | 53 +++- src/qora/transaction/Transaction.java | 69 ++++- .../transaction/TransferAssetTransaction.java | 60 ++++- src/repository/BlockRepository.java | 10 +- .../hsqldb/HSQLDBBlockRepository.java | 4 + src/repository/hsqldb/HSQLDBRepository.java | 23 +- src/test/GenesisTests.java | 3 +- src/test/SerializationTests.java | 93 +++++++ src/test/SignatureTests.java | 4 +- src/test/TransactionTests.java | 166 +++++++----- .../PaymentTransactionTransformer.java | 4 +- 24 files changed, 952 insertions(+), 198 deletions(-) create mode 100644 src/test/SerializationTests.java diff --git a/src/data/block/BlockData.java b/src/data/block/BlockData.java index 33d2bd20..8c49b71b 100644 --- a/src/data/block/BlockData.java +++ b/src/data/block/BlockData.java @@ -69,6 +69,10 @@ public class BlockData { return this.signature; } + public void setSignature(byte[] signature) { + this.signature = signature; + } + public int getVersion() { return this.version; } diff --git a/src/qora/account/Account.java b/src/qora/account/Account.java index 3153d2cf..ff57a610 100644 --- a/src/qora/account/Account.java +++ b/src/qora/account/Account.java @@ -4,6 +4,12 @@ import java.math.BigDecimal; import data.account.AccountBalanceData; import data.account.AccountData; +import data.block.BlockData; +import qora.assets.Asset; +import qora.block.Block; +import qora.block.BlockChain; +import qora.transaction.Transaction; +import repository.BlockRepository; import repository.DataException; import repository.Repository; @@ -26,6 +32,52 @@ public class Account { return this.accountData.getAddress(); } + // More information + + /** + * Calculate current generating balance for this account. + *

+ * This is the current confirmed balance minus amounts received in the last BlockChain.BLOCK_RETARGET_INTERVAL blocks. + * + * @throws DataException + */ + public BigDecimal getGeneratingBalance() throws DataException { + BigDecimal balance = this.getConfirmedBalance(Asset.QORA); + + BlockRepository blockRepository = this.repository.getBlockRepository(); + BlockData blockData = blockRepository.getLastBlock(); + + for (int i = 1; i < BlockChain.BLOCK_RETARGET_INTERVAL && blockData != null && blockData.getHeight() > 1; ++i) { + Block block = new Block(this.repository, blockData); + + for (Transaction transaction : block.getTransactions()) { + if (transaction.isInvolved(this)) { + final BigDecimal amount = transaction.getAmount(this); + + // Subtract positive amounts only + if (amount.compareTo(BigDecimal.ZERO) > 0) + balance = balance.subtract(amount); + } + } + + // TODO + /* + * LinkedHashMap, AT_Transaction> atTxs = db.getATTransactionMap().getATTransactions(block.getHeight(db)); + * Iterator iter = atTxs.values().iterator(); while (iter.hasNext()) { AT_Transaction key = iter.next(); + * + * if (key.getRecipient().equals(this.getAddress())) balance = balance.subtract(BigDecimal.valueOf(key.getAmount(), 8)); } + */ + + blockData = block.getParent(); + } + + // Do not go below 0 + // XXX: How would this even be possible? + balance = balance.max(BigDecimal.ZERO); + + return balance; + } + // Balance manipulations - assetId is 0 for QORA public BigDecimal getBalance(long assetId, int confirmations) { @@ -42,7 +94,7 @@ public class Account { } public void setConfirmedBalance(long assetId, BigDecimal balance) throws DataException { - AccountBalanceData accountBalanceData = new AccountBalanceData(this.accountData.getAddress(), assetId, balance); + AccountBalanceData accountBalanceData = new AccountBalanceData(this.accountData.getAddress(), assetId, balance); this.repository.getAccountRepository().save(accountBalanceData); } @@ -71,7 +123,7 @@ public class Account { * * @param reference * -- null allowed - * @throws DataException + * @throws DataException */ public void setLastReference(byte[] reference) throws DataException { this.repository.getAccountRepository().save(accountData); diff --git a/src/qora/block/Block.java b/src/qora/block/Block.java index 986b5546..2d226434 100644 --- a/src/qora/block/Block.java +++ b/src/qora/block/Block.java @@ -1,19 +1,27 @@ package qora.block; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toMap; + import java.math.BigDecimal; +import java.math.BigInteger; import java.sql.Connection; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Map; import com.google.common.primitives.Bytes; import data.block.BlockData; import data.block.BlockTransactionData; import data.transaction.TransactionData; +import qora.account.Account; import qora.account.PrivateKeyAccount; import qora.account.PublicKeyAccount; import qora.assets.Asset; +import qora.crypto.Crypto; import qora.transaction.GenesisTransaction; import qora.transaction.Transaction; import repository.BlockRepository; @@ -48,6 +56,25 @@ import utils.NTP; public class Block { + // Validation results + public enum ValidationResult { + OK(1), REFERENCE_MISSING(10), PARENT_DOES_NOT_EXIST(11), BLOCKCHAIN_NOT_EMPTY(12), TIMESTAMP_OLDER_THAN_PARENT(20), TIMESTAMP_IN_FUTURE( + 21), TIMESTAMP_MS_INCORRECT(22), VERSION_INCORRECT(30), FEATURE_NOT_YET_RELEASED(31), GENERATING_BALANCE_INCORRECT(40), GENERATOR_NOT_ACCEPTED( + 41), GENESIS_TRANSACTIONS_INVALID(50), TRANSACTION_TIMESTAMP_INVALID(51), TRANSACTION_INVALID(52), TRANSACTION_PROCESSING_FAILED(53); + + public final int value; + + private final static Map map = stream(ValidationResult.values()).collect(toMap(result -> result.value, result -> result)); + + ValidationResult(int value) { + this.value = value; + } + + public static ValidationResult valueOf(int value) { + return map.get(value); + } + } + // Properties protected Repository repository; protected BlockData blockData; @@ -59,20 +86,6 @@ public class Block { // Other useful constants public static final int MAX_BLOCK_BYTES = 1048576; - /** - * Number of blocks between recalculating block's generating balance. - */ - private static final int BLOCK_RETARGET_INTERVAL = 10; - /** - * Maximum acceptable timestamp disagreement offset in milliseconds. - */ - private static final long BLOCK_TIMESTAMP_MARGIN = 500L; - - // Various release timestamps / block heights - public static final int MESSAGE_RELEASE_HEIGHT = 99000; - public static final int AT_BLOCK_HEIGHT_RELEASE = 99000; - public static final long POWFIX_RELEASE_TIMESTAMP = 1456426800000L; // Block Version 3 // 2016-02-25T19:00:00+00:00 - public static final long ASSETS_RELEASE_TIMESTAMP = 0L; // From Qora epoch // Constructors @@ -82,17 +95,37 @@ public class Block { this.generator = new PublicKeyAccount(repository, blockData.getGeneratorPublicKey()); } - // For creating a new block + // For creating a new block? public Block(Repository repository, int version, byte[] reference, long timestamp, BigDecimal generatingBalance, PrivateKeyAccount generator, byte[] atBytes, BigDecimal atFees) { this.repository = repository; this.generator = generator; + this.blockData = new BlockData(version, reference, 0, BigDecimal.ZERO.setScale(8), null, 0, timestamp, generatingBalance, generator.getPublicKey(), null, atBytes, atFees); this.transactions = new ArrayList(); } + public Block(Repository repository, BlockData parentBlockData, PrivateKeyAccount generator, byte[] atBytes, BigDecimal atFees) throws DataException { + this.repository = repository; + this.generator = generator; + + Block parentBlock = new Block(repository, parentBlockData); + + int version = parentBlock.getNextBlockVersion(); + byte[] reference = parentBlockData.getSignature(); + long timestamp = parentBlock.calcNextBlockTimestamp(generator); + BigDecimal generatingBalance = parentBlock.calcNextBlockGeneratingBalance(); + + this.blockData = new BlockData(version, reference, 0, BigDecimal.ZERO.setScale(8), null, 0, timestamp, generatingBalance, generator.getPublicKey(), + null, atBytes, atFees); + + calcGeneratorSignature(); + + this.transactions = new ArrayList(); + } + // Getters/setters public BlockData getBlockData() { @@ -123,9 +156,9 @@ public class Block { * @return 1, 2 or 3 */ public int getNextBlockVersion() { - if (this.blockData.getHeight() < AT_BLOCK_HEIGHT_RELEASE) + if (this.blockData.getHeight() < BlockChain.AT_BLOCK_HEIGHT_RELEASE) return 1; - else if (this.blockData.getTimestamp() < POWFIX_RELEASE_TIMESTAMP) + else if (this.blockData.getTimestamp() < BlockChain.POWFIX_RELEASE_TIMESTAMP) return 2; else return 3; @@ -140,11 +173,14 @@ public class Block { * Within this interval, the generating balance stays the same so the current block's generating balance will be returned. * * @return next block's generating balance - * @throws SQLException + * @throws DataException */ - public BigDecimal getNextBlockGeneratingBalance() throws SQLException { + public BigDecimal calcNextBlockGeneratingBalance() throws DataException { + if (this.blockData.getHeight() == 0) + throw new IllegalStateException("Block height is unset"); + // This block not at the start of an interval? - if (this.blockData.getHeight() % BLOCK_RETARGET_INTERVAL != 0) + if (this.blockData.getHeight() % BlockChain.BLOCK_RETARGET_INTERVAL != 0) return this.blockData.getGeneratingBalance(); // Return cached calculation if we have one @@ -159,7 +195,7 @@ public class Block { BlockData firstBlock = this.blockData; try { - for (int i = 1; firstBlock != null && i < BLOCK_RETARGET_INTERVAL; ++i) + for (int i = 1; firstBlock != null && i < BlockChain.BLOCK_RETARGET_INTERVAL; ++i) firstBlock = blockRepo.fromSignature(firstBlock.getReference()); } catch (DataException e) { firstBlock = null; @@ -173,7 +209,7 @@ public class Block { long previousGeneratingTime = this.blockData.getTimestamp() - firstBlock.getTimestamp(); // Calculate expected forging time (in ms) for a whole interval based on this block's generating balance. - long expectedGeneratingTime = Block.calcForgingDelay(this.blockData.getGeneratingBalance()) * BLOCK_RETARGET_INTERVAL * 1000; + long expectedGeneratingTime = Block.calcForgingDelay(this.blockData.getGeneratingBalance()) * BlockChain.BLOCK_RETARGET_INTERVAL * 1000; // Finally, scale generating balance such that faster than expected previous intervals produce larger generating balances. BigDecimal multiplier = BigDecimal.valueOf((double) expectedGeneratingTime / (double) previousGeneratingTime); @@ -182,8 +218,13 @@ public class Block { return this.cachedNextGeneratingBalance; } + public static long calcBaseTarget(BigDecimal generatingBalance) { + generatingBalance = BlockChain.minMaxBalance(generatingBalance); + return generatingBalance.longValue() * calcForgingDelay(generatingBalance); + } + /** - * Return expected forging delay, in seconds, since previous block based on block's generating balance. + * Return expected forging delay, in seconds, since previous block based on passed generating balance. */ public static long calcForgingDelay(BigDecimal generatingBalance) { generatingBalance = BlockChain.minMaxBalance(generatingBalance); @@ -194,6 +235,60 @@ public class Block { return actualBlockTime; } + private BigInteger calcGeneratorsTarget(Account nextBlockGenerator) throws DataException { + // Start with 32-byte maximum integer representing all possible correct "guesses" + // Where a "correct guess" is an integer greater than the threshold represented by calcBlockHash() + byte[] targetBytes = new byte[32]; + Arrays.fill(targetBytes, Byte.MAX_VALUE); + BigInteger target = new BigInteger(1, targetBytes); + + // Divide by next block's base target + // So if next block requires a higher generating balance then there are fewer remaining "correct guesses" + BigInteger baseTarget = BigInteger.valueOf(calcBaseTarget(calcNextBlockGeneratingBalance())); + target = target.divide(baseTarget); + + // Multiply by account's generating balance + // So the greater the account's generating balance then the greater the remaining "correct guesses" + target = target.multiply(nextBlockGenerator.getGeneratingBalance().toBigInteger()); + + return target; + } + + private BigInteger calcBlockHash() { + byte[] hashData; + + if (this.blockData.getVersion() < 3) + hashData = this.blockData.getSignature(); + else + hashData = Bytes.concat(this.blockData.getSignature(), generator.getPublicKey()); + + // Calculate 32-byte hash as pseudo-random, but deterministic, integer (unique to this generator for v3+ blocks) + byte[] hash = Crypto.digest(hashData); + + // Convert hash to BigInteger form + return new BigInteger(1, hash); + } + + private long calcNextBlockTimestamp(Account nextBlockGenerator) throws DataException { + BigInteger hashValue = calcBlockHash(); + BigInteger target = calcGeneratorsTarget(nextBlockGenerator); + + // If target is zero then generator has no balance so return longest value + if (target.compareTo(BigInteger.ZERO) == 0) + return Long.MAX_VALUE; + + // Use ratio of "correct guesses" to calculate minimum delay until this generator can forge a block + BigInteger seconds = hashValue.divide(target).add(BigInteger.ONE); + + // Calculate next block timestamp using delay + BigInteger timestamp = seconds.multiply(BigInteger.valueOf(1000)).add(BigInteger.valueOf(this.blockData.getTimestamp())); + + // Limit timestamp to maximum long value + timestamp = timestamp.min(BigInteger.valueOf(Long.MAX_VALUE)); + + return timestamp.longValue(); + } + /** * Return block's transactions. *

@@ -222,6 +317,36 @@ public class Block { return this.transactions; } + // Navigation + + /** + * Load parent block's data from repository via this block's reference. + * + * @return parent's BlockData, or null if no parent found + * @throws DataException + */ + public BlockData getParent() throws DataException { + byte[] reference = this.blockData.getReference(); + if (reference == null) + return null; + + return this.repository.getBlockRepository().fromSignature(reference); + } + + /** + * Load child block's data from repository via this block's signature. + * + * @return child's BlockData, or null if no parent found + * @throws DataException + */ + public BlockData getChild() throws DataException { + byte[] signature = this.blockData.getSignature(); + if (signature == null) + return null; + + return this.repository.getBlockRepository().fromReference(signature); + } + // Processing /** @@ -244,6 +369,9 @@ public class Block { if (!(this.generator instanceof PrivateKeyAccount)) throw new IllegalStateException("Block's generator has no private key"); + if (this.blockData.getGeneratorSignature() == null) + throw new IllegalStateException("Cannot calculate transactions signature as block has no generator signature"); + // Check there is space in block try { if (BlockTransformer.getDataLength(this) + TransactionTransformer.getDataLength(transactionData) > MAX_BLOCK_BYTES) @@ -261,7 +389,6 @@ public class Block { // Update totalFees this.blockData.setTotalFees(this.blockData.getTotalFees().add(transactionData.getFee())); - // Update transactions signature calcTransactionsSignature(); return true; @@ -277,7 +404,7 @@ public class Block { * @throws RuntimeException * if somehow the generator signature cannot be calculated */ - public void calcGeneratorSignature() { + protected void calcGeneratorSignature() { if (!(this.generator instanceof PrivateKeyAccount)) throw new IllegalStateException("Block's generator has no private key"); @@ -298,7 +425,7 @@ public class Block { * @throws RuntimeException * if somehow the transactions signature cannot be calculated */ - public void calcTransactionsSignature() { + protected void calcTransactionsSignature() { if (!(this.generator instanceof PrivateKeyAccount)) throw new IllegalStateException("Block's generator has no private key"); @@ -309,6 +436,13 @@ public class Block { } } + public void sign() { + this.calcGeneratorSignature(); + this.calcTransactionsSignature(); + + this.blockData.setSignature(this.getSignature()); + } + public boolean isSignatureValid() { try { // Check generator's signature first @@ -336,39 +470,59 @@ public class Block { * @throws SQLException * @throws DataException */ - public boolean isValid() throws SQLException, DataException { + public ValidationResult isValid() throws DataException { // TODO - // Check parent blocks exists + // Check parent block exists if (this.blockData.getReference() == null) - return false; + return ValidationResult.REFERENCE_MISSING; BlockData parentBlockData = this.repository.getBlockRepository().fromSignature(this.blockData.getReference()); if (parentBlockData == null) - return false; + return ValidationResult.PARENT_DOES_NOT_EXIST; Block parentBlock = new Block(this.repository, parentBlockData); - // Check timestamp is valid, i.e. later than parent timestamp and not in the future, within ~500ms margin - if (this.blockData.getTimestamp() < parentBlockData.getTimestamp() || this.blockData.getTimestamp() - BLOCK_TIMESTAMP_MARGIN > NTP.getTime()) - return false; + // Check timestamp is newer than parent timestamp + if (this.blockData.getTimestamp() <= parentBlockData.getTimestamp() + || this.blockData.getTimestamp() - BlockChain.BLOCK_TIMESTAMP_MARGIN > NTP.getTime()) + return ValidationResult.TIMESTAMP_OLDER_THAN_PARENT; + + // Check timestamp is not in the future (within configurable ~500ms margin) + if (this.blockData.getTimestamp() - BlockChain.BLOCK_TIMESTAMP_MARGIN > NTP.getTime()) + return ValidationResult.TIMESTAMP_IN_FUTURE; // Legacy gen1 test: check timestamp ms is the same as parent timestamp ms? if (this.blockData.getTimestamp() % 1000 != parentBlockData.getTimestamp() % 1000) - return false; + return ValidationResult.TIMESTAMP_MS_INCORRECT; // Check block version if (this.blockData.getVersion() != parentBlock.getNextBlockVersion()) - return false; + return ValidationResult.VERSION_INCORRECT; if (this.blockData.getVersion() < 2 && (this.blockData.getAtBytes() != null || this.blockData.getAtFees() != null)) - return false; + return ValidationResult.FEATURE_NOT_YET_RELEASED; // Check generating balance - if (this.blockData.getGeneratingBalance() != parentBlock.getNextBlockGeneratingBalance()) - return false; + if (this.blockData.getGeneratingBalance() != parentBlock.calcNextBlockGeneratingBalance()) + return ValidationResult.GENERATING_BALANCE_INCORRECT; - // Check generator's proof of stake against block's generating balance - // TODO + // Check generator is allowed to forge this block at this time + BigInteger hashValue = parentBlock.calcBlockHash(); + BigInteger target = parentBlock.calcGeneratorsTarget(this.generator); + + // Multiply target by guesses + long guesses = (this.blockData.getTimestamp() - parentBlockData.getTimestamp()) / 1000; + BigInteger lowerTarget = target.multiply(BigInteger.valueOf(guesses - 1)); + target = target.multiply(BigInteger.valueOf(guesses)); + + // Generator's target must exceed block's hashValue threshold + if (hashValue.compareTo(target) >= 0) + return ValidationResult.GENERATOR_NOT_ACCEPTED; + + // XXX Odd gen1 test: "CHECK IF FIRST BLOCK OF USER" + // Is the comment wrong and this each second elapsed allows generator to test a new "target" window against hashValue? + if (hashValue.compareTo(lowerTarget) < 0) + return ValidationResult.GENERATOR_NOT_ACCEPTED; // Check CIYAM AT if (this.blockData.getAtBytes() != null && this.blockData.getAtBytes().length > 0) { @@ -386,28 +540,28 @@ public class Block { for (Transaction transaction : this.getTransactions()) { // GenesisTransactions are not allowed (GenesisBlock overrides isValid() to allow them) if (transaction instanceof GenesisTransaction) - return false; + return ValidationResult.GENESIS_TRANSACTIONS_INVALID; // Check timestamp and deadline if (transaction.getTransactionData().getTimestamp() > this.blockData.getTimestamp() || transaction.getDeadline() <= this.blockData.getTimestamp()) - return false; + return ValidationResult.TRANSACTION_TIMESTAMP_INVALID; // Check transaction is even valid // NOTE: in Gen1 there was an extra block height passed to DeployATTransaction.isValid if (transaction.isValid() != Transaction.ValidationResult.OK) - return false; + return ValidationResult.TRANSACTION_INVALID; // Process transaction to make sure other transactions validate properly try { transaction.process(); } catch (Exception e) { // LOGGER.error("Exception during transaction processing, tx " + Base58.encode(transaction.getSignature()), e); - return false; + return ValidationResult.TRANSACTION_PROCESSING_FAILED; } } } catch (DataException e) { - return false; + return ValidationResult.TRANSACTION_TIMESTAMP_INVALID; } finally { // Revert back to savepoint try { @@ -421,7 +575,7 @@ public class Block { } // Block is valid - return true; + return ValidationResult.OK; } public void process() throws DataException { diff --git a/src/qora/block/BlockChain.java b/src/qora/block/BlockChain.java index 05082e0e..2adfd3a8 100644 --- a/src/qora/block/BlockChain.java +++ b/src/qora/block/BlockChain.java @@ -25,6 +25,10 @@ public class BlockChain { * Maximum Qora balance. */ public static final BigDecimal MAX_BALANCE = BigDecimal.valueOf(10_000_000_000L).setScale(8); + /** + * Number of blocks between recalculating block's generating balance. + */ + public static final int BLOCK_RETARGET_INTERVAL = 10; /** * Minimum target time between blocks, in seconds. */ @@ -33,6 +37,17 @@ public class BlockChain { * Maximum target time between blocks, in seconds. */ public static final long MAX_BLOCK_TIME = 300; + /** + * Maximum acceptable timestamp disagreement offset in milliseconds. + */ + public static final long BLOCK_TIMESTAMP_MARGIN = 500L; + + // Various release timestamps / block heights + public static final int MESSAGE_RELEASE_HEIGHT = 99000; + public static final int AT_BLOCK_HEIGHT_RELEASE = 99000; + public static final long POWFIX_RELEASE_TIMESTAMP = 1456426800000L; // Block Version 3 // 2016-02-25T19:00:00+00:00 + public static final long ASSETS_RELEASE_TIMESTAMP = 0L; // From Qora epoch + /** * Some sort start-up/initialization/checking method. diff --git a/src/qora/block/GenesisBlock.java b/src/qora/block/GenesisBlock.java index 3737186c..e816556f 100644 --- a/src/qora/block/GenesisBlock.java +++ b/src/qora/block/GenesisBlock.java @@ -289,17 +289,17 @@ public class GenesisBlock extends Block { } @Override - public boolean isValid() throws DataException { + public ValidationResult isValid() throws DataException { // Check there is no other block in DB if (this.repository.getBlockRepository().getBlockchainHeight() != 0) - return false; + return ValidationResult.BLOCKCHAIN_NOT_EMPTY; // Validate transactions for (Transaction transaction : this.getTransactions()) if (transaction.isValid() != Transaction.ValidationResult.OK) - return false; + return ValidationResult.TRANSACTION_INVALID; - return true; + return ValidationResult.OK; } } diff --git a/src/qora/crypto/Crypto.java b/src/qora/crypto/Crypto.java index 678fac0b..10570bc8 100644 --- a/src/qora/crypto/Crypto.java +++ b/src/qora/crypto/Crypto.java @@ -12,7 +12,17 @@ public class Crypto { public static final byte ADDRESS_VERSION = 58; public static final byte AT_ADDRESS_VERSION = 23; + /** + * Returns 32-byte SHA-256 digest of message passed in input. + * + * @param input + * variable-length byte[] message + * @return byte[32] digest, or null if SHA-256 algorithm can't be accessed + */ public static byte[] digest(byte[] input) { + if (input == null) + return null; + try { // SHA2-256 MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); @@ -22,8 +32,14 @@ public class Crypto { } } + /** + * Returns 32-byte digest of two rounds of SHA-256 on message passed in input. + * + * @param input + * variable-length byte[] message + * @return byte[32] digest, or null if SHA-256 algorithm can't be accessed + */ public static byte[] doubleDigest(byte[] input) { - // Two rounds of SHA2-256 return digest(digest(input)); } diff --git a/src/qora/payment/Payment.java b/src/qora/payment/Payment.java index 86782a9a..21837f65 100644 --- a/src/qora/payment/Payment.java +++ b/src/qora/payment/Payment.java @@ -59,8 +59,12 @@ public class Payment { if (!Crypto.isValidAddress(paymentData.getRecipient())) return ValidationResult.INVALID_ADDRESS; - // Check asset amount is integer if asset is not divisible AssetData assetData = assetRepository.fromAssetId(paymentData.getAssetId()); + // Check asset even exists + if (assetData == null) + return ValidationResult.ASSET_DOES_NOT_EXIST; + + // Check asset amount is integer if asset is not divisible if (!assetData.getIsDivisible() && paymentData.getAmount().stripTrailingZeros().scale() > 0) return ValidationResult.INVALID_AMOUNT; diff --git a/src/qora/transaction/CancelOrderTransaction.java b/src/qora/transaction/CancelOrderTransaction.java index 368addf2..0b6137fc 100644 --- a/src/qora/transaction/CancelOrderTransaction.java +++ b/src/qora/transaction/CancelOrderTransaction.java @@ -1,7 +1,9 @@ package qora.transaction; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import data.assets.OrderData; import data.transaction.CancelOrderTransactionData; @@ -17,17 +19,40 @@ import repository.Repository; public class CancelOrderTransaction extends Transaction { + // Properties + private CancelOrderTransactionData cancelOrderTransactionData; + // Constructors public CancelOrderTransaction(Repository repository, TransactionData transactionData) { super(repository, transactionData); + + this.cancelOrderTransactionData = (CancelOrderTransactionData) this.transactionData; + } + + // More information + + public List getRecipientAccounts() { + return new ArrayList(); + } + + public boolean isInvolved(Account account) throws DataException { + return account.getAddress().equals(this.getCreator().getAddress()); + } + + public BigDecimal getAmount(Account account) throws DataException { + BigDecimal amount = BigDecimal.ZERO.setScale(8); + + if (account.getAddress().equals(this.getCreator().getAddress())) + amount = amount.subtract(this.transactionData.getFee()); + + return amount; } // Processing @Override public ValidationResult isValid() throws DataException { - CancelOrderTransactionData cancelOrderTransactionData = (CancelOrderTransactionData) this.transactionData; AssetRepository assetRepository = this.repository.getAssetRepository(); // Check fee is positive @@ -62,11 +87,8 @@ public class CancelOrderTransaction extends Transaction { return ValidationResult.OK; } - // PROCESS/ORPHAN - @Override public void process() throws DataException { - CancelOrderTransactionData cancelOrderTransactionData = (CancelOrderTransactionData) this.transactionData; Account creator = new PublicKeyAccount(this.repository, cancelOrderTransactionData.getCreatorPublicKey()); // Save this transaction itself @@ -89,7 +111,6 @@ public class CancelOrderTransaction extends Transaction { @Override public void orphan() throws DataException { - CancelOrderTransactionData cancelOrderTransactionData = (CancelOrderTransactionData) this.transactionData; Account creator = new PublicKeyAccount(this.repository, cancelOrderTransactionData.getCreatorPublicKey()); // Save this transaction itself diff --git a/src/qora/transaction/CreateOrderTransaction.java b/src/qora/transaction/CreateOrderTransaction.java index 96ec2fbd..a37f03a9 100644 --- a/src/qora/transaction/CreateOrderTransaction.java +++ b/src/qora/transaction/CreateOrderTransaction.java @@ -1,7 +1,9 @@ package qora.transaction; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import data.assets.AssetData; import data.assets.OrderData; @@ -11,7 +13,7 @@ import qora.account.Account; import qora.account.PublicKeyAccount; import qora.assets.Asset; import qora.assets.Order; -import qora.block.Block; +import qora.block.BlockChain; import repository.AssetRepository; import repository.DataException; import repository.Repository; @@ -19,11 +21,33 @@ import repository.Repository; public class CreateOrderTransaction extends Transaction { // Properties + private CreateOrderTransactionData createOrderTransactionData; // Constructors public CreateOrderTransaction(Repository repository, TransactionData transactionData) { super(repository, transactionData); + + this.createOrderTransactionData = (CreateOrderTransactionData) this.transactionData; + } + + // More information + + public List getRecipientAccounts() { + return new ArrayList(); + } + + public boolean isInvolved(Account account) throws DataException { + return account.getAddress().equals(this.getCreator().getAddress()); + } + + public BigDecimal getAmount(Account account) throws DataException { + BigDecimal amount = BigDecimal.ZERO.setScale(8); + + if (account.getAddress().equals(this.getCreator().getAddress())) + amount = amount.subtract(this.transactionData.getFee()); + + return amount; } // Navigation @@ -37,7 +61,6 @@ public class CreateOrderTransaction extends Transaction { // Processing public ValidationResult isValid() throws DataException { - CreateOrderTransactionData createOrderTransactionData = (CreateOrderTransactionData) this.transactionData; long haveAssetId = createOrderTransactionData.getHaveAssetId(); long wantAssetId = createOrderTransactionData.getWantAssetId(); @@ -88,7 +111,7 @@ public class CreateOrderTransaction extends Transaction { // Check creator has enough funds for fee in QORA // NOTE: in Gen1 pre-POWFIX-RELEASE transactions didn't have this check - if (createOrderTransactionData.getTimestamp() >= Block.POWFIX_RELEASE_TIMESTAMP + if (createOrderTransactionData.getTimestamp() >= BlockChain.POWFIX_RELEASE_TIMESTAMP && creator.getConfirmedBalance(Asset.QORA).compareTo(createOrderTransactionData.getFee()) == -1) return ValidationResult.NO_BALANCE; } @@ -106,7 +129,6 @@ public class CreateOrderTransaction extends Transaction { } public void process() throws DataException { - CreateOrderTransactionData createOrderTransactionData = (CreateOrderTransactionData) this.transactionData; Account creator = new PublicKeyAccount(this.repository, createOrderTransactionData.getCreatorPublicKey()); // Update creator's balance due to fee @@ -130,7 +152,6 @@ public class CreateOrderTransaction extends Transaction { } public void orphan() throws DataException { - CreateOrderTransactionData createOrderTransactionData = (CreateOrderTransactionData) this.transactionData; Account creator = new PublicKeyAccount(this.repository, createOrderTransactionData.getCreatorPublicKey()); // Update creator's balance due to fee diff --git a/src/qora/transaction/GenesisTransaction.java b/src/qora/transaction/GenesisTransaction.java index 91fabcd8..1b997f98 100644 --- a/src/qora/transaction/GenesisTransaction.java +++ b/src/qora/transaction/GenesisTransaction.java @@ -2,6 +2,8 @@ package qora.transaction; import java.math.BigDecimal; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import com.google.common.primitives.Bytes; @@ -18,6 +20,9 @@ import transform.transaction.TransactionTransformer; public class GenesisTransaction extends Transaction { + // Properties + private GenesisTransactionData genesisTransactionData; + // Constructors public GenesisTransaction(Repository repository, TransactionData transactionData) { @@ -25,6 +30,38 @@ public class GenesisTransaction extends Transaction { if (this.transactionData.getSignature() == null) this.transactionData.setSignature(this.calcSignature()); + + this.genesisTransactionData = (GenesisTransactionData) this.transactionData; + } + + // More information + + public List getRecipientAccounts() throws DataException { + return Collections.singletonList(new Account(this.repository, genesisTransactionData.getRecipient())); + } + + public boolean isInvolved(Account account) throws DataException { + String address = account.getAddress(); + + if (address.equals(this.getCreator().getAddress())) + return true; + + if (address.equals(genesisTransactionData.getRecipient())) + return true; + + return false; + } + + public BigDecimal getAmount(Account account) throws DataException { + String address = account.getAddress(); + BigDecimal amount = BigDecimal.ZERO.setScale(8); + + // NOTE: genesis transactions have no fee, so no need to test against creator as sender + + if (address.equals(genesisTransactionData.getRecipient())) + amount = amount.add(genesisTransactionData.getAmount()); + + return amount; } // Processing @@ -39,7 +76,7 @@ public class GenesisTransaction extends Transaction { * @throws IllegalStateException */ @Override - public byte[] calcSignature(PrivateKeyAccount signer) { + public void calcSignature(PrivateKeyAccount signer) { throw new IllegalStateException("There is no private key for genesis transactions"); } @@ -77,8 +114,6 @@ public class GenesisTransaction extends Transaction { @Override public ValidationResult isValid() { - GenesisTransactionData genesisTransactionData = (GenesisTransactionData) this.transactionData; - // Check amount is zero or positive if (genesisTransactionData.getAmount().compareTo(BigDecimal.ZERO) == -1) return ValidationResult.NEGATIVE_AMOUNT; @@ -92,8 +127,6 @@ public class GenesisTransaction extends Transaction { @Override public void process() throws DataException { - GenesisTransactionData genesisTransactionData = (GenesisTransactionData) this.transactionData; - // Save this transaction itself this.repository.getTransactionRepository().save(this.transactionData); @@ -107,8 +140,6 @@ public class GenesisTransaction extends Transaction { @Override public void orphan() throws DataException { - GenesisTransactionData genesisTransactionData = (GenesisTransactionData) this.transactionData; - // Delete this transaction this.repository.getTransactionRepository().delete(this.transactionData); diff --git a/src/qora/transaction/IssueAssetTransaction.java b/src/qora/transaction/IssueAssetTransaction.java index 2a5306be..1bbdf190 100644 --- a/src/qora/transaction/IssueAssetTransaction.java +++ b/src/qora/transaction/IssueAssetTransaction.java @@ -2,6 +2,8 @@ package qora.transaction; import java.math.BigDecimal; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import data.assets.AssetData; import data.transaction.IssueAssetTransactionData; @@ -9,7 +11,7 @@ import data.transaction.TransactionData; import qora.account.Account; import qora.account.PublicKeyAccount; import qora.assets.Asset; -import qora.block.Block; +import qora.block.BlockChain; import qora.crypto.Crypto; import repository.DataException; import repository.Repository; @@ -18,21 +20,62 @@ import utils.NTP; public class IssueAssetTransaction extends Transaction { + // Properties + private IssueAssetTransactionData issueAssetTransactionData; + // Constructors public IssueAssetTransaction(Repository repository, TransactionData transactionData) { super(repository, transactionData); + + this.issueAssetTransactionData = (IssueAssetTransactionData) this.transactionData; + } + + // More information + + public List getRecipientAccounts() throws DataException { + return Collections.singletonList(getOwner()); + } + + public boolean isInvolved(Account account) throws DataException { + String address = account.getAddress(); + + if (address.equals(this.getIssuer().getAddress())) + return true; + + if (address.equals(this.getOwner().getAddress())) + return true; + + return false; + } + + public BigDecimal getAmount(Account account) throws DataException { + String address = account.getAddress(); + BigDecimal amount = BigDecimal.ZERO.setScale(8); + + if (address.equals(this.getIssuer().getAddress())) + amount = amount.subtract(this.transactionData.getFee()); + + // NOTE: we're only interested in QORA amounts, and genesis account issued QORA so no need to check owner + + return amount; + } + + // Navigation + + public Account getIssuer() throws DataException { + return new PublicKeyAccount(this.repository, this.issueAssetTransactionData.getIssuerPublicKey()); + } + + public Account getOwner() throws DataException { + return new Account(this.repository, this.issueAssetTransactionData.getOwner()); } // Processing public ValidationResult isValid() throws DataException { - // Lowest cost checks first - - IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) this.transactionData; - // Are IssueAssetTransactions even allowed at this point? - if (NTP.getTime() < Block.ASSETS_RELEASE_TIMESTAMP) + if (NTP.getTime() < BlockChain.ASSETS_RELEASE_TIMESTAMP) return ValidationResult.NOT_YET_RELEASED; // Check owner address is valid @@ -76,8 +119,6 @@ public class IssueAssetTransaction extends Transaction { } public void process() throws DataException { - IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) this.transactionData; - // Issue asset AssetData assetData = new AssetData(issueAssetTransactionData.getOwner(), issueAssetTransactionData.getAssetName(), issueAssetTransactionData.getDescription(), issueAssetTransactionData.getQuantity(), issueAssetTransactionData.getIsDivisible(), @@ -103,8 +144,6 @@ public class IssueAssetTransaction extends Transaction { } public void orphan() throws DataException { - IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) this.transactionData; - // Remove asset from owner Account owner = new Account(this.repository, issueAssetTransactionData.getOwner()); owner.deleteBalance(issueAssetTransactionData.getAssetId()); diff --git a/src/qora/transaction/MessageTransaction.java b/src/qora/transaction/MessageTransaction.java index fff22925..f1f957ed 100644 --- a/src/qora/transaction/MessageTransaction.java +++ b/src/qora/transaction/MessageTransaction.java @@ -1,6 +1,9 @@ package qora.transaction; +import java.math.BigDecimal; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import data.PaymentData; import data.transaction.MessageTransactionData; @@ -8,35 +11,86 @@ import data.transaction.TransactionData; import qora.account.Account; import qora.account.PublicKeyAccount; import qora.assets.Asset; -import qora.block.Block; +import qora.block.BlockChain; import qora.payment.Payment; import repository.DataException; import repository.Repository; public class MessageTransaction extends Transaction { + // Properties + private MessageTransactionData messageTransactionData; + + // Useful constants private static final int MAX_DATA_SIZE = 4000; // Constructors + public MessageTransaction(Repository repository, TransactionData transactionData) { super(repository, transactionData); + + this.messageTransactionData = (MessageTransactionData) this.transactionData; + } + + // More information + + public List getRecipientAccounts() throws DataException { + return Collections.singletonList(new Account(this.repository, messageTransactionData.getRecipient())); + } + + public boolean isInvolved(Account account) throws DataException { + String address = account.getAddress(); + + if (address.equals(this.getSender().getAddress())) + return true; + + if (address.equals(messageTransactionData.getRecipient())) + return true; + + return false; + } + + public BigDecimal getAmount(Account account) throws DataException { + String address = account.getAddress(); + BigDecimal amount = BigDecimal.ZERO.setScale(8); + String senderAddress = this.getSender().getAddress(); + + if (address.equals(senderAddress)) + amount = amount.subtract(this.transactionData.getFee()); + + // We're only interested in QORA + if (messageTransactionData.getAssetId() == Asset.QORA) { + if (address.equals(messageTransactionData.getRecipient())) + amount = amount.add(messageTransactionData.getAmount()); + else if (address.equals(senderAddress)) + amount = amount.subtract(messageTransactionData.getAmount()); + } + + return amount; + } + + // Navigation + + public Account getSender() throws DataException { + return new PublicKeyAccount(this.repository, this.messageTransactionData.getSenderPublicKey()); + } + + public Account getRecipient() throws DataException { + return new Account(this.repository, this.messageTransactionData.getRecipient()); } // Processing private PaymentData getPaymentData() { - MessageTransactionData messageTransactionData = (MessageTransactionData) this.transactionData; return new PaymentData(messageTransactionData.getRecipient(), Asset.QORA, messageTransactionData.getAmount()); } public ValidationResult isValid() throws DataException { - MessageTransactionData messageTransactionData = (MessageTransactionData) this.transactionData; - // Are message transactions even allowed at this point? if (messageTransactionData.getVersion() != MessageTransaction.getVersionByTimestamp(messageTransactionData.getTimestamp())) return ValidationResult.NOT_YET_RELEASED; - if (this.repository.getBlockRepository().getBlockchainHeight() < Block.MESSAGE_RELEASE_HEIGHT) + if (this.repository.getBlockRepository().getBlockchainHeight() < BlockChain.MESSAGE_RELEASE_HEIGHT) return ValidationResult.NOT_YET_RELEASED; // Check data length @@ -57,8 +111,6 @@ public class MessageTransaction extends Transaction { } public void process() throws DataException { - MessageTransactionData messageTransactionData = (MessageTransactionData) this.transactionData; - // Save this transaction itself this.repository.getTransactionRepository().save(this.transactionData); @@ -68,8 +120,6 @@ public class MessageTransaction extends Transaction { } public void orphan() throws DataException { - MessageTransactionData messageTransactionData = (MessageTransactionData) this.transactionData; - // Delete this transaction itself this.repository.getTransactionRepository().delete(this.transactionData); diff --git a/src/qora/transaction/MultiPaymentTransaction.java b/src/qora/transaction/MultiPaymentTransaction.java index 30b3b996..a8def611 100644 --- a/src/qora/transaction/MultiPaymentTransaction.java +++ b/src/qora/transaction/MultiPaymentTransaction.java @@ -1,14 +1,17 @@ package qora.transaction; +import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import data.PaymentData; import data.transaction.MultiPaymentTransactionData; import data.transaction.TransactionData; +import qora.account.Account; import qora.account.PublicKeyAccount; import qora.assets.Asset; -import qora.block.Block; +import qora.block.BlockChain; import qora.payment.Payment; import repository.DataException; import repository.Repository; @@ -16,21 +19,78 @@ import utils.NTP; public class MultiPaymentTransaction extends Transaction { + // Properties + private MultiPaymentTransactionData multiPaymentTransactionData; + + // Useful constants private static final int MAX_PAYMENTS_COUNT = 400; // Constructors public MultiPaymentTransaction(Repository repository, TransactionData transactionData) { super(repository, transactionData); + + this.multiPaymentTransactionData = (MultiPaymentTransactionData) this.transactionData; } + // More information + + public List getRecipientAccounts() throws DataException { + List recipients = new ArrayList(); + + for (PaymentData paymentData : multiPaymentTransactionData.getPayments()) + recipients.add(new Account(this.repository, paymentData.getRecipient())); + + return recipients; + } + + public boolean isInvolved(Account account) throws DataException { + String address = account.getAddress(); + + if (address.equals(this.getSender().getAddress())) + return true; + + for (PaymentData paymentData : multiPaymentTransactionData.getPayments()) + if (address.equals(paymentData.getRecipient())) + return true; + + return false; + } + + public BigDecimal getAmount(Account account) throws DataException { + String address = account.getAddress(); + BigDecimal amount = BigDecimal.ZERO.setScale(8); + String senderAddress = this.getSender().getAddress(); + + if (address.equals(senderAddress)) + amount = amount.subtract(this.transactionData.getFee()); + + // We're only interested in QORA + for (PaymentData paymentData : multiPaymentTransactionData.getPayments()) + if (paymentData.getAssetId() == Asset.QORA) { + if (address.equals(paymentData.getRecipient())) + amount = amount.add(paymentData.getAmount()); + else if (address.equals(senderAddress)) + amount = amount.subtract(paymentData.getAmount()); + } + + return amount; + } + + // Navigation + + public Account getSender() throws DataException { + return new PublicKeyAccount(this.repository, this.multiPaymentTransactionData.getSenderPublicKey()); + } + + // Processing + @Override public ValidationResult isValid() throws DataException { - MultiPaymentTransactionData multiPaymentTransactionData = (MultiPaymentTransactionData) this.transactionData; List payments = multiPaymentTransactionData.getPayments(); // Are MultiPaymentTransactions even allowed at this point? - if (NTP.getTime() < Block.ASSETS_RELEASE_TIMESTAMP) + if (NTP.getTime() < BlockChain.ASSETS_RELEASE_TIMESTAMP) return ValidationResult.NOT_YET_RELEASED; // Check number of payments @@ -45,19 +105,15 @@ public class MultiPaymentTransaction extends Transaction { // Check sender has enough funds for fee // NOTE: in Gen1 pre-POWFIX-RELEASE transactions didn't have this check - if (multiPaymentTransactionData.getTimestamp() >= Block.POWFIX_RELEASE_TIMESTAMP + if (multiPaymentTransactionData.getTimestamp() >= BlockChain.POWFIX_RELEASE_TIMESTAMP && sender.getConfirmedBalance(Asset.QORA).compareTo(multiPaymentTransactionData.getFee()) == -1) return ValidationResult.NO_BALANCE; return new Payment(this.repository).isValid(multiPaymentTransactionData.getSenderPublicKey(), payments, multiPaymentTransactionData.getFee()); } - // PROCESS/ORPHAN - @Override public void process() throws DataException { - MultiPaymentTransactionData multiPaymentTransactionData = (MultiPaymentTransactionData) this.transactionData; - // Save this transaction itself this.repository.getTransactionRepository().save(this.transactionData); @@ -68,8 +124,6 @@ public class MultiPaymentTransaction extends Transaction { @Override public void orphan() throws DataException { - MultiPaymentTransactionData multiPaymentTransactionData = (MultiPaymentTransactionData) this.transactionData; - // Delete this transaction itself this.repository.getTransactionRepository().delete(this.transactionData); diff --git a/src/qora/transaction/PaymentTransaction.java b/src/qora/transaction/PaymentTransaction.java index 28d24a86..31d5cf21 100644 --- a/src/qora/transaction/PaymentTransaction.java +++ b/src/qora/transaction/PaymentTransaction.java @@ -1,6 +1,9 @@ package qora.transaction; +import java.math.BigDecimal; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import data.PaymentData; import data.transaction.PaymentTransactionData; @@ -14,22 +17,62 @@ import repository.Repository; public class PaymentTransaction extends Transaction { + // Properties + private PaymentTransactionData paymentTransactionData; + // Constructors public PaymentTransaction(Repository repository, TransactionData transactionData) { super(repository, transactionData); + + this.paymentTransactionData = (PaymentTransactionData) this.transactionData; + } + + // More information + + public List getRecipientAccounts() throws DataException { + return Collections.singletonList(new Account(this.repository, paymentTransactionData.getRecipient())); + } + + public boolean isInvolved(Account account) throws DataException { + String address = account.getAddress(); + + if (address.equals(this.getSender().getAddress())) + return true; + + if (address.equals(paymentTransactionData.getRecipient())) + return true; + + return false; + } + + public BigDecimal getAmount(Account account) throws DataException { + String address = account.getAddress(); + BigDecimal amount = BigDecimal.ZERO.setScale(8); + String senderAddress = this.getSender().getAddress(); + + if (address.equals(senderAddress)) + amount = amount.subtract(this.transactionData.getFee()).subtract(paymentTransactionData.getAmount()); + + if (address.equals(paymentTransactionData.getRecipient())) + amount = amount.add(paymentTransactionData.getAmount()); + + return amount; + } + + // Navigation + + public Account getSender() throws DataException { + return new PublicKeyAccount(this.repository, this.paymentTransactionData.getSenderPublicKey()); } // Processing private PaymentData getPaymentData() { - PaymentTransactionData paymentTransactionData = (PaymentTransactionData) this.transactionData; return new PaymentData(paymentTransactionData.getRecipient(), Asset.QORA, paymentTransactionData.getAmount()); } public ValidationResult isValid() throws DataException { - PaymentTransactionData paymentTransactionData = (PaymentTransactionData) this.transactionData; - // Check reference is correct Account sender = new PublicKeyAccount(repository, paymentTransactionData.getSenderPublicKey()); if (!Arrays.equals(sender.getLastReference(), paymentTransactionData.getReference())) @@ -40,8 +83,6 @@ public class PaymentTransaction extends Transaction { } public void process() throws DataException { - PaymentTransactionData paymentTransactionData = (PaymentTransactionData) this.transactionData; - // Save this transaction itself this.repository.getTransactionRepository().save(this.transactionData); @@ -51,8 +92,6 @@ public class PaymentTransaction extends Transaction { } public void orphan() throws DataException { - PaymentTransactionData paymentTransactionData = (PaymentTransactionData) this.transactionData; - // Delete this transaction this.repository.getTransactionRepository().delete(this.transactionData); diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java index f524deef..b65e4e06 100644 --- a/src/qora/transaction/Transaction.java +++ b/src/qora/transaction/Transaction.java @@ -3,15 +3,17 @@ package qora.transaction; import java.math.BigDecimal; import java.math.MathContext; import java.util.Arrays; +import java.util.List; import java.util.Map; import static java.util.Arrays.stream; import static java.util.stream.Collectors.toMap; import data.block.BlockData; import data.transaction.TransactionData; +import qora.account.Account; import qora.account.PrivateKeyAccount; import qora.account.PublicKeyAccount; -import qora.block.Block; +import qora.block.BlockChain; import repository.DataException; import repository.Repository; import settings.Settings; @@ -149,7 +151,7 @@ public abstract class Transaction { } public static int getVersionByTimestamp(long timestamp) { - if (timestamp < Block.POWFIX_RELEASE_TIMESTAMP) { + if (timestamp < BlockChain.POWFIX_RELEASE_TIMESTAMP) { return 1; } else { return 3; @@ -183,12 +185,53 @@ public abstract class Transaction { return blockChainHeight - ourHeight + 1; } + /** + * Returns a list of recipient accounts for this transaction. + * + * @return list of recipients accounts, or empty list if none + * @throws DataException + */ + public abstract List getRecipientAccounts() throws DataException; + + /** + * Returns whether passed account is an involved party in this transaction. + *

+ * Account could be sender, or any one of the potential recipients. + * + * @param account + * @return true if account is involved, false otherwise + * @throws DataException + */ + public abstract boolean isInvolved(Account account) throws DataException; + + /** + * Returns amount of QORA lost/gained by passed account due to this transaction. + *

+ * Amounts "lost", e.g. sent by sender and fees, are returned as negative values.
+ * Amounts "gained", e.g. QORA sent to recipient, are returned as positive values. + * + * @param account + * @return Amount of QORA lost/gained by account, or BigDecimal.ZERO otherwise + * @throws DataException + */ + public abstract BigDecimal getAmount(Account account) throws DataException; + // Navigation /** - * Load encapsulating Block from DB, if any + * Return transaction's "creator" account. * - * @return Block, or null if transaction is not in a Block + * @return creator + * @throws DataException + */ + protected Account getCreator() throws DataException { + return new PublicKeyAccount(this.repository, this.transactionData.getCreatorPublicKey()); + } + + /** + * Load encapsulating block's data from repository, if any + * + * @return BlockData, or null if transaction is not in a Block * @throws DataException */ public BlockData getBlock() throws DataException { @@ -196,9 +239,9 @@ public abstract class Transaction { } /** - * Load parent Transaction from DB via this transaction's reference. + * Load parent's transaction data from repository via this transaction's reference. * - * @return Transaction, or null if no parent found (which should not happen) + * @return Parent's TransactionData, or null if no parent found (which should not happen) * @throws DataException */ public TransactionData getParent() throws DataException { @@ -210,9 +253,9 @@ public abstract class Transaction { } /** - * Load child Transaction from DB, if any. + * Load child's transaction data from repository, if any. * - * @return Transaction, or null if no child found + * @return Child's TransactionData, or null if no child found * @throws DataException */ public TransactionData getChild() throws DataException { @@ -220,7 +263,7 @@ public abstract class Transaction { if (signature == null) return null; - return this.repository.getTransactionRepository().fromSignature(signature); + return this.repository.getTransactionRepository().fromReference(signature); } /** @@ -233,6 +276,10 @@ public abstract class Transaction { private byte[] toBytesLessSignature() { try { byte[] bytes = TransactionTransformer.toBytes(this.transactionData); + + if (this.transactionData.getSignature() == null) + return bytes; + return Arrays.copyOf(bytes, bytes.length - Transformer.SIGNATURE_LENGTH); } catch (TransformationException e) { // XXX this isn't good @@ -242,8 +289,8 @@ public abstract class Transaction { // Processing - public byte[] calcSignature(PrivateKeyAccount signer) { - return signer.sign(this.toBytesLessSignature()); + public void calcSignature(PrivateKeyAccount signer) { + this.transactionData.setSignature(signer.sign(this.toBytesLessSignature())); } public boolean isSignatureValid() { diff --git a/src/qora/transaction/TransferAssetTransaction.java b/src/qora/transaction/TransferAssetTransaction.java index 9e72e490..e6d861a8 100644 --- a/src/qora/transaction/TransferAssetTransaction.java +++ b/src/qora/transaction/TransferAssetTransaction.java @@ -1,29 +1,81 @@ package qora.transaction; +import java.math.BigDecimal; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import data.PaymentData; import data.transaction.TransactionData; import data.transaction.TransferAssetTransactionData; import utils.NTP; +import qora.account.Account; import qora.account.PublicKeyAccount; -import qora.block.Block; +import qora.assets.Asset; +import qora.block.BlockChain; import qora.payment.Payment; import repository.DataException; import repository.Repository; public class TransferAssetTransaction extends Transaction { + // Properties + private TransferAssetTransactionData transferAssetTransactionData; + // Constructors public TransferAssetTransaction(Repository repository, TransactionData transactionData) { super(repository, transactionData); + + this.transferAssetTransactionData = (TransferAssetTransactionData) this.transactionData; + } + + // More information + + public List getRecipientAccounts() throws DataException { + return Collections.singletonList(new Account(this.repository, transferAssetTransactionData.getRecipient())); + } + + public boolean isInvolved(Account account) throws DataException { + String address = account.getAddress(); + + if (address.equals(this.getSender().getAddress())) + return true; + + if (address.equals(transferAssetTransactionData.getRecipient())) + return true; + + return false; + } + + public BigDecimal getAmount(Account account) throws DataException { + String address = account.getAddress(); + BigDecimal amount = BigDecimal.ZERO.setScale(8); + String senderAddress = this.getSender().getAddress(); + + if (address.equals(senderAddress)) + amount = amount.subtract(this.transactionData.getFee()); + + // We're only interested in QORA amounts + if (transferAssetTransactionData.getAssetId() == Asset.QORA) { + if (address.equals(transferAssetTransactionData.getRecipient())) + amount = amount.add(transferAssetTransactionData.getAmount()); + else if (address.equals(senderAddress)) + amount = amount.subtract(transferAssetTransactionData.getAmount()); + } + + return amount; + } + + // Navigation + + public Account getSender() throws DataException { + return new PublicKeyAccount(this.repository, this.transferAssetTransactionData.getSenderPublicKey()); } // Processing private PaymentData getPaymentData() { - TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) this.transactionData; return new PaymentData(transferAssetTransactionData.getRecipient(), transferAssetTransactionData.getAssetId(), transferAssetTransactionData.getAmount()); } @@ -33,7 +85,7 @@ public class TransferAssetTransaction extends Transaction { TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) this.transactionData; // Are IssueAssetTransactions even allowed at this point? - if (NTP.getTime() < Block.ASSETS_RELEASE_TIMESTAMP) + if (NTP.getTime() < BlockChain.ASSETS_RELEASE_TIMESTAMP) return ValidationResult.NOT_YET_RELEASED; // Check reference is correct @@ -46,8 +98,6 @@ public class TransferAssetTransaction extends Transaction { return new Payment(this.repository).isValid(transferAssetTransactionData.getSenderPublicKey(), getPaymentData(), transferAssetTransactionData.getFee()); } - // PROCESS/ORPHAN - @Override public void process() throws DataException { TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) this.transactionData; diff --git a/src/repository/BlockRepository.java b/src/repository/BlockRepository.java index 6d6213c1..38861374 100644 --- a/src/repository/BlockRepository.java +++ b/src/repository/BlockRepository.java @@ -24,12 +24,20 @@ public interface BlockRepository { public int getHeightFromSignature(byte[] signature) throws DataException; /** - * Return highest block height from DB. + * Return highest block height from repository. * * @return height, or 0 if there are no blocks in DB (not very likely). */ public int getBlockchainHeight() throws DataException; + /** + * Return highest block in blockchain. + * + * @return highest block's data + * @throws DataException + */ + public BlockData getLastBlock() throws DataException; + public List getTransactionsFromSignature(byte[] signature) throws DataException; public void save(BlockData blockData) throws DataException; diff --git a/src/repository/hsqldb/HSQLDBBlockRepository.java b/src/repository/hsqldb/HSQLDBBlockRepository.java index 8887ce8d..a0a3f2ed 100644 --- a/src/repository/hsqldb/HSQLDBBlockRepository.java +++ b/src/repository/hsqldb/HSQLDBBlockRepository.java @@ -101,6 +101,10 @@ public class HSQLDBBlockRepository implements BlockRepository { } } + public BlockData getLastBlock() throws DataException { + return fromHeight(getBlockchainHeight()); + } + public List getTransactionsFromSignature(byte[] signature) throws DataException { List transactions = new ArrayList(); diff --git a/src/repository/hsqldb/HSQLDBRepository.java b/src/repository/hsqldb/HSQLDBRepository.java index e2984203..7864a5fd 100644 --- a/src/repository/hsqldb/HSQLDBRepository.java +++ b/src/repository/hsqldb/HSQLDBRepository.java @@ -7,6 +7,7 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; import repository.AccountRepository; import repository.AssetRepository; @@ -18,7 +19,7 @@ import repository.hsqldb.transaction.HSQLDBTransactionRepository; public class HSQLDBRepository implements Repository { - Connection connection; + protected Connection connection; // NB: no visibility modifier so only callable from within same package HSQLDBRepository(Connection connection) { @@ -68,11 +69,25 @@ public class HSQLDBRepository implements Repository { @Override public void close() throws DataException { try { + // Diagnostic check for uncommitted changes + Statement stmt = this.connection.createStatement(); + if (!stmt.execute("SELECT transaction, transaction_size FROM information_schema.system_sessions")) // TRANSACTION_SIZE() broken? + throw new DataException("Unable to check repository status during close"); + + ResultSet rs = stmt.getResultSet(); + if (rs == null || !rs.next()) + throw new DataException("Unable to check repository status during close"); + + boolean inTransaction = rs.getBoolean(1); + int transactionCount = rs.getInt(2); + if (inTransaction && transactionCount != 0) + System.out.println("Uncommitted changes (" + transactionCount + ") during repository close"); + // give connection back to the pool this.connection.close(); this.connection = null; } catch (SQLException e) { - throw new DataException("close error", e); + throw new DataException("Error while closing repository", e); } } @@ -171,7 +186,7 @@ public class HSQLDBRepository implements Repository { } /** - * Efficiently query database for existing of matching row. + * Efficiently query database for existence of matching row. *

* {@code whereClause} is SQL "WHERE" clause containing "?" placeholders suitable for use with PreparedStatements. *

@@ -179,7 +194,7 @@ public class HSQLDBRepository implements Repository { *

* {@code String manufacturer = "Lamborghini";}
* {@code int maxMileage = 100_000;}
- * {@code boolean isAvailable = DB.exists("Cars", "manufacturer = ? AND mileage <= ?", manufacturer, maxMileage);} + * {@code boolean isAvailable = exists("Cars", "manufacturer = ? AND mileage <= ?", manufacturer, maxMileage);} * * @param tableName * @param whereClause diff --git a/src/test/GenesisTests.java b/src/test/GenesisTests.java index 67758300..aa2f002b 100644 --- a/src/test/GenesisTests.java +++ b/src/test/GenesisTests.java @@ -12,6 +12,7 @@ import org.junit.Test; import data.transaction.TransactionData; import qora.account.Account; import qora.assets.Asset; +import qora.block.Block; import qora.block.GenesisBlock; import qora.transaction.Transaction; import repository.DataException; @@ -46,7 +47,7 @@ public class GenesisTests { assertNotNull(block); assertTrue(block.isSignatureValid()); // Note: only true if blockchain is empty - assertTrue(block.isValid()); + assertEquals(Block.ValidationResult.OK, block.isValid()); List transactions = block.getTransactions(); assertNotNull(transactions); diff --git a/src/test/SerializationTests.java b/src/test/SerializationTests.java new file mode 100644 index 00000000..db7a4748 --- /dev/null +++ b/src/test/SerializationTests.java @@ -0,0 +1,93 @@ +package test; + +import static org.junit.Assert.*; + +import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; + +import data.block.BlockData; +import data.transaction.GenesisTransactionData; +import data.transaction.TransactionData; +import qora.block.Block; +import qora.block.GenesisBlock; +import qora.transaction.GenesisTransaction; +import qora.transaction.Transaction; +import repository.DataException; +import repository.Repository; +import repository.RepositoryManager; +import transform.TransformationException; +import transform.transaction.TransactionTransformer; + +public class SerializationTests extends Common { + + @Test + public void testGenesisSerialization() throws TransformationException, DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + GenesisBlock block = new GenesisBlock(repository); + + GenesisTransaction transaction = (GenesisTransaction) block.getTransactions().get(1); + assertNotNull(transaction); + + GenesisTransactionData genesisTransactionData = (GenesisTransactionData) transaction.getTransactionData(); + + System.out.println(genesisTransactionData.getTimestamp() + ": " + genesisTransactionData.getRecipient() + " received " + + genesisTransactionData.getAmount().toPlainString()); + + byte[] bytes = TransactionTransformer.toBytes(genesisTransactionData); + + GenesisTransactionData parsedTransactionData = (GenesisTransactionData) TransactionTransformer.fromBytes(bytes); + + System.out.println(parsedTransactionData.getTimestamp() + ": " + parsedTransactionData.getRecipient() + " received " + + parsedTransactionData.getAmount().toPlainString()); + + /* + * NOTE: parsedTransactionData.getSignature() will be null as no signature is present in serialized bytes and calculating the signature is performed + * by GenesisTransaction, not GenesisTransactionData + */ + // Not applicable: assertTrue(Arrays.equals(genesisTransactionData.getSignature(), parsedTransactionData.getSignature())); + + GenesisTransaction parsedTransaction = new GenesisTransaction(repository, parsedTransactionData); + assertTrue(Arrays.equals(genesisTransactionData.getSignature(), parsedTransaction.getTransactionData().getSignature())); + } + } + + private void testGenericSerialization(TransactionData transactionData) throws TransformationException { + assertNotNull(transactionData); + + byte[] bytes = TransactionTransformer.toBytes(transactionData); + + TransactionData parsedTransactionData = TransactionTransformer.fromBytes(bytes); + + assertTrue(Arrays.equals(transactionData.getSignature(), parsedTransactionData.getSignature())); + + assertEquals(TransactionTransformer.getDataLength(transactionData), bytes.length); + } + + @Test + public void testPaymentSerialization() throws TransformationException, DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + // Block 949 has lots of varied transactions + // Blocks 390 & 754 have only payment transactions + BlockData blockData = repository.getBlockRepository().fromHeight(754); + assertNotNull("Block 754 is required for this test", blockData); + + Block block = new Block(repository, blockData); + + List transactions = block.getTransactions(); + assertNotNull(transactions); + + for (Transaction transaction : transactions) + testGenericSerialization(transaction.getTransactionData()); + } + } + + @Test + public void testMessageSerialization() throws SQLException, TransformationException { + // Message transactions went live block 99000 + // Some transactions to be found in block 99001/2/5/6 + } + +} \ No newline at end of file diff --git a/src/test/SignatureTests.java b/src/test/SignatureTests.java index 9ac52073..bffc7144 100644 --- a/src/test/SignatureTests.java +++ b/src/test/SignatureTests.java @@ -53,9 +53,7 @@ public class SignatureTests extends Common { BigDecimal atFees = null; Block block = new Block(repository, version, reference, timestamp, generatingBalance, generator, atBytes, atFees); - - block.calcGeneratorSignature(); - block.calcTransactionsSignature(); + block.sign(); assertTrue(block.isSignatureValid()); } diff --git a/src/test/TransactionTests.java b/src/test/TransactionTests.java index f7368603..ac63ef14 100644 --- a/src/test/TransactionTests.java +++ b/src/test/TransactionTests.java @@ -2,92 +2,128 @@ package test; import static org.junit.Assert.*; -import java.sql.SQLException; -import java.util.Arrays; -import java.util.List; +import java.math.BigDecimal; +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.Test; +import com.google.common.hash.HashCode; + +import data.account.AccountBalanceData; +import data.account.AccountData; import data.block.BlockData; -import data.transaction.GenesisTransactionData; -import data.transaction.TransactionData; +import data.transaction.PaymentTransactionData; +import qora.account.Account; +import qora.account.PrivateKeyAccount; +import qora.account.PublicKeyAccount; +import qora.assets.Asset; import qora.block.Block; -import qora.block.GenesisBlock; -import qora.transaction.GenesisTransaction; +import qora.block.BlockChain; +import qora.transaction.PaymentTransaction; import qora.transaction.Transaction; +import qora.transaction.Transaction.ValidationResult; +import repository.AccountRepository; import repository.DataException; import repository.Repository; +import repository.RepositoryFactory; import repository.RepositoryManager; -import transform.TransformationException; -import transform.transaction.TransactionTransformer; +import repository.hsqldb.HSQLDBRepositoryFactory; +import utils.NTP; -public class TransactionTests extends Common { +// Don't extend Common as we want to use an in-memory database +public class TransactionTests { + + private static final String connectionUrl = "jdbc:hsqldb:mem:db/test;create=true;close_result=true;sql.strict_exec=true;sql.enforce_names=true;sql.syntax_mys=true"; + + private static final byte[] generatorSeed = HashCode.fromString("0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210").asBytes(); + private static final byte[] senderSeed = HashCode.fromString("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef").asBytes(); + private static final byte[] recipientSeed = HashCode.fromString("fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210").asBytes(); + + @BeforeClass + public static void setRepository() throws DataException { + RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl); + RepositoryManager.setRepositoryFactory(repositoryFactory); + } + + @AfterClass + public static void closeRepository() throws DataException { + RepositoryManager.closeRepositoryFactory(); + } @Test - public void testGenesisSerialization() throws TransformationException, DataException { + public void testPaymentTransactions() throws DataException { try (final Repository repository = RepositoryManager.getRepository()) { - GenesisBlock block = new GenesisBlock(repository); - - GenesisTransaction transaction = (GenesisTransaction) block.getTransactions().get(1); - assertNotNull(transaction); - - GenesisTransactionData genesisTransactionData = (GenesisTransactionData) transaction.getTransactionData(); - - System.out.println(genesisTransactionData.getTimestamp() + ": " + genesisTransactionData.getRecipient() + " received " - + genesisTransactionData.getAmount().toPlainString()); - - byte[] bytes = TransactionTransformer.toBytes(genesisTransactionData); - - GenesisTransactionData parsedTransactionData = (GenesisTransactionData) TransactionTransformer.fromBytes(bytes); - - System.out.println(parsedTransactionData.getTimestamp() + ": " + parsedTransactionData.getRecipient() + " received " - + parsedTransactionData.getAmount().toPlainString()); - - /* - * NOTE: parsedTransactionData.getSignature() will be null as no signature is present in serialized bytes and calculating the signature is performed - * by GenesisTransaction, not GenesisTransactionData - */ - // Not applicable: assertTrue(Arrays.equals(genesisTransactionData.getSignature(), parsedTransactionData.getSignature())); - - GenesisTransaction parsedTransaction = new GenesisTransaction(repository, parsedTransactionData); - assertTrue(Arrays.equals(genesisTransactionData.getSignature(), parsedTransaction.getTransactionData().getSignature())); + assertEquals("Blockchain should be empty for this test", 0, repository.getBlockRepository().getBlockchainHeight()); } - } - private void testGenericSerialization(TransactionData transactionData) throws TransformationException { - assertNotNull(transactionData); + // This needs to be called outside of acquiring our own repository or it will deadlock + BlockChain.validate(); - byte[] bytes = TransactionTransformer.toBytes(transactionData); - - TransactionData parsedTransactionData = TransactionTransformer.fromBytes(bytes); - - assertTrue(Arrays.equals(transactionData.getSignature(), parsedTransactionData.getSignature())); - - assertEquals(TransactionTransformer.getDataLength(transactionData), bytes.length); - } - - @Test - public void testPaymentSerialization() throws TransformationException, DataException { try (final Repository repository = RepositoryManager.getRepository()) { - // Block 949 has lots of varied transactions - // Blocks 390 & 754 have only payment transactions - BlockData blockData = repository.getBlockRepository().fromHeight(754); - assertNotNull("Block 754 is required for this test", blockData); + // Grab genesis block + BlockData genesisBlockData = repository.getBlockRepository().fromHeight(1); - Block block = new Block(repository, blockData); + AccountRepository accountRepository = repository.getAccountRepository(); - List transactions = block.getTransactions(); - assertNotNull(transactions); + // Create test generator account + BigDecimal generatorBalance = BigDecimal.valueOf(1_000_000_000L); + PrivateKeyAccount generator = new PrivateKeyAccount(repository, generatorSeed); + accountRepository.save(new AccountData(generator.getAddress(), generatorSeed)); + accountRepository.save(new AccountBalanceData(generator.getAddress(), Asset.QORA, generatorBalance)); - for (Transaction transaction : transactions) - testGenericSerialization(transaction.getTransactionData()); + // Create test sender account + PrivateKeyAccount sender = new PrivateKeyAccount(repository, senderSeed); + + // Mock account + byte[] reference = senderSeed; + accountRepository.save(new AccountData(sender.getAddress(), reference)); + + // Mock balance + BigDecimal initialBalance = BigDecimal.valueOf(1_000_000L); + accountRepository.save(new AccountBalanceData(sender.getAddress(), Asset.QORA, initialBalance)); + + repository.saveChanges(); + + // Make a new payment transaction + Account recipient = new PublicKeyAccount(repository, recipientSeed); + BigDecimal amount = BigDecimal.valueOf(1_000L); + BigDecimal fee = BigDecimal.ONE; + long timestamp = genesisBlockData.getTimestamp() + 1_000; + PaymentTransactionData paymentTransactionData = new PaymentTransactionData(sender.getPublicKey(), recipient.getAddress(), amount, fee, timestamp, + reference); + + Transaction paymentTransaction = new PaymentTransaction(repository, paymentTransactionData); + paymentTransaction.calcSignature(sender); + assertTrue(paymentTransaction.isSignatureValid()); + assertEquals(ValidationResult.OK, paymentTransaction.isValid()); + + // Forge new block with payment transaction + Block block = new Block(repository, genesisBlockData, generator, null, null); + block.addTransaction(paymentTransactionData); + block.sign(); + + assertTrue("Block signatures invalid", block.isSignatureValid()); + assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); + + block.process(); + repository.saveChanges(); + + // Check sender's balance + BigDecimal expectedBalance = initialBalance.subtract(amount).subtract(fee); + BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); + assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + + // Fee should be in generator's balance + expectedBalance = generatorBalance.add(fee); + actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); + assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); + + // Amount should be in recipient's balance + expectedBalance = amount; + actualBalance = accountRepository.getBalance(recipient.getAddress(), Asset.QORA).getBalance(); + assertTrue("Recipient's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); } } - @Test - public void testMessageSerialization() throws SQLException, TransformationException { - // Message transactions went live block 99000 - // Some transactions to be found in block 99001/2/5/6 - } - } \ No newline at end of file diff --git a/src/transform/transaction/PaymentTransactionTransformer.java b/src/transform/transaction/PaymentTransactionTransformer.java index 1235e069..019d67ac 100644 --- a/src/transform/transaction/PaymentTransactionTransformer.java +++ b/src/transform/transaction/PaymentTransactionTransformer.java @@ -67,7 +67,9 @@ public class PaymentTransactionTransformer extends TransactionTransformer { Serialization.serializeBigDecimal(bytes, paymentTransactionData.getAmount()); Serialization.serializeBigDecimal(bytes, paymentTransactionData.getFee()); - bytes.write(paymentTransactionData.getSignature()); + + if (paymentTransactionData.getSignature() != null) + bytes.write(paymentTransactionData.getSignature()); return bytes.toByteArray(); } catch (IOException | ClassCastException e) {