ourAtStates; // Generated locally
+ protected BigDecimal ourAtFees; // Generated locally
+
protected BigDecimal cachedNextGeneratingBalance;
// Other useful constants
@@ -104,39 +113,92 @@ public class Block {
// Constructors
+ /**
+ * Constructs Block-handling object without loading transactions and AT states.
+ *
+ * Transactions and AT states are loaded on first call to getTransactions() or getATStates() respectively.
+ *
+ * @param repository
+ * @param blockData
+ * @throws DataException
+ */
public Block(Repository repository, BlockData blockData) throws DataException {
this.repository = repository;
this.blockData = blockData;
this.generator = new PublicKeyAccount(repository, blockData.getGeneratorPublicKey());
}
- // When receiving a block over network?
- public Block(Repository repository, BlockData blockData, List transactions) throws DataException {
+ /**
+ * Constructs Block-handling object using passed transaction and AT states.
+ *
+ * This constructor typically used when receiving a serialized block over the network.
+ *
+ * @param repository
+ * @param blockData
+ * @param transactions
+ * @param atStates
+ * @throws DataException
+ */
+ public Block(Repository repository, BlockData blockData, List transactions, List atStates) throws DataException {
this(repository, blockData);
this.transactions = new ArrayList();
+ BigDecimal totalFees = BigDecimal.ZERO.setScale(8);
+
// We have to sum fees too
for (TransactionData transactionData : transactions) {
this.transactions.add(Transaction.fromData(repository, transactionData));
- this.blockData.setTotalFees(this.blockData.getTotalFees().add(transactionData.getFee()));
+ totalFees = totalFees.add(transactionData.getFee());
}
+
+ this.atStates = atStates;
+ for (ATStateData atState : atStates)
+ totalFees = totalFees.add(atState.getFees());
+
+ this.blockData.setTotalFees(totalFees);
}
- // For creating a new block?
- public Block(Repository repository, int version, byte[] reference, long timestamp, BigDecimal generatingBalance, PrivateKeyAccount generator,
- byte[] atBytes, BigDecimal atFees) {
+ /**
+ * Constructs Block-handling object with basic, initial values.
+ *
+ * This constructor typically used when generating a new block.
+ *
+ * Note that CIYAM ATs will be executed and AT-Transactions prepended to this block, along with AT state data and fees.
+ *
+ * @param repository
+ * @param version
+ * @param reference
+ * @param timestamp
+ * @param generatingBalance
+ * @param generator
+ * @throws DataException
+ */
+ public Block(Repository repository, int version, byte[] reference, long timestamp, BigDecimal generatingBalance, PrivateKeyAccount generator)
+ throws DataException {
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);
+ int transactionCount = 0;
+ byte[] transactionsSignature = null;
+ Integer height = null;
+ byte[] generatorSignature = null;
+
+ this.executeATs();
+
+ int atCount = this.ourAtStates.size();
+ BigDecimal atFees = this.ourAtFees;
+ BigDecimal totalFees = atFees;
+
+ this.blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance,
+ generator.getPublicKey(), generatorSignature, atCount, atFees);
this.transactions = new ArrayList();
+ this.atStates = this.ourAtStates;
}
/** Construct a new block for use in tests */
- public Block(Repository repository, BlockData parentBlockData, PrivateKeyAccount generator, byte[] atBytes, BigDecimal atFees) throws DataException {
+ public Block(Repository repository, BlockData parentBlockData, PrivateKeyAccount generator) throws DataException {
this.repository = repository;
this.generator = generator;
@@ -155,11 +217,18 @@ public class Block {
}
long timestamp = parentBlock.calcNextBlockTimestamp(version, generatorSignature, generator);
+ int transactionCount = 0;
+ BigDecimal totalFees = BigDecimal.ZERO.setScale(8);
+ byte[] transactionsSignature = null;
+ int height = parentBlockData.getHeight() + 1;
+ int atCount = 0;
+ BigDecimal atFees = BigDecimal.ZERO.setScale(8);
- this.blockData = new BlockData(version, reference, 0, BigDecimal.ZERO.setScale(8), null, 0, timestamp, generatingBalance, generator.getPublicKey(),
- generatorSignature, atBytes, atFees);
+ this.blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance,
+ generator.getPublicKey(), generatorSignature, atCount, atFees);
this.transactions = new ArrayList();
+ this.atStates = new ArrayList();
}
// Getters/setters
@@ -192,12 +261,17 @@ public class Block {
* @return 1, 2 or 3
*/
public int getNextBlockVersion() {
+ if (this.blockData.getHeight() == null)
+ throw new IllegalStateException("Can't determine next block's version as this block has no height set");
+
if (this.blockData.getHeight() < BlockChain.getATReleaseHeight())
return 1;
else if (this.blockData.getTimestamp() < BlockChain.getPowFixReleaseTimestamp())
return 2;
- else
+ else if (this.blockData.getTimestamp() < BlockChain.getDeployATV2Timestamp())
return 3;
+ else
+ return 4;
}
/**
@@ -212,8 +286,8 @@ public class Block {
* @throws DataException
*/
public BigDecimal calcNextBlockGeneratingBalance() throws DataException {
- if (this.blockData.getHeight() == 0)
- throw new IllegalStateException("Block height is unset");
+ if (this.blockData.getHeight() == null)
+ throw new IllegalStateException("Can't calculate next block's generating balance as this block's height is unset");
// This block not at the start of an interval?
if (this.blockData.getHeight() % BlockChain.BLOCK_RETARGET_INTERVAL != 0)
@@ -346,7 +420,7 @@ public class Block {
/**
* Return block's transactions.
*
- * If the block was loaded from repository then it's possible this method will call the repository to load the transactions if they are not already loaded.
+ * If the block was loaded from repository then it's possible this method will call the repository to fetch the transactions if not done already.
*
* @return
* @throws DataException
@@ -371,6 +445,37 @@ public class Block {
return this.transactions;
}
+ /**
+ * Return block's AT states.
+ *
+ * If the block was loaded from repository then it's possible this method will call the repository to fetch the AT states if not done already.
+ *
+ * Note: AT states fetched from repository only contain summary info, not actual data like serialized state data or AT creation timestamps!
+ *
+ * @return
+ * @throws DataException
+ */
+ public List getATStates() throws DataException {
+ // Already loaded?
+ if (this.atStates != null)
+ return this.atStates;
+
+ // If loading from repository, this block must have a height
+ if (this.blockData.getHeight() == null)
+ throw new IllegalStateException("Can't fetch block's AT states from repository without a block height");
+
+ // Allocate cache for results
+ List atStateData = this.repository.getATRepository().getBlockATStatesFromHeight(this.blockData.getHeight());
+
+ // The number of AT states fetched from repository should correspond with Block's atCount
+ if (atStateData.size() != this.blockData.getATCount())
+ throw new IllegalStateException("Block's AT states from repository do not match block's AT count");
+
+ this.atStates = atStateData;
+
+ return this.atStates;
+ }
+
// Navigation
/**
@@ -531,8 +636,6 @@ public class Block {
* @throws DataException
*/
public ValidationResult isValid() throws DataException {
- // TODO
-
// Check parent block exists
if (this.blockData.getReference() == null)
return ValidationResult.REFERENCE_MISSING;
@@ -543,6 +646,10 @@ public class Block {
Block parentBlock = new Block(this.repository, parentBlockData);
+ // Check parent doesn't already have a child block
+ if (parentBlock.getChild() != null)
+ return ValidationResult.PARENT_HAS_EXISTING_CHILD;
+
// Check timestamp is newer than parent timestamp
if (this.blockData.getTimestamp() <= parentBlockData.getTimestamp())
return ValidationResult.TIMESTAMP_OLDER_THAN_PARENT;
@@ -558,7 +665,7 @@ public class Block {
// Check block version
if (this.blockData.getVersion() != parentBlock.getNextBlockVersion())
return ValidationResult.VERSION_INCORRECT;
- if (this.blockData.getVersion() < 2 && (this.blockData.getAtBytes() != null || this.blockData.getAtFees() != null))
+ if (this.blockData.getVersion() < 2 && this.blockData.getATCount() != 0)
return ValidationResult.FEATURE_NOT_YET_RELEASED;
// Check generating balance
@@ -583,16 +690,34 @@ public class Block {
if (hashValue.compareTo(lowerTarget) < 0)
return ValidationResult.GENERATOR_NOT_ACCEPTED;
- // Process CIYAM ATs, prepending AT-Transactions to block then compare post-execution checksums
- // XXX We should pre-calculate, and cache, next block's AT-transactions after processing each block to save repeated work
- if (this.blockData.getAtBytes() != null && this.blockData.getAtBytes().length > 0) {
- // TODO
- // try {
- // AT_Block atBlock = AT_Controller.validateATs(this.getBlockATs(), BlockChain.getHeight() + 1);
- // this.atFees = atBlock.getTotalFees();
- // } catch (NoSuchAlgorithmException | AT_Exception e) {
- // return false;
- // }
+ // CIYAM ATs
+ if (this.blockData.getATCount() != 0) {
+ // Locally generated AT states should be valid so no need to re-execute them
+ if (this.ourAtStates != this.getATStates()) {
+ // Otherwise, check locally generated AT states against ones received from elsewhere?
+ this.executeATs();
+
+ if (this.ourAtStates.size() != this.blockData.getATCount())
+ return ValidationResult.AT_STATES_MISMATCH;
+
+ if (this.ourAtFees.compareTo(this.blockData.getATFees()) != 0)
+ return ValidationResult.AT_STATES_MISMATCH;
+
+ // Note: this.atStates fully loaded thanks to this.getATStates() call above
+ for (int s = 0; s < this.atStates.size(); ++s) {
+ ATStateData ourAtState = this.ourAtStates.get(s);
+ ATStateData theirAtState = this.atStates.get(s);
+
+ if (!ourAtState.getATAddress().equals(theirAtState.getATAddress()))
+ return ValidationResult.AT_STATES_MISMATCH;
+
+ if (!ourAtState.getStateHash().equals(theirAtState.getStateHash()))
+ return ValidationResult.AT_STATES_MISMATCH;
+
+ if (ourAtState.getFees().compareTo(theirAtState.getFees()) != 0)
+ return ValidationResult.AT_STATES_MISMATCH;
+ }
+ }
}
// Check transactions
@@ -612,7 +737,7 @@ public class Block {
Transaction.ValidationResult validationResult = transaction.isValid();
if (validationResult != Transaction.ValidationResult.OK) {
LOGGER.error("Error during transaction validation, tx " + Base58.encode(transaction.getTransactionData().getSignature()) + ": "
- + validationResult.value);
+ + validationResult.name());
return ValidationResult.TRANSACTION_INVALID;
}
@@ -643,8 +768,47 @@ public class Block {
return ValidationResult.OK;
}
+ /**
+ * Execute CIYAM ATs for this block.
+ *
+ * This needs to be done locally for all blocks, regardless of origin.
+ * This method is called by isValid.
+ *
+ * After calling, AT-generated transactions are prepended to the block's transactions and AT state data is generated.
+ *
+ * This method is not needed if fetching an existing block from the repository.
+ *
+ * Updates this.ourAtStates and this.ourAtFees.
+ *
+ * @see #isValid()
+ *
+ * @throws DataException
+ *
+ */
+ public void executeATs() throws DataException {
+ // We're expecting a lack of AT state data at this point.
+ if (this.ourAtStates != null)
+ throw new IllegalStateException("Attempted to execute ATs when block's local AT state data already exists");
+
+ // For old v1 CIYAM ATs we blindly accept them
+ if (this.blockData.getVersion() < 4) {
+ this.ourAtStates = this.atStates;
+ this.ourAtFees = this.blockData.getATFees();
+ return;
+ }
+
+ // Find all executable ATs, ordered by earliest creation date first
+
+ // Run each AT, appends AT-Transactions and corresponding AT states, to our lists
+
+ // Finally prepend our entire AT-Transactions/states to block's transactions/states, adjust fees, etc.
+
+ // Note: store locally-calculated AT states separately to this.atStates so we can compare them in isValid()
+ }
+
public void process() throws DataException {
// Process transactions (we'll link them to this block after saving the block itself)
+ // AT-generated transactions are already added to our transactions so no special handling is needed here.
List transactions = this.getTransactions();
for (Transaction transaction : transactions)
transaction.process();
@@ -654,6 +818,17 @@ public class Block {
if (blockFee.compareTo(BigDecimal.ZERO) > 0)
this.generator.setConfirmedBalance(Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).add(blockFee));
+ // Process AT fees and save AT states into repository
+ ATRepository atRepository = this.repository.getATRepository();
+ for (ATStateData atState : this.getATStates()) {
+ Account atAccount = new Account(this.repository, atState.getATAddress());
+
+ // Subtract AT-generated fees from AT accounts
+ atAccount.setConfirmedBalance(Asset.QORA, atAccount.getConfirmedBalance(Asset.QORA).subtract(atState.getFees()));
+
+ atRepository.save(atState);
+ }
+
// Link block into blockchain by fetching signature of highest block and setting that as our reference
int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight();
BlockData latestBlockData = this.repository.getBlockRepository().fromHeight(blockchainHeight);
@@ -675,12 +850,8 @@ public class Block {
}
public void orphan() throws DataException {
- // TODO
-
- // Orphan block's CIYAM ATs
- orphanAutomatedTransactions();
-
// Orphan transactions in reverse order, and unlink them from this block
+ // AT-generated transactions are already added to our transactions so no special handling is needed here.
List transactions = this.getTransactions();
for (int sequence = transactions.size() - 1; sequence >= 0; --sequence) {
Transaction transaction = transactions.get(sequence);
@@ -696,25 +867,19 @@ public class Block {
if (blockFee.compareTo(BigDecimal.ZERO) > 0)
this.generator.setConfirmedBalance(Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).subtract(blockFee));
+ // Return AT fees and delete AT states from repository
+ ATRepository atRepository = this.repository.getATRepository();
+ for (ATStateData atState : this.getATStates()) {
+ Account atAccount = new Account(this.repository, atState.getATAddress());
+
+ // Return AT-generated fees to AT accounts
+ atAccount.setConfirmedBalance(Asset.QORA, atAccount.getConfirmedBalance(Asset.QORA).add(atState.getFees()));
+ }
+ // Delete ATStateData for this height
+ atRepository.deleteATStates(this.blockData.getHeight());
+
// Delete block from blockchain
this.repository.getBlockRepository().delete(this.blockData);
}
- public void orphanAutomatedTransactions() throws DataException {
- // TODO - CIYAM AT support
- /*
- * LinkedHashMap< Tuple2 , AT_Transaction > atTxs = DBSet.getInstance().getATTransactionMap().getATTransactions(this.getHeight(db));
- *
- * Iterator iter = atTxs.values().iterator();
- *
- * while ( iter.hasNext() ) { AT_Transaction key = iter.next(); Long amount = key.getAmount(); if (key.getRecipientId() != null &&
- * !Arrays.equals(key.getRecipientId(), new byte[ AT_Constants.AT_ID_SIZE ]) && !key.getRecipient().equalsIgnoreCase("1") ) { Account recipient = new
- * Account( key.getRecipient() ); recipient.setConfirmedBalance( recipient.getConfirmedBalance( db ).subtract( BigDecimal.valueOf( amount, 8 ) ) , db );
- * if ( Arrays.equals(recipient.getLastReference(db),new byte[64])) { recipient.removeReference(db); } } Account sender = new Account( key.getSender()
- * ); sender.setConfirmedBalance( sender.getConfirmedBalance( db ).add( BigDecimal.valueOf( amount, 8 ) ) , db );
- *
- * }
- */
- }
-
}
diff --git a/src/qora/block/GenesisBlock.java b/src/qora/block/GenesisBlock.java
index 9daab7b6..0d309dd4 100644
--- a/src/qora/block/GenesisBlock.java
+++ b/src/qora/block/GenesisBlock.java
@@ -34,7 +34,7 @@ public class GenesisBlock extends Block {
public GenesisBlock(Repository repository) throws DataException {
super(repository, new BlockData(GENESIS_BLOCK_VERSION, GENESIS_REFERENCE, 0, BigDecimal.ZERO.setScale(8), GENESIS_TRANSACTIONS_SIGNATURE, 1,
- Settings.getInstance().getGenesisTimestamp(), GENESIS_GENERATING_BALANCE, GENESIS_GENERATOR_PUBLIC_KEY, GENESIS_GENERATOR_SIGNATURE, null, null));
+ Settings.getInstance().getGenesisTimestamp(), GENESIS_GENERATING_BALANCE, GENESIS_GENERATOR_PUBLIC_KEY, GENESIS_GENERATOR_SIGNATURE, 0, BigDecimal.ZERO.setScale(8)));
this.transactions = new ArrayList();
diff --git a/src/qora/payment/Payment.java b/src/qora/payment/Payment.java
index fbf0899f..66e2175d 100644
--- a/src/qora/payment/Payment.java
+++ b/src/qora/payment/Payment.java
@@ -46,31 +46,31 @@ public class Payment {
amountsByAssetId.put(Asset.QORA, fee);
// Check payments, and calculate amount total by assetId
- if (payments != null)
- for (PaymentData paymentData : payments) {
- // Check amount is positive
- if (paymentData.getAmount().compareTo(BigDecimal.ZERO) < 0)
- return ValidationResult.NEGATIVE_AMOUNT;
+ for (PaymentData paymentData : payments) {
+ // Check amount is zero or positive
+ if (paymentData.getAmount().compareTo(BigDecimal.ZERO) < 0)
+ return ValidationResult.NEGATIVE_AMOUNT;
- // Optional zero-amount check
- if (!isZeroAmountValid && paymentData.getAmount().compareTo(BigDecimal.ZERO) <= 0)
- return ValidationResult.NEGATIVE_AMOUNT;
+ // Optional zero-amount check
+ if (!isZeroAmountValid && paymentData.getAmount().compareTo(BigDecimal.ZERO) <= 0)
+ return ValidationResult.NEGATIVE_AMOUNT;
- // Check recipient address is valid
- if (!Crypto.isValidAddress(paymentData.getRecipient()))
- return ValidationResult.INVALID_ADDRESS;
+ // Check recipient address is valid
+ if (!Crypto.isValidAddress(paymentData.getRecipient()))
+ return ValidationResult.INVALID_ADDRESS;
- AssetData assetData = assetRepository.fromAssetId(paymentData.getAssetId());
- // Check asset even exists
- if (assetData == null)
- return ValidationResult.ASSET_DOES_NOT_EXIST;
+ 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;
+ // Check asset amount is integer if asset is not divisible
+ if (!assetData.getIsDivisible() && paymentData.getAmount().stripTrailingZeros().scale() > 0)
+ return ValidationResult.INVALID_AMOUNT;
- amountsByAssetId.compute(paymentData.getAssetId(), (assetId, amount) -> amount == null ? amount : amount.add(paymentData.getAmount()));
- }
+ // Set or add amount into amounts-by-asset map
+ amountsByAssetId.compute(paymentData.getAssetId(), (assetId, amount) -> amount == null ? amount : amount.add(paymentData.getAmount()));
+ }
// Check sender has enough of each asset
Account sender = new PublicKeyAccount(this.repository, senderPublicKey);
@@ -87,7 +87,7 @@ public class Payment {
// Single payment forms
public ValidationResult isValid(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee, boolean isZeroAmountValid) throws DataException {
- return isValid(senderPublicKey, Collections.singletonList(paymentData), fee);
+ return isValid(senderPublicKey, Collections.singletonList(paymentData), fee, isZeroAmountValid);
}
public ValidationResult isValid(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee) throws DataException {
@@ -105,22 +105,22 @@ public class Payment {
sender.setLastReference(signature);
// Process all payments
- if (payments != null)
- for (PaymentData paymentData : payments) {
- Account recipient = new Account(this.repository, paymentData.getRecipient());
- long assetId = paymentData.getAssetId();
- BigDecimal amount = paymentData.getAmount();
+ for (PaymentData paymentData : payments) {
+ Account recipient = new Account(this.repository, paymentData.getRecipient());
- // Update sender's balance due to amount
- sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).subtract(amount));
+ long assetId = paymentData.getAssetId();
+ BigDecimal amount = paymentData.getAmount();
- // Update recipient's balance
- recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).add(amount));
+ // Update sender's balance due to amount
+ sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).subtract(amount));
- // For QORA amounts only: if recipient has no reference yet, then this is their starting reference
- if ((alwaysInitializeRecipientReference || assetId == Asset.QORA) && recipient.getLastReference() == null)
- recipient.setLastReference(signature);
- }
+ // Update recipient's balance
+ recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).add(amount));
+
+ // For QORA amounts only: if recipient has no reference yet, then this is their starting reference
+ if ((alwaysInitializeRecipientReference || assetId == Asset.QORA) && recipient.getLastReference() == null)
+ recipient.setLastReference(signature);
+ }
}
public void process(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee, byte[] signature, boolean alwaysInitializeRecipientReference)
@@ -139,25 +139,24 @@ public class Payment {
sender.setLastReference(reference);
// Orphan all payments
- if (payments != null)
- for (PaymentData paymentData : payments) {
- Account recipient = new Account(this.repository, paymentData.getRecipient());
- long assetId = paymentData.getAssetId();
- BigDecimal amount = paymentData.getAmount();
+ for (PaymentData paymentData : payments) {
+ Account recipient = new Account(this.repository, paymentData.getRecipient());
+ long assetId = paymentData.getAssetId();
+ BigDecimal amount = paymentData.getAmount();
- // Update sender's balance due to amount
- sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).add(amount));
+ // Update sender's balance due to amount
+ sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).add(amount));
- // Update recipient's balance
- recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).subtract(amount));
+ // Update recipient's balance
+ recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).subtract(amount));
- /*
- * For QORA amounts only: If recipient's last reference is this transaction's signature, then they can't have made any transactions of their own
- * (which would have changed their last reference) thus this is their first reference so remove it.
- */
- if ((alwaysUninitializeRecipientReference || assetId == Asset.QORA) && Arrays.equals(recipient.getLastReference(), signature))
- recipient.setLastReference(null);
- }
+ /*
+ * For QORA amounts only: If recipient's last reference is this transaction's signature, then they can't have made any transactions of their own
+ * (which would have changed their last reference) thus this is their first reference so remove it.
+ */
+ if ((alwaysUninitializeRecipientReference || assetId == Asset.QORA) && Arrays.equals(recipient.getLastReference(), signature))
+ recipient.setLastReference(null);
+ }
}
public void orphan(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee, byte[] signature, byte[] reference,
diff --git a/src/qora/transaction/ATTransaction.java b/src/qora/transaction/ATTransaction.java
new file mode 100644
index 00000000..790ba578
--- /dev/null
+++ b/src/qora/transaction/ATTransaction.java
@@ -0,0 +1,179 @@
+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.ATTransactionData;
+import data.transaction.TransactionData;
+import qora.account.Account;
+import qora.assets.Asset;
+import qora.crypto.Crypto;
+import repository.DataException;
+import repository.Repository;
+
+public class ATTransaction extends Transaction {
+
+ // Properties
+ private ATTransactionData atTransactionData;
+
+ // Other useful constants
+ public static final int MAX_DATA_SIZE = 256;
+
+ // Constructors
+
+ public ATTransaction(Repository repository, TransactionData transactionData) {
+ super(repository, transactionData);
+
+ this.atTransactionData = (ATTransactionData) this.transactionData;
+ }
+
+ // More information
+
+ @Override
+ public List getRecipientAccounts() throws DataException {
+ return Collections.singletonList(new Account(this.repository, this.atTransactionData.getRecipient()));
+ }
+
+ @Override
+ public boolean isInvolved(Account account) throws DataException {
+ String address = account.getAddress();
+
+ if (address.equals(this.atTransactionData.getATAddress()))
+ return true;
+
+ if (address.equals(this.atTransactionData.getRecipient()))
+ return true;
+
+ return false;
+ }
+
+ @Override
+ public BigDecimal getAmount(Account account) throws DataException {
+ String address = account.getAddress();
+ BigDecimal amount = BigDecimal.ZERO.setScale(8);
+ String atAddress = this.atTransactionData.getATAddress();
+
+ if (address.equals(atAddress)) {
+ amount = amount.subtract(this.atTransactionData.getFee());
+
+ if (this.atTransactionData.getAmount() != null && this.atTransactionData.getAssetId() == Asset.QORA)
+ amount = amount.subtract(this.atTransactionData.getAmount());
+ }
+
+ if (address.equals(this.atTransactionData.getRecipient()) && this.atTransactionData.getAmount() != null)
+ amount = amount.add(this.atTransactionData.getAmount());
+
+ return amount;
+ }
+
+ // Navigation
+
+ public Account getATAccount() throws DataException {
+ return new Account(this.repository, this.atTransactionData.getATAddress());
+ }
+
+ public Account getRecipient() throws DataException {
+ return new Account(this.repository, this.atTransactionData.getRecipient());
+ }
+
+ // Processing
+
+ @Override
+ public ValidationResult isValid() throws DataException {
+ // Check reference is correct
+ Account atAccount = getATAccount();
+ if (!Arrays.equals(atAccount.getLastReference(), atTransactionData.getReference()))
+ return ValidationResult.INVALID_REFERENCE;
+
+ if (this.atTransactionData.getMessage().length > MAX_DATA_SIZE)
+ return ValidationResult.INVALID_DATA_LENGTH;
+
+ BigDecimal amount = this.atTransactionData.getAmount();
+
+ // If we have no payment then we're done
+ if (amount == null)
+ return ValidationResult.OK;
+
+ // Check amount is zero or positive
+ if (amount.compareTo(BigDecimal.ZERO) < 0)
+ return ValidationResult.NEGATIVE_AMOUNT;
+
+ // Check recipient address is valid
+ if (!Crypto.isValidAddress(this.atTransactionData.getRecipient()))
+ return ValidationResult.INVALID_ADDRESS;
+
+ long assetId = this.atTransactionData.getAssetId();
+ AssetData assetData = this.repository.getAssetRepository().fromAssetId(assetId);
+ // 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() && amount.stripTrailingZeros().scale() > 0)
+ return ValidationResult.INVALID_AMOUNT;
+
+ Account sender = getATAccount();
+ // Check sender has enough of asset
+ if (sender.getConfirmedBalance(assetId).compareTo(amount) < 0)
+ return ValidationResult.NO_BALANCE;
+
+ return ValidationResult.OK;
+ }
+
+ @Override
+ public void process() throws DataException {
+ // Save this transaction itself
+ this.repository.getTransactionRepository().save(this.transactionData);
+
+ if (this.atTransactionData.getAmount() != null) {
+ Account sender = getATAccount();
+ Account recipient = getRecipient();
+
+ long assetId = this.atTransactionData.getAssetId();
+ BigDecimal amount = this.atTransactionData.getAmount();
+
+ // Update sender's balance due to amount
+ sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).subtract(amount));
+
+ // Update recipient's balance
+ recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).add(amount));
+
+ // For QORA amounts only: if recipient has no reference yet, then this is their starting reference
+ if (assetId == Asset.QORA && recipient.getLastReference() == null)
+ // In Qora1 last reference was set to 64-bytes of zero
+ // In Qora2 we use AT-Transction's signature, which makes more sense
+ recipient.setLastReference(this.atTransactionData.getSignature());
+ }
+ }
+
+ @Override
+ public void orphan() throws DataException {
+ // Delete this transaction
+ this.repository.getTransactionRepository().delete(this.transactionData);
+
+ if (this.atTransactionData.getAmount() != null) {
+ Account sender = getATAccount();
+ Account recipient = getRecipient();
+
+ long assetId = this.atTransactionData.getAssetId();
+ BigDecimal amount = this.atTransactionData.getAmount();
+
+ // Update sender's balance due to amount
+ sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).add(amount));
+
+ // Update recipient's balance
+ recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).subtract(amount));
+
+ /*
+ * For QORA amounts only: If recipient's last reference is this transaction's signature, then they can't have made any transactions of their own
+ * (which would have changed their last reference) thus this is their first reference so remove it.
+ */
+ if (assetId == Asset.QORA && Arrays.equals(recipient.getLastReference(), this.atTransactionData.getSignature()))
+ recipient.setLastReference(null);
+ }
+ }
+
+}
diff --git a/src/qora/transaction/ArbitraryTransaction.java b/src/qora/transaction/ArbitraryTransaction.java
index 92d7f07b..ef82afaa 100644
--- a/src/qora/transaction/ArbitraryTransaction.java
+++ b/src/qora/transaction/ArbitraryTransaction.java
@@ -149,9 +149,9 @@ public class ArbitraryTransaction extends Transaction {
// Make sure directory structure exists
try {
Files.createDirectories(dataPath.getParent());
- } catch (IOException e1) {
+ } catch (IOException e) {
// TODO Auto-generated catch block
- e1.printStackTrace();
+ e.printStackTrace();
}
// Output actual transaction data
diff --git a/src/qora/transaction/DeployATTransaction.java b/src/qora/transaction/DeployATTransaction.java
index 45d77600..e80412bf 100644
--- a/src/qora/transaction/DeployATTransaction.java
+++ b/src/qora/transaction/DeployATTransaction.java
@@ -75,6 +75,13 @@ public class DeployATTransaction extends Transaction {
return amount;
}
+ /** Returns AT version from the header bytes */
+ private short getVersion() {
+ byte[] creationBytes = deployATTransactionData.getCreationBytes();
+ short version = (short) (creationBytes[0] | (creationBytes[1] << 8)); // Little-endian
+ return version;
+ }
+
/** Make sure deployATTransactionData has an ATAddress */
private void ensureATAddress() throws DataException {
if (this.deployATTransactionData.getATAddress() != null)
@@ -82,7 +89,7 @@ public class DeployATTransaction extends Transaction {
int blockHeight = this.getHeight();
if (blockHeight == 0)
- blockHeight = this.repository.getBlockRepository().getBlockchainHeight();
+ blockHeight = this.repository.getBlockRepository().getBlockchainHeight() + 1;
try {
byte[] name = this.deployATTransactionData.getName().getBytes("UTF-8");
@@ -163,11 +170,8 @@ public class DeployATTransaction extends Transaction {
if (creator.getConfirmedBalance(Asset.QORA).compareTo(minimumBalance) < 0)
return ValidationResult.NO_BALANCE;
- // Check creation bytes are valid (for v3+)
- byte[] creationBytes = deployATTransactionData.getCreationBytes();
- short version = (short) (creationBytes[0] | (creationBytes[1] << 8)); // Little-endian
-
- if (version >= 3) {
+ // Check creation bytes are valid (for v2+)
+ if (this.getVersion() >= 2) {
// Do actual validation
} else {
// Skip validation for old, dead ATs
@@ -194,6 +198,13 @@ public class DeployATTransaction extends Transaction {
// Update creator's reference
creator.setLastReference(deployATTransactionData.getSignature());
+
+ // Update AT's reference, which also creates AT account
+ Account atAccount = this.getATAccount();
+ atAccount.setLastReference(deployATTransactionData.getSignature());
+
+ // Update AT's balance
+ atAccount.setConfirmedBalance(Asset.QORA, deployATTransactionData.getAmount());
}
@Override
@@ -212,6 +223,9 @@ public class DeployATTransaction extends Transaction {
// Update creator's reference
creator.setLastReference(deployATTransactionData.getReference());
+
+ // Delete AT's account
+ this.repository.getAccountRepository().delete(this.deployATTransactionData.getATAddress());
}
}
diff --git a/src/qora/transaction/GenesisTransaction.java b/src/qora/transaction/GenesisTransaction.java
index d3ae1bc1..3a1a83e9 100644
--- a/src/qora/transaction/GenesisTransaction.java
+++ b/src/qora/transaction/GenesisTransaction.java
@@ -133,12 +133,13 @@ public class GenesisTransaction extends Transaction {
// Save this transaction itself
this.repository.getTransactionRepository().save(this.transactionData);
- // Update recipient's balance
Account recipient = new Account(repository, genesisTransactionData.getRecipient());
- recipient.setConfirmedBalance(Asset.QORA, genesisTransactionData.getAmount());
- // Set recipient's starting reference
+ // Set recipient's starting reference (also creates account)
recipient.setLastReference(genesisTransactionData.getSignature());
+
+ // Update recipient's balance
+ recipient.setConfirmedBalance(Asset.QORA, genesisTransactionData.getAmount());
}
@Override
@@ -146,12 +147,8 @@ public class GenesisTransaction extends Transaction {
// Delete this transaction
this.repository.getTransactionRepository().delete(this.transactionData);
- // Delete recipient's balance
- Account recipient = new Account(repository, genesisTransactionData.getRecipient());
- recipient.deleteBalance(Asset.QORA);
-
- // Delete recipient's last reference
- recipient.setLastReference(null);
+ // Delete recipient's account (and balance)
+ this.repository.getAccountRepository().delete(genesisTransactionData.getRecipient());
}
}
diff --git a/src/qora/transaction/MessageTransaction.java b/src/qora/transaction/MessageTransaction.java
index b01dcd3b..abc93d0a 100644
--- a/src/qora/transaction/MessageTransaction.java
+++ b/src/qora/transaction/MessageTransaction.java
@@ -85,7 +85,7 @@ public class MessageTransaction extends Transaction {
// Processing
private PaymentData getPaymentData() {
- return new PaymentData(messageTransactionData.getRecipient(), Asset.QORA, messageTransactionData.getAmount());
+ return new PaymentData(messageTransactionData.getRecipient(), messageTransactionData.getAssetId(), messageTransactionData.getAmount());
}
@Override
diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java
index aaba8836..f532e3bf 100644
--- a/src/qora/transaction/Transaction.java
+++ b/src/qora/transaction/Transaction.java
@@ -193,11 +193,14 @@ public abstract class Transaction {
case MULTIPAYMENT:
return new MultiPaymentTransaction(repository, transactionData);
+ case DEPLOY_AT:
+ return new DeployATTransaction(repository, transactionData);
+
case MESSAGE:
return new MessageTransaction(repository, transactionData);
- case DEPLOY_AT:
- return new DeployATTransaction(repository, transactionData);
+ case AT:
+ return new ATTransaction(repository, transactionData);
default:
throw new IllegalStateException("Unsupported transaction type [" + transactionData.getType().value + "] during fetch from repository");
diff --git a/src/repository/ATRepository.java b/src/repository/ATRepository.java
index fef0c76e..70e494ef 100644
--- a/src/repository/ATRepository.java
+++ b/src/repository/ATRepository.java
@@ -1,5 +1,7 @@
package repository;
+import java.util.List;
+
import data.at.ATData;
import data.at.ATStateData;
@@ -17,8 +19,14 @@ public interface ATRepository {
public ATStateData getATState(String atAddress, int height) throws DataException;
+ public List getBlockATStatesFromHeight(int height) throws DataException;
+
public void save(ATStateData atStateData) throws DataException;
+ /** Delete AT's state data at this height */
public void delete(String atAddress, int height) throws DataException;
+ /** Delete state data for all ATs at this height */
+ public void deleteATStates(int height) throws DataException;
+
}
diff --git a/src/repository/AccountRepository.java b/src/repository/AccountRepository.java
index d740fe80..d4356a60 100644
--- a/src/repository/AccountRepository.java
+++ b/src/repository/AccountRepository.java
@@ -7,10 +7,14 @@ public interface AccountRepository {
// General account
+ public void create(String address) throws DataException;
+
public AccountData getAccount(String address) throws DataException;
public void save(AccountData accountData) throws DataException;
+ public void delete(String address) throws DataException;
+
// Account balances
public AccountBalanceData getBalance(String address, long assetId) throws DataException;
diff --git a/src/repository/hsqldb/HSQLDBATRepository.java b/src/repository/hsqldb/HSQLDBATRepository.java
index 5c2cc88d..a634fe98 100644
--- a/src/repository/hsqldb/HSQLDBATRepository.java
+++ b/src/repository/hsqldb/HSQLDBATRepository.java
@@ -3,6 +3,10 @@ package repository.hsqldb;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
import data.at.ATData;
import data.at.ATStateData;
@@ -21,30 +25,32 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public ATData fromATAddress(String atAddress) throws DataException {
- try (ResultSet resultSet = this.repository
- .checkedExecute("SELECT owner, asset_name, description, quantity, is_divisible, reference FROM Assets WHERE AT_address = ?", atAddress)) {
+ try (ResultSet resultSet = this.repository.checkedExecute(
+ "SELECT creator, creation, version, code_bytes, is_sleeping, sleep_until_height, is_finished, had_fatal_error, is_frozen, frozen_balance FROM ATs WHERE AT_address = ?",
+ atAddress)) {
if (resultSet == null)
return null;
- int version = resultSet.getInt(1);
- byte[] codeBytes = resultSet.getBytes(2); // Actually BLOB
- boolean isSleeping = resultSet.getBoolean(3);
+ String creator = resultSet.getString(1);
+ long creation = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
+ int version = resultSet.getInt(3);
+ byte[] codeBytes = resultSet.getBytes(4); // Actually BLOB
+ boolean isSleeping = resultSet.getBoolean(5);
- Integer sleepUntilHeight = resultSet.getInt(4);
+ Integer sleepUntilHeight = resultSet.getInt(6);
if (resultSet.wasNull())
sleepUntilHeight = null;
- boolean isFinished = resultSet.getBoolean(5);
- boolean hadFatalError = resultSet.getBoolean(6);
- boolean isFrozen = resultSet.getBoolean(7);
+ boolean isFinished = resultSet.getBoolean(7);
+ boolean hadFatalError = resultSet.getBoolean(8);
+ boolean isFrozen = resultSet.getBoolean(9);
- BigDecimal frozenBalance = resultSet.getBigDecimal(8);
+ BigDecimal frozenBalance = resultSet.getBigDecimal(10);
if (resultSet.wasNull())
frozenBalance = null;
- byte[] deploySignature = resultSet.getBytes(9);
-
- return new ATData(atAddress, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance, deploySignature);
+ return new ATData(atAddress, creator, creation, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen,
+ frozenBalance);
} catch (SQLException e) {
throw new DataException("Unable to fetch AT from repository", e);
}
@@ -54,10 +60,10 @@ public class HSQLDBATRepository implements ATRepository {
public void save(ATData atData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("ATs");
- saveHelper.bind("AT_address", atData.getATAddress()).bind("version", atData.getVersion()).bind("code_bytes", atData.getCodeBytes())
- .bind("is_sleeping", atData.getIsSleeping()).bind("sleep_until_height", atData.getSleepUntilHeight())
- .bind("is_finished", atData.getIsFinished()).bind("had_fatal_error", atData.getHadFatalError()).bind("is_frozen", atData.getIsFrozen())
- .bind("frozen_balance", atData.getFrozenBalance()).bind("deploy_signature", atData.getDeploySignature());
+ saveHelper.bind("AT_address", atData.getATAddress()).bind("creator", atData.getCreator()).bind("creation", new Timestamp(atData.getCreation()))
+ .bind("version", atData.getVersion()).bind("code_bytes", atData.getCodeBytes()).bind("is_sleeping", atData.getIsSleeping())
+ .bind("sleep_until_height", atData.getSleepUntilHeight()).bind("is_finished", atData.getIsFinished())
+ .bind("had_fatal_error", atData.getHadFatalError()).bind("is_frozen", atData.getIsFrozen()).bind("frozen_balance", atData.getFrozenBalance());
try {
saveHelper.execute(this.repository);
@@ -69,7 +75,7 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public void delete(String atAddress) throws DataException {
try {
- this.repository.delete("ATs", "atAddress = ?", atAddress);
+ this.repository.delete("ATs", "AT_address = ?", atAddress);
// AT States also deleted via ON DELETE CASCADE
} catch (SQLException e) {
throw new DataException("Unable to delete AT from repository", e);
@@ -80,28 +86,63 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public ATStateData getATState(String atAddress, int height) throws DataException {
- try (ResultSet resultSet = this.repository.checkedExecute("SELECT state_data FROM ATStates WHERE AT_address = ? AND height = ?", atAddress, height)) {
+ try (ResultSet resultSet = this.repository
+ .checkedExecute("SELECT creation, state_data, state_hash, fees FROM ATStates WHERE AT_address = ? AND height = ?", atAddress, height)) {
if (resultSet == null)
return null;
- byte[] stateData = resultSet.getBytes(1); // Actually BLOB
+ long creation = resultSet.getTimestamp(1, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
+ byte[] stateData = resultSet.getBytes(2); // Actually BLOB
+ byte[] stateHash = resultSet.getBytes(3);
+ BigDecimal fees = resultSet.getBigDecimal(4);
- return new ATStateData(atAddress, height, stateData);
+ return new ATStateData(atAddress, height, creation, stateData, stateHash, fees);
} catch (SQLException e) {
- throw new DataException("Unable to fetch AT State from repository", e);
+ throw new DataException("Unable to fetch AT state from repository", e);
}
}
+ @Override
+ public List getBlockATStatesFromHeight(int height) throws DataException {
+ List atStates = new ArrayList();
+
+ try (ResultSet resultSet = this.repository.checkedExecute("SELECT AT_address, state_hash, fees FROM ATStates WHERE height = ? ORDER BY creation ASC",
+ height)) {
+ if (resultSet == null)
+ return atStates; // No atStates in this block
+
+ // NB: do-while loop because .checkedExecute() implicitly calls ResultSet.next() for us
+ do {
+ String atAddress = resultSet.getString(1);
+ byte[] stateHash = resultSet.getBytes(2);
+ BigDecimal fees = resultSet.getBigDecimal(3);
+
+ ATStateData atStateData = new ATStateData(atAddress, height, stateHash, fees);
+ atStates.add(atStateData);
+ } while (resultSet.next());
+ } catch (SQLException e) {
+ throw new DataException("Unable to fetch AT states for this height from repository", e);
+ }
+
+ return atStates;
+ }
+
@Override
public void save(ATStateData atStateData) throws DataException {
+ // We shouldn't ever save partial ATStateData
+ if (atStateData.getCreation() == null || atStateData.getStateHash() == null || atStateData.getHeight() == null)
+ throw new IllegalArgumentException("Refusing to save partial AT state into repository!");
+
HSQLDBSaver saveHelper = new HSQLDBSaver("ATStates");
- saveHelper.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight()).bind("state_data", atStateData.getStateData());
+ saveHelper.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
+ .bind("creation", new Timestamp(atStateData.getCreation())).bind("state_data", atStateData.getStateData())
+ .bind("state_hash", atStateData.getStateHash()).bind("fees", atStateData.getFees());
try {
saveHelper.execute(this.repository);
} catch (SQLException e) {
- throw new DataException("Unable to save AT State into repository", e);
+ throw new DataException("Unable to save AT state into repository", e);
}
}
@@ -110,7 +151,16 @@ public class HSQLDBATRepository implements ATRepository {
try {
this.repository.delete("ATStates", "AT_address = ? AND height = ?", atAddress, height);
} catch (SQLException e) {
- throw new DataException("Unable to delete AT State from repository", e);
+ throw new DataException("Unable to delete AT state from repository", e);
+ }
+ }
+
+ @Override
+ public void deleteATStates(int height) throws DataException {
+ try {
+ this.repository.delete("ATStates", "height = ?", height);
+ } catch (SQLException e) {
+ throw new DataException("Unable to delete AT states from repository", e);
}
}
diff --git a/src/repository/hsqldb/HSQLDBAccountRepository.java b/src/repository/hsqldb/HSQLDBAccountRepository.java
index 1e2c669e..5c716863 100644
--- a/src/repository/hsqldb/HSQLDBAccountRepository.java
+++ b/src/repository/hsqldb/HSQLDBAccountRepository.java
@@ -17,6 +17,21 @@ public class HSQLDBAccountRepository implements AccountRepository {
this.repository = repository;
}
+ // General account
+
+ @Override
+ public void create(String address) throws DataException {
+ HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts");
+
+ saveHelper.bind("account", address);
+
+ try {
+ saveHelper.execute(this.repository);
+ } catch (SQLException e) {
+ throw new DataException("Unable to create account in repository", e);
+ }
+ }
+
@Override
public AccountData getAccount(String address) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT reference FROM Accounts WHERE account = ?", address)) {
@@ -32,6 +47,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
@Override
public void save(AccountData accountData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts");
+
saveHelper.bind("account", accountData.getAddress()).bind("reference", accountData.getReference());
try {
@@ -41,6 +57,19 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
+ @Override
+ public void delete(String address) throws DataException {
+ // NOTE: Account balances are deleted automatically by the database thanks to "ON DELETE CASCADE" in AccountBalances' FOREIGN KEY
+ // definition.
+ try {
+ this.repository.delete("Accounts", "account = ?", address);
+ } catch (SQLException e) {
+ throw new DataException("Unable to delete account from repository", e);
+ }
+ }
+
+ // Account balances
+
@Override
public AccountBalanceData getBalance(String address, long assetId) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT balance FROM AccountBalances WHERE account = ? and asset_id = ?", address, assetId)) {
@@ -58,6 +87,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
@Override
public void save(AccountBalanceData accountBalanceData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("AccountBalances");
+
saveHelper.bind("account", accountBalanceData.getAddress()).bind("asset_id", accountBalanceData.getAssetId()).bind("balance",
accountBalanceData.getBalance());
diff --git a/src/repository/hsqldb/HSQLDBBlockRepository.java b/src/repository/hsqldb/HSQLDBBlockRepository.java
index 6b17aa30..96119aae 100644
--- a/src/repository/hsqldb/HSQLDBBlockRepository.java
+++ b/src/repository/hsqldb/HSQLDBBlockRepository.java
@@ -18,7 +18,7 @@ import repository.TransactionRepository;
public class HSQLDBBlockRepository implements BlockRepository {
private static final String BLOCK_DB_COLUMNS = "version, reference, transaction_count, total_fees, "
- + "transactions_signature, height, generation, generating_balance, generator, generator_signature, AT_data, AT_fees";
+ + "transactions_signature, height, generation, generating_balance, generator, generator_signature, AT_count, AT_fees";
protected HSQLDBRepository repository;
@@ -41,11 +41,11 @@ public class HSQLDBBlockRepository implements BlockRepository {
BigDecimal generatingBalance = resultSet.getBigDecimal(8);
byte[] generatorPublicKey = resultSet.getBytes(9);
byte[] generatorSignature = resultSet.getBytes(10);
- byte[] atBytes = resultSet.getBytes(11);
+ int atCount = resultSet.getInt(11);
BigDecimal atFees = resultSet.getBigDecimal(12);
return new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance,
- generatorPublicKey, generatorSignature, atBytes, atFees);
+ generatorPublicKey, generatorSignature, atCount, atFees);
} catch (SQLException e) {
throw new DataException("Error extracting data from result set", e);
}
@@ -62,7 +62,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
@Override
public BlockData fromReference(byte[] reference) throws DataException {
- try (ResultSet resultSet = this.repository.checkedExecute("SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE height = ?", reference)) {
+ try (ResultSet resultSet = this.repository.checkedExecute("SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE reference = ?", reference)) {
return getBlockFromResultSet(resultSet);
} catch (SQLException e) {
throw new DataException("Error loading data from DB", e);
@@ -123,7 +123,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
transactions.add(transactionRepo.fromSignature(transactionSignature));
} while (resultSet.next());
} catch (SQLException e) {
- throw new DataException(e);
+ throw new DataException("Unable to fetch block's transactions from repository", e);
}
return transactions;
@@ -138,7 +138,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
.bind("transactions_signature", blockData.getTransactionsSignature()).bind("height", blockData.getHeight())
.bind("generation", new Timestamp(blockData.getTimestamp())).bind("generating_balance", blockData.getGeneratingBalance())
.bind("generator", blockData.getGeneratorPublicKey()).bind("generator_signature", blockData.getGeneratorSignature())
- .bind("AT_data", blockData.getAtBytes()).bind("AT_fees", blockData.getAtFees());
+ .bind("AT_count", blockData.getATCount()).bind("AT_fees", blockData.getATFees());
try {
saveHelper.execute(this.repository);
@@ -159,6 +159,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
@Override
public void save(BlockTransactionData blockTransactionData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("BlockTransactions");
+
saveHelper.bind("block_signature", blockTransactionData.getBlockSignature()).bind("sequence", blockTransactionData.getSequence())
.bind("transaction_signature", blockTransactionData.getTransactionSignature());
diff --git a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java
index f3f1dba7..8d4742cf 100644
--- a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java
+++ b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java
@@ -85,7 +85,7 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE TYPE Signature AS VARBINARY(64)");
stmt.execute("CREATE TYPE QoraAddress AS VARCHAR(36)");
stmt.execute("CREATE TYPE QoraPublicKey AS VARBINARY(32)");
- stmt.execute("CREATE TYPE QoraAmount AS DECIMAL(19, 8)");
+ stmt.execute("CREATE TYPE QoraAmount AS DECIMAL(27, 8)");
stmt.execute("CREATE TYPE RegisteredName AS VARCHAR(400) COLLATE SQL_TEXT_NO_PAD");
stmt.execute("CREATE TYPE NameData AS VARCHAR(4000)");
stmt.execute("CREATE TYPE PollName AS VARCHAR(400) COLLATE SQL_TEXT_NO_PAD");
@@ -99,6 +99,7 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE TYPE ATType AS VARCHAR(200) COLLATE SQL_TEXT_UCC_NO_PAD");
stmt.execute("CREATE TYPE ATCode AS BLOB(64K)"); // 16bit * 1
stmt.execute("CREATE TYPE ATState AS BLOB(1M)"); // 16bit * 8 + 16bit * 4 + 16bit * 4
+ stmt.execute("CREATE TYPE ATStateHash as VARBINARY(32)");
stmt.execute("CREATE TYPE ATMessage AS VARBINARY(256)");
break;
@@ -107,7 +108,7 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE TABLE Blocks (signature BlockSignature PRIMARY KEY, version TINYINT NOT NULL, reference BlockSignature, "
+ "transaction_count INTEGER NOT NULL, total_fees QoraAmount NOT NULL, transactions_signature Signature NOT NULL, "
+ "height INTEGER NOT NULL, generation TIMESTAMP WITH TIME ZONE NOT NULL, generating_balance QoraAmount NOT NULL, "
- + "generator QoraPublicKey NOT NULL, generator_signature Signature NOT NULL, AT_data VARBINARY(20000), AT_fees QoraAmount)");
+ + "generator QoraPublicKey NOT NULL, generator_signature Signature NOT NULL, AT_count INTEGER NOT NULL, AT_fees QoraAmount NOT NULL)");
// For finding blocks by height.
stmt.execute("CREATE INDEX BlockHeightIndex ON Blocks (height)");
// For finding blocks by the account that generated them.
@@ -302,7 +303,7 @@ public class HSQLDBDatabaseUpdates {
// Accounts
stmt.execute("CREATE TABLE Accounts (account QoraAddress, reference Signature, PRIMARY KEY (account))");
stmt.execute("CREATE TABLE AccountBalances (account QoraAddress, asset_id AssetID, balance QoraAmount NOT NULL, "
- + "PRIMARY KEY (account, asset_id))");
+ + "PRIMARY KEY (account, asset_id), FOREIGN KEY (account) REFERENCES Accounts (account) ON DELETE CASCADE)");
break;
case 23:
@@ -352,17 +353,21 @@ public class HSQLDBDatabaseUpdates {
case 27:
// CIYAM Automated Transactions
- stmt.execute("CREATE TABLE ATs (AT_address QoraAddress, version INTEGER NOT NULL, code_bytes ATCode NOT NULL, "
- + "is_sleeping BOOLEAN NOT NULL, sleep_until_height INTEGER, is_finished BOOLEAN NOT NULL, had_fatal_error BOOLEAN NOT NULL, "
- + "is_frozen BOOLEAN NOT NULL, frozen_balance QoraAmount, deploy_signature Signature NOT NULL, PRIMARY key (AT_address))");
- // For finding executable ATs
- stmt.execute("CREATE INDEX ATIndex on ATs (is_finished, AT_address)");
+ stmt.execute("CREATE TABLE ATs (AT_address QoraAddress, creator QoraAddress, creation TIMESTAMP WITH TIME ZONE, version INTEGER NOT NULL, "
+ + "code_bytes ATCode NOT NULL, is_sleeping BOOLEAN NOT NULL, sleep_until_height INTEGER, "
+ + "is_finished BOOLEAN NOT NULL, had_fatal_error BOOLEAN NOT NULL, is_frozen BOOLEAN NOT NULL, frozen_balance QoraAmount, "
+ + "PRIMARY key (AT_address))");
+ // For finding executable ATs, ordered by creation timestamp
+ stmt.execute("CREATE INDEX ATIndex on ATs (is_finished, creation, AT_address)");
// AT state on a per-block basis
- stmt.execute("CREATE TABLE ATStates (AT_address QoraAddress, height INTEGER NOT NULL, state_data ATState, "
+ stmt.execute("CREATE TABLE ATStates (AT_address QoraAddress, height INTEGER NOT NULL, creation TIMESTAMP WITH TIME ZONE, "
+ + "state_data ATState, state_hash ATStateHash NOT NULL, fees QoraAmount NOT NULL, "
+ "PRIMARY KEY (AT_address, height), FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
+ // For finding per-block AT states, ordered by creation timestamp
+ stmt.execute("CREATE INDEX BlockATStateIndex on ATStates (height, creation, AT_address)");
// Generated AT Transactions
stmt.execute(
- "CREATE TABLE ATTransactions (signature Signature, sender QoraPublicKey NOT NULL, recipient QoraAddress, amount QoraAmount NOT NULL, message ATMessage, "
+ "CREATE TABLE ATTransactions (signature Signature, AT_address QoraAddress NOT NULL, recipient QoraAddress, amount QoraAmount, asset_id AssetID, message ATMessage, "
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
break;
diff --git a/src/repository/hsqldb/transaction/HSQLDBATTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBATTransactionRepository.java
new file mode 100644
index 00000000..f1103985
--- /dev/null
+++ b/src/repository/hsqldb/transaction/HSQLDBATTransactionRepository.java
@@ -0,0 +1,63 @@
+package repository.hsqldb.transaction;
+
+import java.math.BigDecimal;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import data.transaction.ATTransactionData;
+import data.transaction.TransactionData;
+import repository.DataException;
+import repository.hsqldb.HSQLDBRepository;
+import repository.hsqldb.HSQLDBSaver;
+
+public class HSQLDBATTransactionRepository extends HSQLDBTransactionRepository {
+
+ public HSQLDBATTransactionRepository(HSQLDBRepository repository) {
+ this.repository = repository;
+ }
+
+ TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException {
+ try (ResultSet resultSet = this.repository
+ .checkedExecute("SELECT AT_address, recipient, amount, asset_id, message FROM ATTransactions WHERE signature = ?", signature)) {
+ if (resultSet == null)
+ return null;
+
+ String atAddress = resultSet.getString(1);
+ String recipient = resultSet.getString(2);
+
+ BigDecimal amount = resultSet.getBigDecimal(3);
+ if (resultSet.wasNull())
+ amount = null;
+
+ Long assetId = resultSet.getLong(4);
+ if (resultSet.wasNull())
+ assetId = null;
+
+ byte[] message = resultSet.getBytes(5);
+ if (resultSet.wasNull())
+ message = null;
+
+ return new ATTransactionData(atAddress, recipient, amount, assetId, message, fee, timestamp, reference, signature);
+ } catch (SQLException e) {
+ throw new DataException("Unable to fetch AT transaction from repository", e);
+ }
+ }
+
+ @Override
+ public void save(TransactionData transactionData) throws DataException {
+ ATTransactionData atTransactionData = (ATTransactionData) transactionData;
+
+ HSQLDBSaver saveHelper = new HSQLDBSaver("ATTransactions");
+
+ saveHelper.bind("signature", atTransactionData.getSignature()).bind("AT_address", atTransactionData.getATAddress())
+ .bind("recipient", atTransactionData.getRecipient()).bind("amount", atTransactionData.getAmount())
+ .bind("asset_id", atTransactionData.getAssetId()).bind("message", atTransactionData.getMessage());
+
+ try {
+ saveHelper.execute(this.repository);
+ } catch (SQLException e) {
+ throw new DataException("Unable to save AT transaction into repository", e);
+ }
+ }
+
+}
diff --git a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java
index 316ee6a7..8360274a 100644
--- a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java
+++ b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java
@@ -37,6 +37,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
private HSQLDBMultiPaymentTransactionRepository multiPaymentTransactionRepository;
private HSQLDBDeployATTransactionRepository deployATTransactionRepository;
private HSQLDBMessageTransactionRepository messageTransactionRepository;
+ private HSQLDBATTransactionRepository atTransactionRepository;
public HSQLDBTransactionRepository(HSQLDBRepository repository) {
this.repository = repository;
@@ -57,6 +58,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
this.multiPaymentTransactionRepository = new HSQLDBMultiPaymentTransactionRepository(repository);
this.deployATTransactionRepository = new HSQLDBDeployATTransactionRepository(repository);
this.messageTransactionRepository = new HSQLDBMessageTransactionRepository(repository);
+ this.atTransactionRepository = new HSQLDBATTransactionRepository(repository);
}
protected HSQLDBTransactionRepository() {
@@ -154,18 +156,30 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
case MESSAGE:
return this.messageTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee);
+ case AT:
+ return this.atTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee);
+
default:
- throw new DataException("Unsupported transaction type [" + type.value + "] during fetch from HSQLDB repository");
+ throw new DataException("Unsupported transaction type [" + type.name() + "] during fetch from HSQLDB repository");
}
}
+ /**
+ * Returns payments associated with a transaction's signature.
+ *
+ * Used by various transaction types, like Payment, MultiPayment, ArbitraryTransaction.
+ *
+ * @param signature
+ * @return list of payments, empty if none found
+ * @throws DataException
+ */
protected List getPaymentsFromSignature(byte[] signature) throws DataException {
+ List payments = new ArrayList();
+
try (ResultSet resultSet = this.repository.checkedExecute("SELECT recipient, amount, asset_id FROM SharedTransactionPayments WHERE signature = ?",
signature)) {
if (resultSet == null)
- return null;
-
- List payments = new ArrayList();
+ return payments;
// NOTE: do-while because checkedExecute() above has already called rs.next() for us
do {
@@ -317,8 +331,12 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
this.messageTransactionRepository.save(transactionData);
break;
+ case AT:
+ this.atTransactionRepository.save(transactionData);
+ break;
+
default:
- throw new DataException("Unsupported transaction type [" + transactionData.getType().value + "] during save into HSQLDB repository");
+ throw new DataException("Unsupported transaction type [" + transactionData.getType().name() + "] during save into HSQLDB repository");
}
}
diff --git a/src/test/ATTests.java b/src/test/ATTests.java
index 4d2942eb..860d381d 100644
--- a/src/test/ATTests.java
+++ b/src/test/ATTests.java
@@ -8,6 +8,7 @@ import java.util.Arrays;
import com.google.common.hash.HashCode;
+import data.at.ATStateData;
import data.block.BlockData;
import data.block.BlockTransactionData;
import data.transaction.DeployATTransactionData;
@@ -69,13 +70,22 @@ public class ATTests extends Common {
long blockTimestamp = 1439997158336L;
BigDecimal generatingBalance = BigDecimal.valueOf(1440368826L).setScale(8);
byte[] generatorPublicKey = Base58.decode("X4s833bbtghh7gejmaBMbWqD44HrUobw93ANUuaNhFc");
- byte[] atBytes = HashCode.fromString("17950a6c62d17ff0caa545651c054a105f1c464daca443df846cc6a3d58f764b78c09cff50f0fd9ec2").asBytes();
+ int atCount = 1;
BigDecimal atFees = BigDecimal.valueOf(50.0).setScale(8);
BlockData blockData = new BlockData(version, blockReference, transactionCount, totalFees, transactionsSignature, height, blockTimestamp,
- generatingBalance, generatorPublicKey, generatorSignature, atBytes, atFees);
+ generatingBalance, generatorPublicKey, generatorSignature, atCount, atFees);
repository.getBlockRepository().save(blockData);
+
+ byte[] atBytes = HashCode.fromString("17950a6c62d17ff0caa545651c054a105f1c464daca443df846cc6a3d58f764b78c09cff50f0fd9ec2").asBytes();
+
+ String atAddress = Base58.encode(Arrays.copyOfRange(atBytes, 0, 25));
+ byte[] stateHash = Arrays.copyOfRange(atBytes, 25, atBytes.length);
+
+ ATStateData atStateData = new ATStateData(atAddress, height, timestamp, new byte[0], stateHash, atFees);
+
+ repository.getATRepository().save(atStateData);
}
int sequence = 0;
diff --git a/src/test/NavigationTests.java b/src/test/NavigationTests.java
index 504b7d1b..952a59c0 100644
--- a/src/test/NavigationTests.java
+++ b/src/test/NavigationTests.java
@@ -36,7 +36,7 @@ public class NavigationTests extends Common {
System.out.println("Block " + blockData.getHeight() + ", signature: " + Base58.encode(blockData.getSignature()));
- assertEquals(49778, blockData.getHeight());
+ assertEquals((Integer) 49778, blockData.getHeight());
}
}
diff --git a/src/test/SignatureTests.java b/src/test/SignatureTests.java
index 54aeafa4..bc15ee14 100644
--- a/src/test/SignatureTests.java
+++ b/src/test/SignatureTests.java
@@ -47,11 +47,7 @@ public class SignatureTests extends Common {
PrivateKeyAccount generator = new PrivateKeyAccount(repository,
new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 });
- byte[] atBytes = null;
-
- BigDecimal atFees = null;
-
- Block block = new Block(repository, version, reference, timestamp, generatingBalance, generator, atBytes, atFees);
+ Block block = new Block(repository, version, reference, timestamp, generatingBalance, generator);
block.sign();
assertTrue(block.isSignatureValid());
diff --git a/src/test/TransactionTests.java b/src/test/TransactionTests.java
index 3d491db0..cb73ccb2 100644
--- a/src/test/TransactionTests.java
+++ b/src/test/TransactionTests.java
@@ -172,7 +172,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, paymentTransaction.isValid());
// Forge new block with transaction
- Block block = new Block(repository, parentBlockData, generator, null, null);
+ Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(paymentTransactionData);
block.sign();
@@ -233,7 +233,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, registerNameTransaction.isValid());
// Forge new block with transaction
- Block block = new Block(repository, parentBlockData, generator, null, null);
+ Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(registerNameTransactionData);
block.sign();
@@ -289,7 +289,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, updateNameTransaction.isValid());
// Forge new block with transaction
- Block block = new Block(repository, parentBlockData, generator, null, null);
+ Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(updateNameTransactionData);
block.sign();
@@ -334,7 +334,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, sellNameTransaction.isValid());
// Forge new block with transaction
- Block block = new Block(repository, parentBlockData, generator, null, null);
+ Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(sellNameTransactionData);
block.sign();
@@ -385,7 +385,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, cancelSellNameTransaction.isValid());
// Forge new block with transaction
- Block block = new Block(repository, parentBlockData, generator, null, null);
+ Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(cancelSellNameTransactionData);
block.sign();
@@ -432,7 +432,7 @@ public class TransactionTests {
byte[] buyersReference = somePaymentTransaction.getTransactionData().getSignature();
// Forge new block with transaction
- Block block = new Block(repository, parentBlockData, generator, null, null);
+ Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(somePaymentTransaction.getTransactionData());
block.sign();
@@ -451,7 +451,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, buyNameTransaction.isValid());
// Forge new block with transaction
- block = new Block(repository, parentBlockData, generator, null, null);
+ block = new Block(repository, parentBlockData, generator);
block.addTransaction(buyNameTransactionData);
block.sign();
@@ -504,7 +504,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, createPollTransaction.isValid());
// Forge new block with transaction
- Block block = new Block(repository, parentBlockData, generator, null, null);
+ Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(createPollTransactionData);
block.sign();
@@ -563,7 +563,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, voteOnPollTransaction.isValid());
// Forge new block with transaction
- Block block = new Block(repository, parentBlockData, generator, null, null);
+ Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(voteOnPollTransactionData);
block.sign();
@@ -630,7 +630,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, issueAssetTransaction.isValid());
// Forge new block with transaction
- Block block = new Block(repository, parentBlockData, generator, null, null);
+ Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(issueAssetTransactionData);
block.sign();
@@ -720,7 +720,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, transferAssetTransaction.isValid());
// Forge new block with transaction
- Block block = new Block(repository, parentBlockData, generator, null, null);
+ Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(transferAssetTransactionData);
block.sign();
@@ -800,7 +800,7 @@ public class TransactionTests {
byte[] buyersReference = somePaymentTransaction.getTransactionData().getSignature();
// Forge new block with transaction
- Block block = new Block(repository, parentBlockData, generator, null, null);
+ Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(somePaymentTransaction.getTransactionData());
block.sign();
@@ -824,7 +824,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, createOrderTransaction.isValid());
// Forge new block with transaction
- block = new Block(repository, parentBlockData, generator, null, null);
+ block = new Block(repository, parentBlockData, generator);
block.addTransaction(createOrderTransactionData);
block.sign();
@@ -905,7 +905,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, cancelOrderTransaction.isValid());
// Forge new block with transaction
- Block block = new Block(repository, parentBlockData, generator, null, null);
+ Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(cancelOrderTransactionData);
block.sign();
@@ -980,7 +980,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, createOrderTransaction.isValid());
// Forge new block with transaction
- Block block = new Block(repository, parentBlockData, generator, null, null);
+ Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(createOrderTransactionData);
block.sign();
@@ -1089,7 +1089,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, multiPaymentTransaction.isValid());
// Forge new block with payment transaction
- Block block = new Block(repository, parentBlockData, generator, null, null);
+ Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(multiPaymentTransactionData);
block.sign();
@@ -1159,7 +1159,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, messageTransaction.isValid());
// Forge new block with message transaction
- Block block = new Block(repository, parentBlockData, generator, null, null);
+ Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(messageTransactionData);
block.sign();
diff --git a/src/transform/Transformer.java b/src/transform/Transformer.java
index cd6b4328..ad9959f9 100644
--- a/src/transform/Transformer.java
+++ b/src/transform/Transformer.java
@@ -14,4 +14,7 @@ public abstract class Transformer {
public static final int SIGNATURE_LENGTH = 64;
public static final int TIMESTAMP_LENGTH = LONG_LENGTH;
+ public static final int MD5_LENGTH = 16;
+ public static final int SHA256_LENGTH = 32;
+
}
diff --git a/src/transform/block/BlockTransformer.java b/src/transform/block/BlockTransformer.java
index 7d7336ae..5bc5796e 100644
--- a/src/transform/block/BlockTransformer.java
+++ b/src/transform/block/BlockTransformer.java
@@ -17,6 +17,7 @@ import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import data.assets.TradeData;
+import data.at.ATStateData;
import data.block.BlockData;
import data.transaction.TransactionData;
import qora.account.PublicKeyAccount;
@@ -24,12 +25,13 @@ import qora.assets.Order;
import qora.block.Block;
import qora.transaction.CreateOrderTransaction;
import qora.transaction.Transaction;
+import qora.transaction.Transaction.TransactionType;
import repository.DataException;
import transform.TransformationException;
import transform.Transformer;
import transform.transaction.TransactionTransformer;
import utils.Base58;
-import utils.Pair;
+import utils.Triple;
import utils.Serialization;
public class BlockTransformer extends Transformer {
@@ -52,6 +54,9 @@ public class BlockTransformer extends Transformer {
protected static final int AT_FEES_LENGTH = LONG_LENGTH;
protected static final int AT_LENGTH = AT_FEES_LENGTH + AT_BYTES_LENGTH;
+ protected static final int V2_AT_ENTRY_LENGTH = ADDRESS_LENGTH + MD5_LENGTH;
+ protected static final int V4_AT_ENTRY_LENGTH = ADDRESS_LENGTH + SHA256_LENGTH + BIG_DECIMAL_LENGTH;
+
/**
* Extract block data and transaction data from serialized bytes.
*
@@ -59,7 +64,7 @@ public class BlockTransformer extends Transformer {
* @return BlockData and a List of transactions.
* @throws TransformationException
*/
- public static Pair> fromBytes(byte[] bytes) throws TransformationException {
+ public static Triple, List> fromBytes(byte[] bytes) throws TransformationException {
if (bytes == null)
return null;
@@ -88,25 +93,74 @@ public class BlockTransformer extends Transformer {
byte[] generatorSignature = new byte[GENERATOR_SIGNATURE_LENGTH];
byteBuffer.get(generatorSignature);
- byte[] atBytes = null;
- BigDecimal atFees = null;
+ BigDecimal totalFees = BigDecimal.ZERO.setScale(8);
+
+ int atCount = 0;
+ BigDecimal atFees = BigDecimal.ZERO.setScale(8);
+ List atStates = new ArrayList();
+
if (version >= 2) {
int atBytesLength = byteBuffer.getInt();
if (atBytesLength > Block.MAX_BLOCK_BYTES)
throw new TransformationException("Byte data too long for Block's AT info");
- atBytes = new byte[atBytesLength];
- byteBuffer.get(atBytes);
+ ByteBuffer atByteBuffer = byteBuffer.slice();
+ atByteBuffer.limit(atBytesLength);
- atFees = BigDecimal.valueOf(byteBuffer.getLong()).setScale(8);
+ if (version < 4) {
+ // For versions < 4, read AT-address & MD5 pairs
+ if (atBytesLength % V2_AT_ENTRY_LENGTH != 0)
+ throw new TransformationException("AT byte data not a multiple of version 2+ entries");
+
+ while (atByteBuffer.hasRemaining()) {
+ byte[] atAddressBytes = new byte[ADDRESS_LENGTH];
+ atByteBuffer.get(atAddressBytes);
+ String atAddress = Base58.encode(atAddressBytes);
+
+ byte[] stateHash = new byte[MD5_LENGTH];
+ atByteBuffer.get(stateHash);
+
+ atStates.add(new ATStateData(atAddress, stateHash));
+ }
+
+ // Bump byteBuffer over AT states just read in slice
+ byteBuffer.position(byteBuffer.position() + atBytesLength);
+
+ // AT fees follow in versions < 4
+ atFees = Serialization.deserializeBigDecimal(byteBuffer);
+ } else {
+ // For block versions >= 4, read AT-address, SHA256 hash and fees
+ if (atBytesLength % V4_AT_ENTRY_LENGTH != 0)
+ throw new TransformationException("AT byte data not a multiple of version 4+ entries");
+
+ while (atByteBuffer.hasRemaining()) {
+ byte[] atAddressBytes = new byte[ADDRESS_LENGTH];
+ atByteBuffer.get(atAddressBytes);
+ String atAddress = Base58.encode(atAddressBytes);
+
+ byte[] stateHash = new byte[SHA256_LENGTH];
+ atByteBuffer.get(stateHash);
+
+ BigDecimal fees = Serialization.deserializeBigDecimal(atByteBuffer);
+ // Add this AT's fees to our total
+ atFees = atFees.add(fees);
+
+ atStates.add(new ATStateData(atAddress, stateHash, fees));
+ }
+ }
+
+ // AT count to reflect the number of states we have
+ atCount = atStates.size();
+
+ // Add AT fees to totalFees
+ totalFees = totalFees.add(atFees);
}
int transactionCount = byteBuffer.getInt();
// Parse transactions now, compared to deferred parsing in Gen1, so we can throw ParseException if need be.
List transactions = new ArrayList();
- BigDecimal totalFees = BigDecimal.ZERO.setScale(8);
for (int t = 0; t < transactionCount; ++t) {
if (byteBuffer.remaining() < TRANSACTION_SIZE_LENGTH)
@@ -126,26 +180,28 @@ public class BlockTransformer extends Transformer {
TransactionData transactionData = TransactionTransformer.fromBytes(transactionBytes);
transactions.add(transactionData);
- totalFees.add(transactionData.getFee());
+ totalFees = totalFees.add(transactionData.getFee());
}
if (byteBuffer.hasRemaining())
throw new TransformationException("Excess byte data found after parsing Block");
- // XXX we don't know height!
- int height = 0;
+ // We don't have a height!
+ Integer height = null;
BlockData blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance,
- generatorPublicKey, generatorSignature, atBytes, atFees);
+ generatorPublicKey, generatorSignature, atCount, atFees);
- return new Pair>(blockData, transactions);
+ return new Triple, List>(blockData, transactions, atStates);
}
public static int getDataLength(Block block) throws TransformationException {
BlockData blockData = block.getBlockData();
int blockLength = BASE_LENGTH;
- if (blockData.getVersion() >= 2 && blockData.getAtBytes() != null)
- blockLength += AT_FEES_LENGTH + AT_BYTES_LENGTH + blockData.getAtBytes().length;
+ if (blockData.getVersion() >= 4)
+ blockLength += AT_BYTES_LENGTH + blockData.getATCount() * V4_AT_ENTRY_LENGTH;
+ else if (blockData.getVersion() >= 2)
+ blockLength += AT_FEES_LENGTH + AT_BYTES_LENGTH + blockData.getATCount() * V2_AT_ENTRY_LENGTH;
try {
// Short cut for no transactions
@@ -177,18 +233,29 @@ public class BlockTransformer extends Transformer {
bytes.write(blockData.getTransactionsSignature());
bytes.write(blockData.getGeneratorSignature());
- if (blockData.getVersion() >= 2) {
- byte[] atBytes = blockData.getAtBytes();
+ if (blockData.getVersion() >= 4) {
+ int atBytesLength = blockData.getATCount() * V4_AT_ENTRY_LENGTH;
+ bytes.write(Ints.toByteArray(atBytesLength));
- if (atBytes != null) {
- bytes.write(Ints.toByteArray(atBytes.length));
- bytes.write(atBytes);
- // NOTE: atFees serialized as long value, not as BigDecimal, for historic compatibility
- bytes.write(Longs.toByteArray(blockData.getAtFees().longValue()));
- } else {
- bytes.write(Ints.toByteArray(0));
- bytes.write(Longs.toByteArray(0L));
+ for (ATStateData atStateData : block.getATStates()) {
+ bytes.write(Base58.decode(atStateData.getATAddress()));
+ bytes.write(atStateData.getStateHash());
+ Serialization.serializeBigDecimal(bytes, atStateData.getFees());
}
+ } else if (blockData.getVersion() >= 2) {
+ int atBytesLength = blockData.getATCount() * V2_AT_ENTRY_LENGTH;
+ bytes.write(Ints.toByteArray(atBytesLength));
+
+ for (ATStateData atStateData : block.getATStates()) {
+ bytes.write(Base58.decode(atStateData.getATAddress()));
+ bytes.write(atStateData.getStateHash());
+ }
+
+ if (blockData.getATFees() != null)
+ // NOTE: atFees serialized as long value, not as BigDecimal, for historic compatibility
+ bytes.write(Longs.toByteArray(blockData.getATFees().longValue()));
+ else
+ bytes.write(Longs.toByteArray(0));
}
// Transactions
@@ -263,30 +330,39 @@ public class BlockTransformer extends Transformer {
json.put("assetTrades", tradesHappened);
// Add CIYAM AT info (if any)
- if (blockData.getAtBytes() != null) {
- json.put("blockATs", HashCode.fromBytes(blockData.getAtBytes()).toString());
- json.put("atFees", blockData.getAtFees());
+ if (blockData.getATCount() > 0) {
+ JSONArray atsJson = new JSONArray();
+
+ try {
+ for (ATStateData atStateData : block.getATStates()) {
+ JSONObject atJson = new JSONObject();
+
+ atJson.put("AT", atStateData.getATAddress());
+ atJson.put("stateHash", HashCode.fromBytes(atStateData.getStateHash()).toString());
+
+ if (blockData.getVersion() >= 4)
+ atJson.put("fees", atStateData.getFees().toPlainString());
+
+ atsJson.add(atJson);
+ }
+ } catch (DataException e) {
+ throw new TransformationException("Unable to transform block into JSON", e);
+ }
+
+ json.put("ATs", atsJson);
+
+ if (blockData.getVersion() >= 2)
+ json.put("atFees", blockData.getATFees());
}
return json;
}
public static byte[] getBytesForGeneratorSignature(BlockData blockData) throws TransformationException {
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH);
+ byte[] generatorSignature = Arrays.copyOf(blockData.getReference(), GENERATOR_SIGNATURE_LENGTH);
+ PublicKeyAccount generator = new PublicKeyAccount(null, blockData.getGeneratorPublicKey());
- // Only copy the generator signature from reference, which is the first 64 bytes.
- bytes.write(Arrays.copyOf(blockData.getReference(), GENERATOR_SIGNATURE_LENGTH));
-
- bytes.write(Longs.toByteArray(blockData.getGeneratingBalance().longValue()));
-
- // We're padding here just in case the generator is the genesis account whose public key is only 8 bytes long.
- bytes.write(Bytes.ensureCapacity(blockData.getGeneratorPublicKey(), GENERATOR_LENGTH, 0));
-
- return bytes.toByteArray();
- } catch (IOException e) {
- throw new TransformationException(e);
- }
+ return getBytesForGeneratorSignature(generatorSignature, blockData.getGeneratingBalance(), generator);
}
public static byte[] getBytesForGeneratorSignature(byte[] generatorSignature, BigDecimal generatingBalance, PublicKeyAccount generator)
@@ -308,13 +384,18 @@ public class BlockTransformer extends Transformer {
}
public static byte[] getBytesForTransactionsSignature(Block block) throws TransformationException {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream(
- GENERATOR_SIGNATURE_LENGTH + block.getBlockData().getTransactionCount() * TransactionTransformer.SIGNATURE_LENGTH);
-
try {
+ List transactions = block.getTransactions();
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + transactions.size() * TransactionTransformer.SIGNATURE_LENGTH);
+
bytes.write(block.getBlockData().getGeneratorSignature());
- for (Transaction transaction : block.getTransactions()) {
+ for (Transaction transaction : transactions) {
+ // For legacy blocks, we don't include AT-Transactions
+ if (block.getBlockData().getVersion() < 4 && transaction.getTransactionData().getType() == TransactionType.AT)
+ continue;
+
if (!transaction.isSignatureValid())
throw new TransformationException("Transaction signature invalid when building block's transactions signature");
diff --git a/src/transform/transaction/ATTransactionTransformer.java b/src/transform/transaction/ATTransactionTransformer.java
new file mode 100644
index 00000000..ff83272b
--- /dev/null
+++ b/src/transform/transaction/ATTransactionTransformer.java
@@ -0,0 +1,92 @@
+package transform.transaction;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+import org.json.simple.JSONObject;
+
+import com.google.common.primitives.Ints;
+import com.google.common.primitives.Longs;
+
+import data.transaction.TransactionData;
+import data.transaction.ATTransactionData;
+import transform.TransformationException;
+import utils.Serialization;
+
+public class ATTransactionTransformer extends TransactionTransformer {
+
+ // Property lengths
+ private static final int SENDER_LENGTH = ADDRESS_LENGTH;
+ private static final int RECIPIENT_LENGTH = ADDRESS_LENGTH;
+ private static final int AMOUNT_LENGTH = BIG_DECIMAL_LENGTH;
+ private static final int ASSET_ID_LENGTH = LONG_LENGTH;
+ private static final int DATA_SIZE_LENGTH = INT_LENGTH;
+
+ private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH + ASSET_ID_LENGTH + DATA_SIZE_LENGTH;
+
+ static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
+ throw new TransformationException("Serialized AT Transactions should not exist!");
+ }
+
+ public static int getDataLength(TransactionData transactionData) throws TransformationException {
+ ATTransactionData atTransactionData = (ATTransactionData) transactionData;
+
+ return TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + atTransactionData.getMessage().length;
+ }
+
+ public static byte[] toBytes(TransactionData transactionData) throws TransformationException {
+ try {
+ ATTransactionData atTransactionData = (ATTransactionData) transactionData;
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ bytes.write(Ints.toByteArray(atTransactionData.getType().value));
+ bytes.write(Longs.toByteArray(atTransactionData.getTimestamp()));
+ bytes.write(atTransactionData.getReference());
+
+ Serialization.serializeAddress(bytes, atTransactionData.getATAddress());
+
+ Serialization.serializeAddress(bytes, atTransactionData.getRecipient());
+
+ if (atTransactionData.getAssetId() != null) {
+ Serialization.serializeBigDecimal(bytes, atTransactionData.getAmount());
+ bytes.write(Longs.toByteArray(atTransactionData.getAssetId()));
+ }
+
+ byte[] message = atTransactionData.getMessage();
+ if (message.length > 0) {
+ bytes.write(Ints.toByteArray(message.length));
+ bytes.write(message);
+ } else {
+ bytes.write(Ints.toByteArray(0));
+ }
+
+ Serialization.serializeBigDecimal(bytes, atTransactionData.getFee());
+
+ if (atTransactionData.getSignature() != null)
+ bytes.write(atTransactionData.getSignature());
+
+ return bytes.toByteArray();
+ } catch (IOException | ClassCastException e) {
+ throw new TransformationException(e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public static JSONObject toJSON(TransactionData transactionData) throws TransformationException {
+ JSONObject json = TransactionTransformer.getBaseJSON(transactionData);
+
+ try {
+ ATTransactionData atTransactionData = (ATTransactionData) transactionData;
+
+ json.put("sender", atTransactionData.getATAddress());
+
+ } catch (ClassCastException e) {
+ throw new TransformationException(e);
+ }
+
+ return json;
+ }
+
+}
diff --git a/src/transform/transaction/DeployATTransactionTransformer.java b/src/transform/transaction/DeployATTransactionTransformer.java
index 0699084f..12d78906 100644
--- a/src/transform/transaction/DeployATTransactionTransformer.java
+++ b/src/transform/transaction/DeployATTransactionTransformer.java
@@ -97,7 +97,9 @@ public class DeployATTransactionTransformer extends TransactionTransformer {
Serialization.serializeSizedString(bytes, deployATTransactionData.getTags());
- bytes.write(deployATTransactionData.getCreationBytes());
+ byte[] creationBytes = deployATTransactionData.getCreationBytes();
+ bytes.write(Ints.toByteArray(creationBytes.length));
+ bytes.write(creationBytes);
Serialization.serializeBigDecimal(bytes, deployATTransactionData.getAmount());
@@ -146,15 +148,14 @@ public class DeployATTransactionTransformer extends TransactionTransformer {
// Omitted: Serialization.serializeSizedString(bytes, deployATTransactionData.getTags());
- bytes.write(deployATTransactionData.getCreationBytes());
+ byte[] creationBytes = deployATTransactionData.getCreationBytes();
+ bytes.write(Ints.toByteArray(creationBytes.length));
+ bytes.write(creationBytes);
Serialization.serializeBigDecimal(bytes, deployATTransactionData.getAmount());
Serialization.serializeBigDecimal(bytes, deployATTransactionData.getFee());
- if (deployATTransactionData.getSignature() != null)
- bytes.write(deployATTransactionData.getSignature());
-
return bytes.toByteArray();
} catch (IOException | ClassCastException e) {
throw new TransformationException(e);
diff --git a/src/utils/Pair.java b/src/utils/Pair.java
index 8c353395..8468bbee 100644
--- a/src/utils/Pair.java
+++ b/src/utils/Pair.java
@@ -29,4 +29,22 @@ public class Pair {
return b;
}
+ @Override
+ public boolean equals(Object o) {
+ if (o == this)
+ return true;
+
+ if (!(o instanceof Pair, ?>))
+ return false;
+
+ Pair, ?> other = (Pair, ?>) o;
+
+ return this.a.equals(other.getA()) && this.b.equals(other.getB());
+ }
+
+ @Override
+ public int hashCode() {
+ return this.a.hashCode() ^ this.b.hashCode();
+ }
+
}
diff --git a/src/utils/Triple.java b/src/utils/Triple.java
new file mode 100644
index 00000000..c19ee068
--- /dev/null
+++ b/src/utils/Triple.java
@@ -0,0 +1,42 @@
+package utils;
+
+public class Triple {
+
+ private T a;
+ private U b;
+ private V c;
+
+ public Triple() {
+ }
+
+ public Triple(T a, U b, V c) {
+ this.a = a;
+ this.b = b;
+ this.c = c;
+ }
+
+ public void setA(T a) {
+ this.a = a;
+ }
+
+ public T getA() {
+ return a;
+ }
+
+ public void setB(U b) {
+ this.b = b;
+ }
+
+ public U getB() {
+ return b;
+ }
+
+ public void setC(V c) {
+ this.c = c;
+ }
+
+ public V getC() {
+ return c;
+ }
+
+}
diff --git a/src/v1feeder.java b/src/v1feeder.java
index 42d69f7f..ab032645 100644
--- a/src/v1feeder.java
+++ b/src/v1feeder.java
@@ -1,7 +1,9 @@
+import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.OutputStream;
+import java.math.BigDecimal;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
@@ -9,17 +11,32 @@ import java.net.SocketTimeoutException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.JSONValue;
+import org.json.simple.parser.ParseException;
+import com.google.common.hash.HashCode;
+import com.google.common.primitives.Bytes;
import com.google.common.primitives.Ints;
+import data.at.ATData;
+import data.at.ATStateData;
import data.block.BlockData;
+import data.transaction.ATTransactionData;
import data.transaction.TransactionData;
+import qora.assets.Asset;
import qora.block.Block;
import qora.block.Block.ValidationResult;
import qora.block.BlockChain;
@@ -29,7 +46,10 @@ import repository.Repository;
import repository.RepositoryManager;
import transform.TransformationException;
import transform.block.BlockTransformer;
+import transform.transaction.ATTransactionTransformer;
+import utils.Base58;
import utils.Pair;
+import utils.Triple;
public class v1feeder extends Thread {
@@ -77,6 +97,9 @@ public class v1feeder extends Thread {
private long lastPingTimestamp = System.currentTimeMillis();
private List signatures = new ArrayList();
+ private static Map, BigDecimal> legacyATFees;
+ private static Map> legacyATTransactions;
+
private v1feeder(String address, int port) throws InterruptedException {
try {
for (int i = 0; i < 10; ++i)
@@ -177,6 +200,9 @@ public class v1feeder extends Thread {
// shove into list
int numSignatures = byteBuffer.getInt();
+ if (numSignatures == 0)
+ throw new RuntimeException("No signatures from peer - are we up to date?");
+
while (numSignatures-- > 0) {
byte[] signature = new byte[SIGNATURE_LENGTH];
byteBuffer.get(signature);
@@ -201,7 +227,7 @@ public class v1feeder extends Thread {
byte[] blockBytes = new byte[byteBuffer.remaining()];
byteBuffer.get(blockBytes);
- Pair> blockInfo = null;
+ Triple, List> blockInfo = null;
try {
blockInfo = BlockTransformer.fromBytes(blockBytes);
@@ -211,7 +237,26 @@ public class v1feeder extends Thread {
}
try (final Repository repository = RepositoryManager.getRepository()) {
- Block block = new Block(repository, blockInfo.getA(), blockInfo.getB());
+ BlockData blockData = blockInfo.getA();
+
+ // Adjust AT state data to include fees
+ List atStates = new ArrayList();
+ for (ATStateData atState : blockInfo.getC()) {
+ BigDecimal fees = legacyATFees.get(new Pair(atState.getATAddress(), claimedHeight));
+ ATData atData = repository.getATRepository().fromATAddress(atState.getATAddress());
+
+ atStates.add(new ATStateData(atState.getATAddress(), claimedHeight, atData.getCreation(), null, atState.getStateHash(), fees));
+ }
+
+ // AT-Transaction injection goes here!
+ List transactions = blockInfo.getB();
+ List atTransactions = legacyATTransactions.get(claimedHeight);
+ if (atTransactions != null) {
+ transactions.addAll(0, atTransactions);
+ blockData.setTransactionCount(blockData.getTransactionCount() + atTransactions.size());
+ }
+
+ Block block = new Block(repository, blockData, transactions, atStates);
if (!block.isSignatureValid()) {
LOGGER.error("Invalid block signature");
@@ -221,8 +266,8 @@ public class v1feeder extends Thread {
ValidationResult result = block.isValid();
if (result != ValidationResult.OK) {
- LOGGER.error("Invalid block, validation result code: " + result.value);
- throw new RuntimeException("Invalid block, validation result code: " + result.value);
+ LOGGER.error("Invalid block, validation result: " + result.name());
+ throw new RuntimeException("Invalid block, validation result: " + result.name());
}
block.process();
@@ -398,12 +443,88 @@ public class v1feeder extends Thread {
}
}
+ private static void readLegacyATs(String legacyATPathname) {
+ legacyATFees = new HashMap, BigDecimal>();
+ legacyATTransactions = new HashMap>();
+
+ Path path = Paths.get(legacyATPathname);
+
+ JSONArray json = null;
+
+ try (BufferedReader in = Files.newBufferedReader(path)) {
+ json = (JSONArray) JSONValue.parseWithException(in);
+ } catch (IOException | ParseException e) {
+ throw new RuntimeException("Couldn't read legacy AT JSON file");
+ }
+
+ for (Object o : json) {
+ JSONObject entry = (JSONObject) o;
+
+ int height = Integer.parseInt((String) entry.get("height"));
+ long timestamp = (Long) entry.get("timestamp");
+
+ JSONArray transactionEntries = (JSONArray) entry.get("transactions");
+
+ List transactions = new ArrayList();
+
+ for (Object t : transactionEntries) {
+ JSONObject transactionEntry = (JSONObject) t;
+
+ String recipient = (String) transactionEntry.get("recipient");
+ String sender = (String) transactionEntry.get("sender");
+ BigDecimal amount = new BigDecimal((String) transactionEntry.get("amount")).setScale(8);
+
+ if (recipient.equals("1111111111111111111111111")) {
+ // fee
+ legacyATFees.put(new Pair(sender, height), amount);
+ } else {
+ // Actual AT Transaction
+ String messageString = (String) transactionEntry.get("message");
+ byte[] message = messageString.isEmpty() ? new byte[0] : HashCode.fromString(messageString).asBytes();
+ int sequence = ((Long) transactionEntry.get("seq")).intValue();
+ byte[] reference = Base58.decode((String) transactionEntry.get("reference"));
+
+ // reference is AT's deploy tx signature
+ // sender's public key is genesis account
+ // zero fee
+ // timestamp is block's timestamp
+ // signature = duplicated hash of transaction data
+
+ BigDecimal fee = BigDecimal.ZERO.setScale(8);
+
+ TransactionData transactionData = new ATTransactionData(sender, recipient, amount, Asset.QORA, message, fee, timestamp, reference);
+ byte[] digest;
+ try {
+ digest = Crypto.digest(ATTransactionTransformer.toBytes(transactionData));
+ byte[] signature = Bytes.concat(digest, digest);
+
+ transactionData = new ATTransactionData(sender, recipient, amount, Asset.QORA, message, fee, timestamp, reference, signature);
+ } catch (TransformationException e) {
+ throw new RuntimeException("Couldn't transform AT Transaction into bytes", e);
+ }
+
+ if (sequence > transactions.size())
+ transactions.add(transactionData);
+ else
+ transactions.add(sequence, transactionData);
+ }
+ }
+
+ if (!transactions.isEmpty())
+ legacyATTransactions.put(height, transactions);
+ }
+ }
+
public static void main(String[] args) {
- if (args.length == 0) {
- System.err.println("usage: v1feeder v1-node-address [port]");
+ if (args.length < 2 || args.length > 3) {
+ System.err.println("usage: v1feeder legacy-AT-json v1-node-address [port]");
+ System.err.println("example: v1feeder legacy-ATs.json 10.0.0.100 9084");
System.exit(1);
}
+ String legacyATPathname = args[0];
+ readLegacyATs(legacyATPathname);
+
try {
test.Common.setRepository();
} catch (DataException e) {
@@ -419,8 +540,8 @@ public class v1feeder extends Thread {
}
// connect to v1 node
- String address = args[0];
- int port = args.length > 1 ? Integer.valueOf(args[1]) : DEFAULT_PORT;
+ String address = args[1];
+ int port = args.length > 2 ? Integer.valueOf(args[2]) : DEFAULT_PORT;
try {
new v1feeder(address, port).join();