forked from Qortal/qortal
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:
parent
9897981de1
commit
4a1c3821db
@ -69,6 +69,10 @@ public class BlockData {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public void setSignature(byte[] signature) {
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
public int getVersion() {
|
||||
return this.version;
|
||||
}
|
||||
|
@ -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) {
|
||||
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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());
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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>();
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
93
src/test/SerializationTests.java
Normal file
93
src/test/SerializationTests.java
Normal 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
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user