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<Account>
* 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.
This commit is contained in:
catbref 2018-06-15 17:16:44 +01:00
parent 9897981de1
commit 4a1c3821db
24 changed files with 952 additions and 198 deletions

View File

@ -69,6 +69,10 @@ public class BlockData {
return this.signature;
}
public void setSignature(byte[] signature) {
this.signature = signature;
}
public int getVersion() {
return this.version;
}

View File

@ -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.
* <p>
* This is the current confirmed balance minus amounts received in the last <code>BlockChain.BLOCK_RETARGET_INTERVAL</code> 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<Tuple2<Integer, Integer>, AT_Transaction> atTxs = db.getATTransactionMap().getATTransactions(block.getHeight(db));
* Iterator<AT_Transaction> 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) {

View File

@ -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<Integer, ValidationResult> 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<Transaction>();
}
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<Transaction>();
}
// 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.
* <p>
@ -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 {

View File

@ -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.

View File

@ -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;
}
}

View File

@ -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));
}

View File

@ -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;

View File

@ -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<Account> getRecipientAccounts() {
return new ArrayList<Account>();
}
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

View File

@ -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<Account> getRecipientAccounts() {
return new ArrayList<Account>();
}
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

View File

@ -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<Account> 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);

View File

@ -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<Account> 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());

View File

@ -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<Account> 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);

View File

@ -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<Account> getRecipientAccounts() throws DataException {
List<Account> recipients = new ArrayList<Account>();
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<PaymentData> 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);

View File

@ -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<Account> 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);

View File

@ -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<Account> getRecipientAccounts() throws DataException;
/**
* Returns whether passed account is an involved party in this transaction.
* <p>
* 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.
* <p>
* Amounts "lost", e.g. sent by sender and fees, are returned as negative values.<br>
* 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() {

View File

@ -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<Account> 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;

View File

@ -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<TransactionData> getTransactionsFromSignature(byte[] signature) throws DataException;
public void save(BlockData blockData) throws DataException;

View File

@ -101,6 +101,10 @@ public class HSQLDBBlockRepository implements BlockRepository {
}
}
public BlockData getLastBlock() throws DataException {
return fromHeight(getBlockchainHeight());
}
public List<TransactionData> getTransactionsFromSignature(byte[] signature) throws DataException {
List<TransactionData> transactions = new ArrayList<TransactionData>();

View File

@ -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.
* <p>
* {@code whereClause} is SQL "WHERE" clause containing "?" placeholders suitable for use with PreparedStatements.
* <p>
@ -179,7 +194,7 @@ public class HSQLDBRepository implements Repository {
* <p>
* {@code String manufacturer = "Lamborghini";}<br>
* {@code int maxMileage = 100_000;}<br>
* {@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

View File

@ -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<Transaction> transactions = block.getTransactions();
assertNotNull(transactions);

View File

@ -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<Transaction> 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
}
}

View File

@ -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());
}

View File

@ -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<Transaction> 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
}
}

View File

@ -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) {