diff --git a/src/data/transaction/ArbitraryTransactionData.java b/src/data/transaction/ArbitraryTransactionData.java new file mode 100644 index 00000000..a5de2238 --- /dev/null +++ b/src/data/transaction/ArbitraryTransactionData.java @@ -0,0 +1,91 @@ +package data.transaction; + +import java.math.BigDecimal; +import java.util.List; + +import data.PaymentData; +import qora.transaction.Transaction.TransactionType; + +public class ArbitraryTransactionData extends TransactionData { + + // "data" field types + public enum DataType { + RAW_DATA, DATA_HASH; + } + + // Properties + private int version; + private byte[] senderPublicKey; + private int service; + private byte[] data; + private DataType dataType; + private List payments; + + // Constructors + + /** Reconstructing a V3 arbitrary transaction with signature */ + public ArbitraryTransactionData(int version, byte[] senderPublicKey, int service, byte[] data, DataType dataType, List payments, + BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { + super(TransactionType.ARBITRARY, fee, senderPublicKey, timestamp, reference, signature); + + this.version = version; + this.senderPublicKey = senderPublicKey; + this.service = service; + this.data = data; + this.dataType = dataType; + this.payments = payments; + } + + /** Constructing a new V3 arbitrary transaction without signature */ + public ArbitraryTransactionData(int version, byte[] senderPublicKey, int service, byte[] data, DataType dataType, List payments, + BigDecimal fee, long timestamp, byte[] reference) { + this(version, senderPublicKey, service, data, dataType, payments, fee, timestamp, reference, null); + } + + /** Reconstructing a V1 arbitrary transaction with signature */ + public ArbitraryTransactionData(int version, byte[] senderPublicKey, int service, byte[] data, DataType dataType, BigDecimal fee, long timestamp, + byte[] reference, byte[] signature) { + this(version, senderPublicKey, service, data, dataType, null, fee, timestamp, reference, signature); + } + + /** Constructing a new V1 arbitrary transaction without signature */ + public ArbitraryTransactionData(int version, byte[] senderPublicKey, int service, byte[] data, DataType dataType, BigDecimal fee, long timestamp, + byte[] reference) { + this(version, senderPublicKey, service, data, dataType, null, fee, timestamp, reference, null); + } + + // Getters/Setters + + public int getVersion() { + return this.version; + } + + public byte[] getSenderPublicKey() { + return this.senderPublicKey; + } + + public int getService() { + return this.service; + } + + public byte[] getData() { + return this.data; + } + + public void setData(byte[] data) { + this.data = data; + } + + public DataType getDataType() { + return this.dataType; + } + + public void setDataType(DataType dataType) { + this.dataType = dataType; + } + + public List getPayments() { + return this.payments; + } + +} diff --git a/src/data/transaction/BuyNameTransactionData.java b/src/data/transaction/BuyNameTransactionData.java index 635e6a24..5f534610 100644 --- a/src/data/transaction/BuyNameTransactionData.java +++ b/src/data/transaction/BuyNameTransactionData.java @@ -23,6 +23,7 @@ public class BuyNameTransactionData extends TransactionData { this.name = name; this.amount = amount; this.seller = seller; + this.nameReference = nameReference; } public BuyNameTransactionData(byte[] buyerPublicKey, String name, BigDecimal amount, String seller, BigDecimal fee, long timestamp, byte[] reference, diff --git a/src/data/transaction/MessageTransactionData.java b/src/data/transaction/MessageTransactionData.java index e9da1e67..734ebb7c 100644 --- a/src/data/transaction/MessageTransactionData.java +++ b/src/data/transaction/MessageTransactionData.java @@ -8,14 +8,14 @@ import qora.transaction.Transaction.TransactionType; public class MessageTransactionData extends TransactionData { // Properties - protected int version; - protected byte[] senderPublicKey; - protected String recipient; - protected Long assetId; - protected BigDecimal amount; - protected byte[] data; - protected boolean isText; - protected boolean isEncrypted; + private int version; + private byte[] senderPublicKey; + private String recipient; + private Long assetId; + private BigDecimal amount; + private byte[] data; + private boolean isText; + private boolean isEncrypted; // Constructors diff --git a/src/orphan.java b/src/orphan.java new file mode 100644 index 00000000..6090c99f --- /dev/null +++ b/src/orphan.java @@ -0,0 +1,52 @@ +import data.block.BlockData; +import qora.block.Block; +import qora.block.BlockChain; +import repository.DataException; +import repository.Repository; +import repository.RepositoryManager; + +public class orphan { + + public static void main(String[] args) { + if (args.length == 0) { + System.err.println("usage: orphan "); + System.exit(1); + } + + int targetHeight = Integer.parseInt(args[0]); + + try { + test.Common.setRepository(); + } catch (DataException e) { + System.err.println("Couldn't connect to repository: " + e.getMessage()); + System.exit(2); + } + + try { + BlockChain.validate(); + } catch (DataException e) { + System.err.println("Couldn't validate repository: " + e.getMessage()); + System.exit(2); + } + + try (final Repository repository = RepositoryManager.getRepository()) { + for (int height = repository.getBlockRepository().getBlockchainHeight(); height > targetHeight; --height) { + System.out.println("Orphaning block " + height); + + BlockData blockData = repository.getBlockRepository().fromHeight(height); + Block block = new Block(repository, blockData); + block.orphan(); + repository.saveChanges(); + } + } catch (DataException e) { + e.printStackTrace(); + } + + try { + test.Common.closeRepository(); + } catch (DataException e) { + e.printStackTrace(); + } + } + +} diff --git a/src/qora/block/Block.java b/src/qora/block/Block.java index 22d5863b..16110038 100644 --- a/src/qora/block/Block.java +++ b/src/qora/block/Block.java @@ -94,6 +94,19 @@ public class Block { this.generator = new PublicKeyAccount(repository, blockData.getGeneratorPublicKey()); } + // When receiving a block over network? + public Block(Repository repository, BlockData blockData, List transactions) throws DataException { + this(repository, blockData); + + this.transactions = new ArrayList(); + + // 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())); + } + } + // For creating a new block? public Block(Repository repository, int version, byte[] reference, long timestamp, BigDecimal generatingBalance, PrivateKeyAccount generator, byte[] atBytes, BigDecimal atFees) { @@ -211,8 +224,11 @@ public class Block { long expectedGeneratingTime = Block.calcForgingDelay(this.blockData.getGeneratingBalance()) * BlockChain.BLOCK_RETARGET_INTERVAL * 1000; // Finally, scale generating balance such that faster than expected previous intervals produce larger generating balances. - BigDecimal multiplier = BigDecimal.valueOf((double) expectedGeneratingTime / (double) previousGeneratingTime); - this.cachedNextGeneratingBalance = BlockChain.minMaxBalance(this.blockData.getGeneratingBalance().multiply(multiplier)); + // NOTE: we have to use doubles and longs here to keep compatibility with Qora v1 results + double multiplier = (double) expectedGeneratingTime / (double) previousGeneratingTime; + long nextGeneratingBalance = (long) (this.blockData.getGeneratingBalance().doubleValue() * multiplier); + + this.cachedNextGeneratingBalance = BlockChain.minMaxBalance(BigDecimal.valueOf(nextGeneratingBalance).setScale(8)); return this.cachedNextGeneratingBalance; } @@ -257,9 +273,9 @@ public class Block { byte[] hashData; if (this.blockData.getVersion() < 3) - hashData = this.blockData.getSignature(); + hashData = this.blockData.getGeneratorSignature(); else - hashData = Bytes.concat(this.blockData.getSignature(), generator.getPublicKey()); + hashData = Bytes.concat(this.blockData.getReference(), generator.getPublicKey()); // Calculate 32-byte hash as pseudo-random, but deterministic, integer (unique to this generator for v3+ blocks) byte[] hash = Crypto.digest(hashData); @@ -501,11 +517,11 @@ public class Block { return ValidationResult.FEATURE_NOT_YET_RELEASED; // Check generating balance - if (this.blockData.getGeneratingBalance() != parentBlock.calcNextBlockGeneratingBalance()) + if (this.blockData.getGeneratingBalance().compareTo(parentBlock.calcNextBlockGeneratingBalance()) != 0) return ValidationResult.GENERATING_BALANCE_INCORRECT; // Check generator is allowed to forge this block at this time - BigInteger hashValue = parentBlock.calcBlockHash(); + BigInteger hashValue = this.calcBlockHash(); BigInteger target = parentBlock.calcGeneratorsTarget(this.generator); // Multiply target by guesses @@ -547,15 +563,19 @@ public class Block { // Check transaction is even valid // NOTE: in Gen1 there was an extra block height passed to DeployATTransaction.isValid - if (transaction.isValid() != Transaction.ValidationResult.OK) + Transaction.ValidationResult validationResult = transaction.isValid(); + if (validationResult != Transaction.ValidationResult.OK) { + System.err.println("Error during transaction validation, tx " + Base58.encode(transaction.getTransactionData().getSignature()) + ": " + + validationResult.value); return ValidationResult.TRANSACTION_INVALID; + } // Process transaction to make sure other transactions validate properly try { transaction.process(); } catch (Exception e) { - // LOGGER.error("Exception during transaction processing, tx " + Base58.encode(transaction.getSignature()), e); - System.err.println("Exception during transaction processing, tx " + Base58.encode(transaction.getTransactionData().getSignature()) + ": " + // LOGGER.error("Exception during transaction validation, tx " + Base58.encode(transaction.getSignature()), e); + System.err.println("Exception during transaction validation, tx " + Base58.encode(transaction.getTransactionData().getSignature()) + ": " + e.getMessage()); e.printStackTrace(); return ValidationResult.TRANSACTION_PROCESSING_FAILED; @@ -587,7 +607,7 @@ public class Block { // If fees are non-zero then add fees to generator's balance BigDecimal blockFee = this.blockData.getTotalFees(); - if (blockFee.compareTo(BigDecimal.ZERO) == 1) + if (blockFee.compareTo(BigDecimal.ZERO) > 0) this.generator.setConfirmedBalance(Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).add(blockFee)); // Link block into blockchain by fetching signature of highest block and setting that as our reference @@ -629,7 +649,7 @@ public class Block { // If fees are non-zero then remove fees from generator's balance BigDecimal blockFee = this.blockData.getTotalFees(); - if (blockFee.compareTo(BigDecimal.ZERO) == 1) + if (blockFee.compareTo(BigDecimal.ZERO) > 0) this.generator.setConfirmedBalance(Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).subtract(blockFee)); // Delete block from blockchain diff --git a/src/qora/block/BlockChain.java b/src/qora/block/BlockChain.java index 476a27d1..f94d8043 100644 --- a/src/qora/block/BlockChain.java +++ b/src/qora/block/BlockChain.java @@ -37,6 +37,10 @@ public class BlockChain { private static final long POWFIX_RELEASE_TIMESTAMP = 1456426800000L; // Block Version 3 // 2016-02-25T19:00:00+00:00 private static final long ASSETS_RELEASE_TIMESTAMP = 0L; // From Qora epoch private static final long VOTING_RELEASE_TIMESTAMP = 1403715600000L; // 2014-06-25T17:00:00+00:00 + private static final long ARBITRARY_RELEASE_TIMESTAMP = 1405702800000L; // 2014-07-18T17:00:00+00:00 + private static final long CREATE_POLL_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 CREATE POLL transactions + private static final long ISSUE_ASSET_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 ISSUE ASSET transactions + private static final long CREATE_ORDER_V2_TIMESTAMP = 1552500000000L; // 2019-03-13T18:00:00+00:00 // Future Qora v2 CREATE ORDER transactions /** * Some sort start-up/initialization/checking method. @@ -132,4 +136,32 @@ public class BlockChain { return VOTING_RELEASE_TIMESTAMP; } + public static long getArbitraryReleaseTimestamp() { + if (Settings.getInstance().isTestNet()) + return 0; + + return ARBITRARY_RELEASE_TIMESTAMP; + } + + public static long getCreatePollV2Timestamp() { + if (Settings.getInstance().isTestNet()) + return 0; + + return CREATE_POLL_V2_TIMESTAMP; + } + + public static long getIssueAssetV2Timestamp() { + if (Settings.getInstance().isTestNet()) + return 0; + + return ISSUE_ASSET_V2_TIMESTAMP; + } + + public static long getCreateOrderV2Timestamp() { + if (Settings.getInstance().isTestNet()) + return 0; + + return CREATE_ORDER_V2_TIMESTAMP; + } + } diff --git a/src/qora/naming/Name.java b/src/qora/naming/Name.java index 917e9479..b3faa076 100644 --- a/src/qora/naming/Name.java +++ b/src/qora/naming/Name.java @@ -9,6 +9,7 @@ import data.transaction.TransactionData; import data.transaction.UpdateNameTransactionData; import qora.account.Account; import qora.account.PublicKeyAccount; +import qora.assets.Asset; import repository.DataException; import repository.Repository; @@ -152,9 +153,15 @@ public class Name { // Mark not for-sale but leave price in case we want to orphan this.nameData.setIsForSale(false); + // Update seller's balance + Account seller = new Account(this.repository, this.nameData.getOwner()); + seller.setConfirmedBalance(Asset.QORA, seller.getConfirmedBalance(Asset.QORA).add(buyNameTransactionData.getAmount())); + // Set new owner Account buyer = new PublicKeyAccount(this.repository, buyNameTransactionData.getBuyerPublicKey()); this.nameData.setOwner(buyer.getAddress()); + // Update buyer's balance + buyer.setConfirmedBalance(Asset.QORA, buyer.getConfirmedBalance(Asset.QORA).subtract(buyNameTransactionData.getAmount())); // Update reference in transaction data buyNameTransactionData.setNameReference(this.nameData.getReference()); @@ -173,9 +180,17 @@ public class Name { // Previous name reference is taken from this transaction's cached copy this.nameData.setReference(buyNameTransactionData.getNameReference()); + // Revert buyer's balance + Account buyer = new PublicKeyAccount(this.repository, buyNameTransactionData.getBuyerPublicKey()); + buyer.setConfirmedBalance(Asset.QORA, buyer.getConfirmedBalance(Asset.QORA).add(buyNameTransactionData.getAmount())); + // Previous Name's owner and/or data taken from referenced transaction this.revert(); + // Revert seller's balance + Account seller = new Account(this.repository, this.nameData.getOwner()); + seller.setConfirmedBalance(Asset.QORA, seller.getConfirmedBalance(Asset.QORA).subtract(buyNameTransactionData.getAmount())); + // Save reverted name data this.repository.getNameRepository().save(this.nameData); } diff --git a/src/qora/payment/Payment.java b/src/qora/payment/Payment.java index 756f0a7e..7625a981 100644 --- a/src/qora/payment/Payment.java +++ b/src/qora/payment/Payment.java @@ -46,35 +46,36 @@ public class Payment { amountsByAssetId.put(Asset.QORA, fee); // Check payments, and calculate amount total by assetId - for (PaymentData paymentData : payments) { - // Check amount is positive - if (paymentData.getAmount().compareTo(BigDecimal.ZERO) < 0) - return ValidationResult.NEGATIVE_AMOUNT; + if (payments != null) + for (PaymentData paymentData : payments) { + // Check amount is 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())); - } + 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); for (Entry pair : amountsByAssetId.entrySet()) - if (sender.getConfirmedBalance(pair.getKey()).compareTo(pair.getValue()) == -1) + if (sender.getConfirmedBalance(pair.getKey()).compareTo(pair.getValue()) < 0) return ValidationResult.NO_BALANCE; return ValidationResult.OK; @@ -103,21 +104,22 @@ public class Payment { sender.setLastReference(signature); // Process all payments - for (PaymentData paymentData : payments) { - Account recipient = new Account(this.repository, paymentData.getRecipient()); - long assetId = paymentData.getAssetId(); - BigDecimal amount = paymentData.getAmount(); + if (payments != null) + 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).subtract(amount)); + // 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)); + // 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) - recipient.setLastReference(signature); - } + // For QORA amounts only: if recipient has no reference yet, then this is their starting reference + if (assetId == Asset.QORA && recipient.getLastReference() == null) + recipient.setLastReference(signature); + } } public void process(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee, byte[] signature) throws DataException { @@ -133,24 +135,26 @@ public class Payment { // Update sender's reference sender.setLastReference(reference); - for (PaymentData paymentData : payments) { - Account recipient = new Account(this.repository, paymentData.getRecipient()); - long assetId = paymentData.getAssetId(); - BigDecimal amount = paymentData.getAmount(); + // 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(); - // 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 (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 (assetId == Asset.QORA && Arrays.equals(recipient.getLastReference(), signature)) + recipient.setLastReference(null); + } } public void orphan(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee, byte[] signature, byte[] reference) throws DataException { diff --git a/src/qora/transaction/ArbitraryTransaction.java b/src/qora/transaction/ArbitraryTransaction.java new file mode 100644 index 00000000..6ca023f3 --- /dev/null +++ b/src/qora/transaction/ArbitraryTransaction.java @@ -0,0 +1,195 @@ +package qora.transaction; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.math.BigDecimal; +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.List; + +import data.PaymentData; +import data.transaction.ArbitraryTransactionData; +import data.transaction.TransactionData; +import data.transaction.ArbitraryTransactionData.DataType; +import qora.account.Account; +import qora.account.PublicKeyAccount; +import qora.assets.Asset; +import qora.block.BlockChain; +import qora.crypto.Crypto; +import qora.payment.Payment; +import repository.DataException; +import repository.Repository; +import settings.Settings; +import utils.Base58; + +public class ArbitraryTransaction extends Transaction { + + // Properties + private ArbitraryTransactionData arbitraryTransactionData; + + // Other useful constants + public static final int MAX_DATA_SIZE = 4000; + + // Constructors + + public ArbitraryTransaction(Repository repository, TransactionData transactionData) { + super(repository, transactionData); + + this.arbitraryTransactionData = (ArbitraryTransactionData) this.transactionData; + } + + // More information + + public List getRecipientAccounts() throws DataException { + List recipients = new ArrayList(); + + if (arbitraryTransactionData.getVersion() != 1) + for (PaymentData paymentData : arbitraryTransactionData.getPayments()) + recipients.add(new Account(this.repository, paymentData.getRecipient())); + + return recipients; + } + + public boolean isInvolved(Account account) throws DataException { + String address = account.getAddress(); + + if (address.equals(this.getSender().getAddress())) + return true; + + if (arbitraryTransactionData.getVersion() != 1) + for (PaymentData paymentData : arbitraryTransactionData.getPayments()) + if (address.equals(paymentData.getRecipient())) + return true; + + return false; + } + + public BigDecimal getAmount(Account account) throws DataException { + String address = account.getAddress(); + BigDecimal amount = BigDecimal.ZERO.setScale(8); + String senderAddress = this.getSender().getAddress(); + + if (address.equals(senderAddress)) + amount = amount.subtract(this.transactionData.getFee()); + + if (arbitraryTransactionData.getVersion() != 1) + for (PaymentData paymentData : arbitraryTransactionData.getPayments()) + // We're only interested in QORA + if (paymentData.getAssetId() == Asset.QORA) { + if (address.equals(paymentData.getRecipient())) + amount = amount.add(paymentData.getAmount()); + else if (address.equals(senderAddress)) + amount = amount.subtract(paymentData.getAmount()); + } + + return amount; + } + + // Navigation + + public Account getSender() throws DataException { + return new PublicKeyAccount(this.repository, this.arbitraryTransactionData.getSenderPublicKey()); + } + + // Processing + + @Override + public ValidationResult isValid() throws DataException { + // Are arbitrary transactions even allowed at this point? + if (arbitraryTransactionData.getVersion() != ArbitraryTransaction.getVersionByTimestamp(arbitraryTransactionData.getTimestamp())) + return ValidationResult.NOT_YET_RELEASED; + + if (this.arbitraryTransactionData.getTimestamp() < BlockChain.getArbitraryReleaseTimestamp()) + return ValidationResult.NOT_YET_RELEASED; + + // Check data length + if (arbitraryTransactionData.getData().length < 1 || arbitraryTransactionData.getData().length > MAX_DATA_SIZE) + return ValidationResult.INVALID_DATA_LENGTH; + + // Check reference is correct + Account sender = getSender(); + if (!Arrays.equals(sender.getLastReference(), arbitraryTransactionData.getReference())) + return ValidationResult.INVALID_REFERENCE; + + // Wrap and delegate final payment checks to Payment class + return new Payment(this.repository).isValid(arbitraryTransactionData.getSenderPublicKey(), arbitraryTransactionData.getPayments(), + arbitraryTransactionData.getFee()); + } + + @Override + public void process() throws DataException { + /* + * We might have either raw data or only a hash of data, depending on content filtering. + * + * If we have raw data then we need to save it somewhere and store the hash in the repository. + */ + if (arbitraryTransactionData.getDataType() == DataType.RAW_DATA) { + byte[] rawData = arbitraryTransactionData.getData(); + + // Calculate hash of data and update our transaction to use that + byte[] dataHash = Crypto.digest(rawData); + arbitraryTransactionData.setData(dataHash); + arbitraryTransactionData.setDataType(DataType.DATA_HASH); + + // Now store actual data somewhere, e.g. /arbitrary///-.raw + Account sender = this.getSender(); + int blockHeight = this.repository.getBlockRepository().getBlockchainHeight(); + String dataPathname = Settings.getInstance().getUserpath() + "arbitrary" + File.separator + sender.getAddress() + File.separator + blockHeight + + File.separator + Base58.encode(arbitraryTransactionData.getSignature()) + "-" + arbitraryTransactionData.getService() + ".raw"; + + Path dataPath = Paths.get(dataPathname); + + // Make sure directory structure exists + try { + Files.createDirectories(dataPath.getParent()); + } catch (IOException e1) { + // TODO Auto-generated catch block + e1.printStackTrace(); + } + + // Output actual transaction data + try (OutputStream dataOut = Files.newOutputStream(dataPath)) { + dataOut.write(rawData); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + // Save this transaction itself + this.repository.getTransactionRepository().save(this.transactionData); + + // Wrap and delegate payment processing to Payment class + new Payment(this.repository).process(arbitraryTransactionData.getSenderPublicKey(), arbitraryTransactionData.getPayments(), + arbitraryTransactionData.getFee(), arbitraryTransactionData.getSignature()); + } + + @Override + public void orphan() throws DataException { + // Delete corresponding data file (if any - storing raw data is optional) + Account sender = this.getSender(); + int blockHeight = this.repository.getBlockRepository().getBlockchainHeight(); + String dataPathname = Settings.getInstance().getUserpath() + "arbitrary" + File.separator + sender.getAddress() + File.separator + blockHeight + + File.separator + Base58.encode(arbitraryTransactionData.getSignature()) + "-" + arbitraryTransactionData.getService() + ".raw"; + + Path dataPath = Paths.get(dataPathname); + try { + Files.deleteIfExists(dataPath); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + // Delete this transaction itself + this.repository.getTransactionRepository().delete(this.transactionData); + + // Wrap and delegate payment processing to Payment class + new Payment(this.repository).orphan(arbitraryTransactionData.getSenderPublicKey(), arbitraryTransactionData.getPayments(), + arbitraryTransactionData.getFee(), arbitraryTransactionData.getSignature(), arbitraryTransactionData.getReference()); + } + +} diff --git a/src/qora/transaction/BuyNameTransaction.java b/src/qora/transaction/BuyNameTransaction.java index a42545a6..d73df077 100644 --- a/src/qora/transaction/BuyNameTransaction.java +++ b/src/qora/transaction/BuyNameTransaction.java @@ -56,7 +56,10 @@ public class BuyNameTransaction extends Transaction { BigDecimal amount = BigDecimal.ZERO.setScale(8); if (address.equals(this.getBuyer().getAddress())) - amount = amount.subtract(this.transactionData.getFee()); + amount = amount.subtract(this.transactionData.getFee()).subtract(this.buyNameTransactionData.getAmount()); + + if (address.equals(this.buyNameTransactionData.getSeller())) + amount = amount.add(this.buyNameTransactionData.getAmount()); return amount; } @@ -112,7 +115,7 @@ public class BuyNameTransaction extends Transaction { return ValidationResult.INVALID_REFERENCE; // Check issuer has enough funds - if (buyer.getConfirmedBalance(Asset.QORA).compareTo(buyNameTransactionData.getFee()) == -1) + if (buyer.getConfirmedBalance(Asset.QORA).compareTo(buyNameTransactionData.getFee()) < 0) return ValidationResult.NO_BALANCE; return ValidationResult.OK; diff --git a/src/qora/transaction/CancelOrderTransaction.java b/src/qora/transaction/CancelOrderTransaction.java index 15bbf456..12012756 100644 --- a/src/qora/transaction/CancelOrderTransaction.java +++ b/src/qora/transaction/CancelOrderTransaction.java @@ -83,7 +83,7 @@ public class CancelOrderTransaction extends Transaction { return ValidationResult.INVALID_ORDER_CREATOR; // Check creator has enough QORA for fee - if (creator.getConfirmedBalance(Asset.QORA).compareTo(cancelOrderTransactionData.getFee()) == -1) + if (creator.getConfirmedBalance(Asset.QORA).compareTo(cancelOrderTransactionData.getFee()) < 0) return ValidationResult.NO_BALANCE; // Check reference is correct diff --git a/src/qora/transaction/CancelSellNameTransaction.java b/src/qora/transaction/CancelSellNameTransaction.java index be4b6bd8..60e49218 100644 --- a/src/qora/transaction/CancelSellNameTransaction.java +++ b/src/qora/transaction/CancelSellNameTransaction.java @@ -101,7 +101,7 @@ public class CancelSellNameTransaction extends Transaction { return ValidationResult.INVALID_REFERENCE; // Check issuer has enough funds - if (owner.getConfirmedBalance(Asset.QORA).compareTo(cancelSellNameTransactionData.getFee()) == -1) + if (owner.getConfirmedBalance(Asset.QORA).compareTo(cancelSellNameTransactionData.getFee()) < 0) return ValidationResult.NO_BALANCE; return ValidationResult.OK; diff --git a/src/qora/transaction/CreateOrderTransaction.java b/src/qora/transaction/CreateOrderTransaction.java index ff9c059c..38a1a63c 100644 --- a/src/qora/transaction/CreateOrderTransaction.java +++ b/src/qora/transaction/CreateOrderTransaction.java @@ -64,6 +64,7 @@ public class CreateOrderTransaction extends Transaction { // Processing + @Override public ValidationResult isValid() throws DataException { long haveAssetId = createOrderTransactionData.getHaveAssetId(); long wantAssetId = createOrderTransactionData.getWantAssetId(); @@ -106,17 +107,17 @@ public class CreateOrderTransaction extends Transaction { // If asset is QORA then we need to check amount + fee in one go if (haveAssetId == Asset.QORA) { // Check creator has enough funds for amount + fee in QORA - if (creator.getConfirmedBalance(Asset.QORA).compareTo(createOrderTransactionData.getAmount().add(createOrderTransactionData.getFee())) == -1) + if (creator.getConfirmedBalance(Asset.QORA).compareTo(createOrderTransactionData.getAmount().add(createOrderTransactionData.getFee())) < 0) return ValidationResult.NO_BALANCE; } else { // Check creator has enough funds for amount in whatever asset - if (creator.getConfirmedBalance(haveAssetId).compareTo(createOrderTransactionData.getAmount()) == -1) + if (creator.getConfirmedBalance(haveAssetId).compareTo(createOrderTransactionData.getAmount()) < 0) return ValidationResult.NO_BALANCE; // Check creator has enough funds for fee in QORA // NOTE: in Gen1 pre-POWFIX-RELEASE transactions didn't have this check if (createOrderTransactionData.getTimestamp() >= BlockChain.getPowFixReleaseTimestamp() - && creator.getConfirmedBalance(Asset.QORA).compareTo(createOrderTransactionData.getFee()) == -1) + && creator.getConfirmedBalance(Asset.QORA).compareTo(createOrderTransactionData.getFee()) < 0) return ValidationResult.NO_BALANCE; } @@ -132,6 +133,7 @@ public class CreateOrderTransaction extends Transaction { return ValidationResult.OK; } + @Override public void process() throws DataException { Account creator = getCreator(); @@ -155,6 +157,7 @@ public class CreateOrderTransaction extends Transaction { new Order(this.repository, orderData).process(); } + @Override public void orphan() throws DataException { Account creator = getCreator(); diff --git a/src/qora/transaction/CreatePollTransaction.java b/src/qora/transaction/CreatePollTransaction.java index 949bfb97..0d009fd1 100644 --- a/src/qora/transaction/CreatePollTransaction.java +++ b/src/qora/transaction/CreatePollTransaction.java @@ -137,7 +137,7 @@ public class CreatePollTransaction extends Transaction { return ValidationResult.INVALID_REFERENCE; // Check issuer has enough funds - if (creator.getConfirmedBalance(Asset.QORA).compareTo(createPollTransactionData.getFee()) == -1) + if (creator.getConfirmedBalance(Asset.QORA).compareTo(createPollTransactionData.getFee()) < 0) return ValidationResult.NO_BALANCE; return ValidationResult.OK; diff --git a/src/qora/transaction/GenesisTransaction.java b/src/qora/transaction/GenesisTransaction.java index 1b997f98..1e1f55fa 100644 --- a/src/qora/transaction/GenesisTransaction.java +++ b/src/qora/transaction/GenesisTransaction.java @@ -76,7 +76,7 @@ public class GenesisTransaction extends Transaction { * @throws IllegalStateException */ @Override - public void calcSignature(PrivateKeyAccount signer) { + public void sign(PrivateKeyAccount signer) { throw new IllegalStateException("There is no private key for genesis transactions"); } @@ -115,7 +115,7 @@ public class GenesisTransaction extends Transaction { @Override public ValidationResult isValid() { // Check amount is zero or positive - if (genesisTransactionData.getAmount().compareTo(BigDecimal.ZERO) == -1) + if (genesisTransactionData.getAmount().compareTo(BigDecimal.ZERO) >= 0) return ValidationResult.NEGATIVE_AMOUNT; // Check recipient address is valid diff --git a/src/qora/transaction/IssueAssetTransaction.java b/src/qora/transaction/IssueAssetTransaction.java index 05127cfc..95d296aa 100644 --- a/src/qora/transaction/IssueAssetTransaction.java +++ b/src/qora/transaction/IssueAssetTransaction.java @@ -76,6 +76,7 @@ public class IssueAssetTransaction extends Transaction { // Processing + @Override public ValidationResult isValid() throws DataException { // Are IssueAssetTransactions even allowed at this point? // XXX In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used? @@ -112,7 +113,7 @@ public class IssueAssetTransaction extends Transaction { return ValidationResult.INVALID_REFERENCE; // Check issuer has enough funds - if (issuer.getConfirmedBalance(Asset.QORA).compareTo(issueAssetTransactionData.getFee()) == -1) + if (issuer.getConfirmedBalance(Asset.QORA).compareTo(issueAssetTransactionData.getFee()) < 0) return ValidationResult.NO_BALANCE; // XXX: Surely we want to check the asset name isn't already taken? This check is not present in gen1. @@ -122,6 +123,7 @@ public class IssueAssetTransaction extends Transaction { return ValidationResult.OK; } + @Override public void process() throws DataException { // Issue asset Asset asset = new Asset(this.repository, issueAssetTransactionData); @@ -145,6 +147,7 @@ public class IssueAssetTransaction extends Transaction { owner.setConfirmedBalance(issueAssetTransactionData.getAssetId(), BigDecimal.valueOf(issueAssetTransactionData.getQuantity()).setScale(8)); } + @Override public void orphan() throws DataException { // Remove asset from owner Account owner = getOwner(); diff --git a/src/qora/transaction/MessageTransaction.java b/src/qora/transaction/MessageTransaction.java index d12f3c8e..3ea62f65 100644 --- a/src/qora/transaction/MessageTransaction.java +++ b/src/qora/transaction/MessageTransaction.java @@ -85,6 +85,7 @@ public class MessageTransaction extends Transaction { return new PaymentData(messageTransactionData.getRecipient(), Asset.QORA, messageTransactionData.getAmount()); } + @Override public ValidationResult isValid() throws DataException { // Are message transactions even allowed at this point? if (messageTransactionData.getVersion() != MessageTransaction.getVersionByTimestamp(messageTransactionData.getTimestamp())) @@ -110,6 +111,7 @@ public class MessageTransaction extends Transaction { isZeroAmountValid); } + @Override public void process() throws DataException { // Save this transaction itself this.repository.getTransactionRepository().save(this.transactionData); @@ -119,6 +121,7 @@ public class MessageTransaction extends Transaction { messageTransactionData.getSignature()); } + @Override public void orphan() throws DataException { // Delete this transaction itself this.repository.getTransactionRepository().delete(this.transactionData); diff --git a/src/qora/transaction/MultiPaymentTransaction.java b/src/qora/transaction/MultiPaymentTransaction.java index 1dcd4ed4..8ca52343 100644 --- a/src/qora/transaction/MultiPaymentTransaction.java +++ b/src/qora/transaction/MultiPaymentTransaction.java @@ -15,7 +15,6 @@ import qora.block.BlockChain; import qora.payment.Payment; import repository.DataException; import repository.Repository; -import utils.NTP; public class MultiPaymentTransaction extends Transaction { @@ -90,7 +89,7 @@ public class MultiPaymentTransaction extends Transaction { List payments = multiPaymentTransactionData.getPayments(); // Are MultiPaymentTransactions even allowed at this point? - if (NTP.getTime() < BlockChain.getAssetsReleaseTimestamp()) + if (this.multiPaymentTransactionData.getTimestamp() < BlockChain.getAssetsReleaseTimestamp()) return ValidationResult.NOT_YET_RELEASED; // Check number of payments @@ -106,7 +105,7 @@ public class MultiPaymentTransaction extends Transaction { // Check sender has enough funds for fee // NOTE: in Gen1 pre-POWFIX-RELEASE transactions didn't have this check if (multiPaymentTransactionData.getTimestamp() >= BlockChain.getPowFixReleaseTimestamp() - && sender.getConfirmedBalance(Asset.QORA).compareTo(multiPaymentTransactionData.getFee()) == -1) + && sender.getConfirmedBalance(Asset.QORA).compareTo(multiPaymentTransactionData.getFee()) < 0) return ValidationResult.NO_BALANCE; return new Payment(this.repository).isValid(multiPaymentTransactionData.getSenderPublicKey(), payments, multiPaymentTransactionData.getFee()); diff --git a/src/qora/transaction/PaymentTransaction.java b/src/qora/transaction/PaymentTransaction.java index 4ce901e2..015985d8 100644 --- a/src/qora/transaction/PaymentTransaction.java +++ b/src/qora/transaction/PaymentTransaction.java @@ -72,6 +72,7 @@ public class PaymentTransaction extends Transaction { return new PaymentData(paymentTransactionData.getRecipient(), Asset.QORA, paymentTransactionData.getAmount()); } + @Override public ValidationResult isValid() throws DataException { // Check reference is correct Account sender = getSender(); @@ -82,6 +83,7 @@ public class PaymentTransaction extends Transaction { return new Payment(this.repository).isValid(paymentTransactionData.getSenderPublicKey(), getPaymentData(), paymentTransactionData.getFee()); } + @Override public void process() throws DataException { // Save this transaction itself this.repository.getTransactionRepository().save(this.transactionData); @@ -91,6 +93,7 @@ public class PaymentTransaction extends Transaction { paymentTransactionData.getSignature()); } + @Override public void orphan() throws DataException { // Delete this transaction this.repository.getTransactionRepository().delete(this.transactionData); diff --git a/src/qora/transaction/RegisterNameTransaction.java b/src/qora/transaction/RegisterNameTransaction.java index 7cb9cf1f..29d12b18 100644 --- a/src/qora/transaction/RegisterNameTransaction.java +++ b/src/qora/transaction/RegisterNameTransaction.java @@ -108,7 +108,7 @@ public class RegisterNameTransaction extends Transaction { return ValidationResult.INVALID_REFERENCE; // Check issuer has enough funds - if (registrant.getConfirmedBalance(Asset.QORA).compareTo(registerNameTransactionData.getFee()) == -1) + if (registrant.getConfirmedBalance(Asset.QORA).compareTo(registerNameTransactionData.getFee()) < 0) return ValidationResult.NO_BALANCE; return ValidationResult.OK; diff --git a/src/qora/transaction/SellNameTransaction.java b/src/qora/transaction/SellNameTransaction.java index 2e6e1b05..16d35504 100644 --- a/src/qora/transaction/SellNameTransaction.java +++ b/src/qora/transaction/SellNameTransaction.java @@ -110,7 +110,7 @@ public class SellNameTransaction extends Transaction { return ValidationResult.INVALID_REFERENCE; // Check issuer has enough funds - if (owner.getConfirmedBalance(Asset.QORA).compareTo(sellNameTransactionData.getFee()) == -1) + if (owner.getConfirmedBalance(Asset.QORA).compareTo(sellNameTransactionData.getFee()) < 0) return ValidationResult.NO_BALANCE; return ValidationResult.OK; diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java index 7364d362..42b7c418 100644 --- a/src/qora/transaction/Transaction.java +++ b/src/qora/transaction/Transaction.java @@ -2,9 +2,9 @@ package qora.transaction; import java.math.BigDecimal; import java.math.MathContext; -import java.util.Arrays; import java.util.List; import java.util.Map; + import static java.util.Arrays.stream; import static java.util.stream.Collectors.toMap; @@ -18,7 +18,6 @@ import repository.DataException; import repository.Repository; import settings.Settings; import transform.TransformationException; -import transform.Transformer; import transform.transaction.TransactionTransformer; public abstract class Transaction { @@ -127,6 +126,9 @@ public abstract class Transaction { case VOTE_ON_POLL: return new VoteOnPollTransaction(repository, transactionData); + case ARBITRARY: + return new ArbitraryTransaction(repository, transactionData); + case ISSUE_ASSET: return new IssueAssetTransaction(repository, transactionData); @@ -322,30 +324,14 @@ public abstract class Transaction { return this.repository.getTransactionRepository().fromReference(signature); } - /** - * Serialize transaction as byte[], stripping off trailing signature. - *

- * Used by signature-related methods such as {@link TransactionHandler#calcSignature(PrivateKeyAccount)} and {@link TransactionHandler#isSignatureValid()} - * - * @return byte[] - */ - private byte[] toBytesLessSignature() { - try { - byte[] bytes = TransactionTransformer.toBytes(this.transactionData); - - if (this.transactionData.getSignature() == null) - return bytes; - - return Arrays.copyOf(bytes, bytes.length - Transformer.SIGNATURE_LENGTH); - } catch (TransformationException e) { - throw new RuntimeException("Unable to transform transaction to signature-less byte array", e); - } - } - // Processing - public void calcSignature(PrivateKeyAccount signer) { - this.transactionData.setSignature(signer.sign(this.toBytesLessSignature())); + public void sign(PrivateKeyAccount signer) { + try { + this.transactionData.setSignature(signer.sign(TransactionTransformer.toBytesForSigning(transactionData))); + } catch (TransformationException e) { + throw new RuntimeException("Unable to transform transaction to byte array for signing", e); + } } public boolean isSignatureValid() { @@ -353,7 +339,11 @@ public abstract class Transaction { if (signature == null) return false; - return PublicKeyAccount.verify(this.transactionData.getCreatorPublicKey(), signature, this.toBytesLessSignature()); + try { + return PublicKeyAccount.verify(this.transactionData.getCreatorPublicKey(), signature, TransactionTransformer.toBytesForSigning(transactionData)); + } catch (TransformationException e) { + throw new RuntimeException("Unable to transform transaction to byte array for verification", e); + } } /** diff --git a/src/qora/transaction/TransferAssetTransaction.java b/src/qora/transaction/TransferAssetTransaction.java index be100a00..3409ffbc 100644 --- a/src/qora/transaction/TransferAssetTransaction.java +++ b/src/qora/transaction/TransferAssetTransaction.java @@ -8,7 +8,6 @@ import java.util.List; import data.PaymentData; import data.transaction.TransactionData; import data.transaction.TransferAssetTransactionData; -import utils.NTP; import qora.account.Account; import qora.account.PublicKeyAccount; import qora.assets.Asset; @@ -82,10 +81,9 @@ public class TransferAssetTransaction extends Transaction { @Override public ValidationResult isValid() throws DataException { - TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) this.transactionData; - // Are IssueAssetTransactions even allowed at this point? - if (NTP.getTime() < BlockChain.getAssetsReleaseTimestamp()) + // XXX In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used? + if (this.transferAssetTransactionData.getTimestamp() < BlockChain.getVotingReleaseTimestamp()) return ValidationResult.NOT_YET_RELEASED; // Check reference is correct @@ -100,8 +98,6 @@ public class TransferAssetTransaction extends Transaction { @Override public void process() throws DataException { - TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) this.transactionData; - // Save this transaction itself this.repository.getTransactionRepository().save(this.transactionData); @@ -112,8 +108,6 @@ public class TransferAssetTransaction extends Transaction { @Override public void orphan() throws DataException { - TransferAssetTransactionData transferAssetTransactionData = (TransferAssetTransactionData) this.transactionData; - // Delete this transaction itself this.repository.getTransactionRepository().delete(this.transactionData); diff --git a/src/qora/transaction/UpdateNameTransaction.java b/src/qora/transaction/UpdateNameTransaction.java index 713bc858..ac87a7e6 100644 --- a/src/qora/transaction/UpdateNameTransaction.java +++ b/src/qora/transaction/UpdateNameTransaction.java @@ -118,7 +118,7 @@ public class UpdateNameTransaction extends Transaction { return ValidationResult.INVALID_REFERENCE; // Check issuer has enough funds - if (owner.getConfirmedBalance(Asset.QORA).compareTo(updateNameTransactionData.getFee()) == -1) + if (owner.getConfirmedBalance(Asset.QORA).compareTo(updateNameTransactionData.getFee()) < 0) return ValidationResult.NO_BALANCE; return ValidationResult.OK; diff --git a/src/qora/transaction/VoteOnPollTransaction.java b/src/qora/transaction/VoteOnPollTransaction.java index 2de138b3..276a0a18 100644 --- a/src/qora/transaction/VoteOnPollTransaction.java +++ b/src/qora/transaction/VoteOnPollTransaction.java @@ -110,7 +110,7 @@ public class VoteOnPollTransaction extends Transaction { return ValidationResult.INVALID_REFERENCE; // Check voter has enough funds - if (voter.getConfirmedBalance(Asset.QORA).compareTo(voteOnPollTransactionData.getFee()) == -1) + if (voter.getConfirmedBalance(Asset.QORA).compareTo(voteOnPollTransactionData.getFee()) < 0) return ValidationResult.NO_BALANCE; return ValidationResult.OK; diff --git a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java index 41bc509a..d645fb81 100644 --- a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -76,6 +76,9 @@ public class HSQLDBDatabaseUpdates { case 0: // create from new stmt.execute("SET DATABASE DEFAULT TABLE TYPE CACHED"); + stmt.execute("SET DATABASE COLLATION SQL_TEXT NO PAD"); + stmt.execute("CREATE COLLATION SQL_TEXT_UCC_NO_PAD FOR SQL_TEXT FROM SQL_TEXT_UCC NO PAD"); + stmt.execute("CREATE COLLATION SQL_TEXT_NO_PAD FOR SQL_TEXT FROM SQL_TEXT NO PAD"); stmt.execute("SET FILES SPACE TRUE"); stmt.execute("CREATE TABLE DatabaseInfo ( version INTEGER NOT NULL )"); stmt.execute("INSERT INTO DatabaseInfo VALUES ( 0 )"); @@ -84,17 +87,17 @@ public class HSQLDBDatabaseUpdates { 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 RegisteredName AS VARCHAR(400) COLLATE SQL_TEXT_UCC"); + 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_UCC"); - stmt.execute("CREATE TYPE PollOption AS VARCHAR(400) COLLATE SQL_TEXT_UCC"); + stmt.execute("CREATE TYPE PollName AS VARCHAR(400) COLLATE SQL_TEXT_NO_PAD"); + stmt.execute("CREATE TYPE PollOption AS VARCHAR(400) COLLATE SQL_TEXT_UCC_NO_PAD"); stmt.execute("CREATE TYPE PollOptionIndex AS INTEGER"); - stmt.execute("CREATE TYPE DataHash AS VARCHAR(100)"); + stmt.execute("CREATE TYPE DataHash AS VARBINARY(32)"); stmt.execute("CREATE TYPE AssetID AS BIGINT"); - stmt.execute("CREATE TYPE AssetName AS VARCHAR(400) COLLATE SQL_TEXT_UCC"); + stmt.execute("CREATE TYPE AssetName AS VARCHAR(400) COLLATE SQL_TEXT_NO_PAD"); stmt.execute("CREATE TYPE AssetOrderID AS VARBINARY(64)"); - stmt.execute("CREATE TYPE ATName AS VARCHAR(200) COLLATE SQL_TEXT_UCC"); - stmt.execute("CREATE TYPE ATType AS VARCHAR(200) COLLATE SQL_TEXT_UCC"); + stmt.execute("CREATE TYPE ATName AS VARCHAR(200) COLLATE SQL_TEXT_UCC_NO_PAD"); + stmt.execute("CREATE TYPE ATType AS VARCHAR(200) COLLATE SQL_TEXT_UCC_NO_PAD"); break; case 1: @@ -200,8 +203,8 @@ public class HSQLDBDatabaseUpdates { + "poll_name PollName NOT NULL, description VARCHAR(4000) NOT NULL, " + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); // Poll options. NB: option is implicitly NON NULL and UNIQUE due to being part of compound primary key - stmt.execute("CREATE TABLE CreatePollTransactionOptions (signature Signature, option_name PollOption, " - + "PRIMARY KEY (signature, option_name), FOREIGN KEY (signature) REFERENCES CreatePollTransactions (signature) ON DELETE CASCADE)"); + stmt.execute("CREATE TABLE CreatePollTransactionOptions (signature Signature, option_index TINYINT NOT NULL, option_name PollOption, " + + "PRIMARY KEY (signature, option_index), FOREIGN KEY (signature) REFERENCES CreatePollTransactions (signature) ON DELETE CASCADE)"); // For the future: add flag to polls to allow one or multiple votes per voter break; @@ -221,8 +224,8 @@ public class HSQLDBDatabaseUpdates { case 13: // Arbitrary Transactions - stmt.execute("CREATE TABLE ArbitraryTransactions (signature Signature, creator QoraPublicKey NOT NULL, service TINYINT NOT NULL, " - + "data_hash DataHash NOT NULL, " + stmt.execute("CREATE TABLE ArbitraryTransactions (signature Signature, sender QoraPublicKey NOT NULL, version TINYINT NOT NULL, " + + "service TINYINT NOT NULL, data_hash DataHash NOT NULL, " + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); // NB: Actual data payload stored elsewhere // For the future: data payload should be encrypted, at the very least with transaction's reference as the seed for the encryption key diff --git a/src/repository/hsqldb/transaction/HSQLDBArbitraryTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBArbitraryTransactionRepository.java new file mode 100644 index 00000000..f6067e77 --- /dev/null +++ b/src/repository/hsqldb/transaction/HSQLDBArbitraryTransactionRepository.java @@ -0,0 +1,67 @@ +package repository.hsqldb.transaction; + +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +import data.PaymentData; +import data.transaction.ArbitraryTransactionData; +import data.transaction.ArbitraryTransactionData.DataType; +import data.transaction.TransactionData; +import repository.DataException; +import repository.hsqldb.HSQLDBRepository; +import repository.hsqldb.HSQLDBSaver; + +public class HSQLDBArbitraryTransactionRepository extends HSQLDBTransactionRepository { + + public HSQLDBArbitraryTransactionRepository(HSQLDBRepository repository) { + this.repository = repository; + } + + TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException { + try { + ResultSet rs = this.repository.checkedExecute("SELECT sender, version, service, data_hash from ArbitraryTransactions WHERE signature = ?", + signature); + if (rs == null) + return null; + + byte[] senderPublicKey = rs.getBytes(1); + int version = rs.getInt(2); + int service = rs.getInt(3); + byte[] dataHash = rs.getBytes(4); + + List payments = this.getPaymentsFromSignature(signature); + + return new ArbitraryTransactionData(version, senderPublicKey, service, dataHash, DataType.DATA_HASH, payments, fee, timestamp, reference, + signature); + } catch (SQLException e) { + throw new DataException("Unable to fetch arbitrary transaction from repository", e); + } + } + + @Override + public void save(TransactionData transactionData) throws DataException { + ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) transactionData; + + // Refuse to store raw data in the repository - it needs to be saved elsewhere! + if (arbitraryTransactionData.getDataType() != DataType.DATA_HASH) + throw new DataException("Refusing to save arbitrary transaction data into repository"); + + HSQLDBSaver saveHelper = new HSQLDBSaver("ArbitraryTransactions"); + + saveHelper.bind("signature", arbitraryTransactionData.getSignature()).bind("sender", arbitraryTransactionData.getSenderPublicKey()) + .bind("version", arbitraryTransactionData.getVersion()).bind("service", arbitraryTransactionData.getService()).bind("data_hash", arbitraryTransactionData.getData()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save arbitrary transaction into repository", e); + } + + if (arbitraryTransactionData.getVersion() != 1) + // Save payments. If this fails then it is the caller's responsibility to catch the DataException as the underlying transaction will have been lost. + this.savePayments(transactionData.getSignature(), arbitraryTransactionData.getPayments()); + } + +} diff --git a/src/repository/hsqldb/transaction/HSQLDBCreatePollTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBCreatePollTransactionRepository.java index 24622d5d..7086fe46 100644 --- a/src/repository/hsqldb/transaction/HSQLDBCreatePollTransactionRepository.java +++ b/src/repository/hsqldb/transaction/HSQLDBCreatePollTransactionRepository.java @@ -29,7 +29,8 @@ public class HSQLDBCreatePollTransactionRepository extends HSQLDBTransactionRepo String pollName = rs.getString(2); String description = rs.getString(3); - rs = this.repository.checkedExecute("SELECT option_name FROM CreatePollTransactionOptions where signature = ?", signature); + rs = this.repository.checkedExecute("SELECT option_name FROM CreatePollTransactionOptions where signature = ? ORDER BY option_index ASC", + signature); if (rs == null) return null; @@ -65,10 +66,14 @@ public class HSQLDBCreatePollTransactionRepository extends HSQLDBTransactionRepo } // Now attempt to save poll options - for (PollOptionData pollOptionData : createPollTransactionData.getPollOptions()) { + List pollOptions = createPollTransactionData.getPollOptions(); + for (int optionIndex = 0; optionIndex < pollOptions.size(); ++optionIndex) { + PollOptionData pollOptionData = pollOptions.get(optionIndex); + HSQLDBSaver optionSaveHelper = new HSQLDBSaver("CreatePollTransactionOptions"); - optionSaveHelper.bind("signature", createPollTransactionData.getSignature()).bind("option_name", pollOptionData.getOptionName()); + optionSaveHelper.bind("signature", createPollTransactionData.getSignature()).bind("option_name", pollOptionData.getOptionName()) + .bind("option_index", optionIndex); try { optionSaveHelper.execute(this.repository); diff --git a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index 2182ff7c..29f891f2 100644 --- a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -28,6 +28,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { private HSQLDBBuyNameTransactionRepository buyNameTransactionRepository; private HSQLDBCreatePollTransactionRepository createPollTransactionRepository; private HSQLDBVoteOnPollTransactionRepository voteOnPollTransactionRepository; + private HSQLDBArbitraryTransactionRepository arbitraryTransactionRepository; private HSQLDBIssueAssetTransactionRepository issueAssetTransactionRepository; private HSQLDBTransferAssetTransactionRepository transferAssetTransactionRepository; private HSQLDBCreateOrderTransactionRepository createOrderTransactionRepository; @@ -46,6 +47,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { this.buyNameTransactionRepository = new HSQLDBBuyNameTransactionRepository(repository); this.createPollTransactionRepository = new HSQLDBCreatePollTransactionRepository(repository); this.voteOnPollTransactionRepository = new HSQLDBVoteOnPollTransactionRepository(repository); + this.arbitraryTransactionRepository = new HSQLDBArbitraryTransactionRepository(repository); this.issueAssetTransactionRepository = new HSQLDBIssueAssetTransactionRepository(repository); this.transferAssetTransactionRepository = new HSQLDBTransferAssetTransactionRepository(repository); this.createOrderTransactionRepository = new HSQLDBCreateOrderTransactionRepository(repository); @@ -123,6 +125,9 @@ public class HSQLDBTransactionRepository implements TransactionRepository { case VOTE_ON_POLL: return this.voteOnPollTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + case ARBITRARY: + return this.arbitraryTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); + case ISSUE_ASSET: return this.issueAssetTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee); @@ -273,6 +278,10 @@ public class HSQLDBTransactionRepository implements TransactionRepository { this.voteOnPollTransactionRepository.save(transactionData); break; + case ARBITRARY: + this.arbitraryTransactionRepository.save(transactionData); + break; + case ISSUE_ASSET: this.issueAssetTransactionRepository.save(transactionData); break; diff --git a/src/settings/Settings.java b/src/settings/Settings.java index 2e64fea0..be62d2a9 100644 --- a/src/settings/Settings.java +++ b/src/settings/Settings.java @@ -18,6 +18,7 @@ public class Settings { private static Settings instance; private long genesisTimestamp = GenesisBlock.GENESIS_TIMESTAMP; private int maxBytePerFee = 1024; + private String userpath = ""; // Constants private static final String SETTINGS_FILENAME = "settings.json"; @@ -66,6 +67,8 @@ public class Settings { } process(settingsJSON); + + this.userpath = path; break; } while (true); } catch (IOException | ClassCastException e) { @@ -115,4 +118,8 @@ public class Settings { return this.genesisTimestamp; } + public String getUserpath() { + return this.userpath; + } + } diff --git a/src/test/Common.java b/src/test/Common.java index e7d44d2a..fc55c4a3 100644 --- a/src/test/Common.java +++ b/src/test/Common.java @@ -10,7 +10,7 @@ import repository.hsqldb.HSQLDBRepositoryFactory; public class Common { - public static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true;close_result=true;sql.strict_exec=true;sql.enforce_names=true;sql.syntax_mys=true"; + public static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true;close_result=true;sql.strict_exec=true;sql.enforce_names=true;sql.syntax_mys=true;sql.pad_space=false"; @BeforeClass public static void setRepository() throws DataException { diff --git a/src/test/CompatibilityTests.java b/src/test/CompatibilityTests.java new file mode 100644 index 00000000..3c630697 --- /dev/null +++ b/src/test/CompatibilityTests.java @@ -0,0 +1,67 @@ +package test; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import com.google.common.hash.HashCode; + +import data.transaction.TransactionData; +import qora.transaction.CreateOrderTransaction; +import qora.transaction.CreatePollTransaction; +import qora.transaction.IssueAssetTransaction; +import transform.TransformationException; +import transform.transaction.TransactionTransformer; + +public class CompatibilityTests { + + @Test + public void testCreateOrderTransactionSignature() throws TransformationException { + // 4EsGzQ87rXqXw2nic8LiihGCrM5iNErK53u9TRo2AJv4FWWyCK7bUKrCmswnrBbkB7Dsk7wfzi9hM2TGGqm6LVpd + byte[] rawTx = HashCode + .fromString("0000000d" + "000001489be3ef8e" + + "10b52b229c73afb40a56df4f1c9f65072041011cf9ae25a053397d9fc5578bc8f1412eb404de4e318e24302863fc52889eb848af65a6b17cfc964267388f5802" + + "bf497fa72ed16894f3acab6c4a101fd8b5fd42f0420dad45474388d5492d38d0" + "0000000000000000" + "0000000000000001" + + "000000000000000005f5e100" + "000000000000000005f5e100" + "0000000005f5e100" + + "a2025bfde5c90254e16150db6aef6189bb2856df51940b6a15b1d5f174451236062c982af4da3429941337abc7002a862782fb9c726bfc95aea31e30bf66a502") + .asBytes(); + + TransactionData transactionData = TransactionTransformer.fromBytes(rawTx); + + CreateOrderTransaction transaction = new CreateOrderTransaction(null, transactionData); + assertTrue(transaction.isSignatureValid()); + } + + @Test + public void testCreatePollTransactionSignature() throws TransformationException { + // 5xo8YxDVTFVR1pdmtxYkRbq3PkcKVttyH7wCVAfgqokDMKE1XW6zrqFgJG8vRQz9qi5r8cqBoSgFKLnQRoSyzpgF + byte[] rawTx = HashCode + .fromString("00000008" + "00000146d4237f03" + + "c201817ee2d4363801b63cbe154f6796719feb5a9673758dfda7b5e616cddd1263bbb75ce6a14ca116abe2d34ea68f353379d0c0d48da62180677053792f3b00" + + "ef893a99782612754157d868fc4194577cca8ca5dd264c90855829f0e4bbec3a" + "3a91655f3c70d7a38980b449ccf7acd84de41f99dae6215ed5" + "0000000a" + + "746869736973706f6c6c" + "00000004" + "74657374" + "00000002" + "00000011" + "546869732069732073706f6e6765626f62" + "00000000" + + "0000000f" + "54686973206973207061747269636b" + "00000000" + "0000000005f5e100" + + "f82f0c7421333c2cae5d0d0200e7f4726cda60baecad4ba067c7da17c681e2fb20612991f75763791b228c258f79ec2ecc40788fdda71b8f11a9032417ec7e08") + .asBytes(); + + TransactionData transactionData = TransactionTransformer.fromBytes(rawTx); + + CreatePollTransaction transaction = new CreatePollTransaction(null, transactionData); + assertTrue(transaction.isSignatureValid()); + } + + @Test + public void testIssueAssetTransactionSignature() throws TransformationException { + // 3JeJ8yGnG8RCQH51S2qYJT5nfbokjHnBmM7KZsj61HPRy8K3ZWkGHh99QQ6HbRHxnknnjjAsffHRaeca1ap3tcFv + byte[] rawTx = HashCode + .fromString( + "0000000b000001489376bea34d4cbdb644be00b5848a2beeee087fdb98de49a010e686de9540f7d83720cdd182ca6efd1a6225f72f2821ed8a19f236002aef33afa4e2e419fe641c2bc4800a8dd3440f3ce0526c924f2cc15f3fdc1afcf4d57e4502c7a13bfed9851e81abc93a6a24ae1a453205b39d0c3bd24fb5eb675cd199e7cb5b316c00000003787878000000117878787878787878787878787878787878000000000000006400733fa8fa762c404ca1ddd799e93cc8ea292cd9fdd84d5c8b094050d4f576ea56071055f9fe337bf610624514f673e66462f8719759242b5635f19da088b311050000000005f5e100733fa8fa762c404ca1ddd799e93cc8ea292cd9fdd84d5c8b094050d4f576ea56071055f9fe337bf610624514f673e66462f8719759242b5635f19da088b31105") + .asBytes(); + + TransactionData transactionData = TransactionTransformer.fromBytes(rawTx); + + IssueAssetTransaction transaction = new IssueAssetTransaction(null, transactionData); + assertTrue(transaction.isSignatureValid()); + } + +} diff --git a/src/test/TransactionTests.java b/src/test/TransactionTests.java index 050ce35b..9365f91f 100644 --- a/src/test/TransactionTests.java +++ b/src/test/TransactionTests.java @@ -148,7 +148,7 @@ public class TransactionTests { PaymentTransactionData paymentTransactionData = new PaymentTransactionData(sender.getPublicKey(), recipient, amount, fee, timestamp, reference); Transaction paymentTransaction = new PaymentTransaction(repository, paymentTransactionData); - paymentTransaction.calcSignature(sender); + paymentTransaction.sign(sender); return paymentTransaction; } @@ -166,7 +166,7 @@ public class TransactionTests { reference); Transaction paymentTransaction = new PaymentTransaction(repository, paymentTransactionData); - paymentTransaction.calcSignature(sender); + paymentTransaction.sign(sender); assertTrue(paymentTransaction.isSignatureValid()); assertEquals(ValidationResult.OK, paymentTransaction.isValid()); @@ -227,7 +227,7 @@ public class TransactionTests { timestamp, reference); Transaction registerNameTransaction = new RegisterNameTransaction(repository, registerNameTransactionData); - registerNameTransaction.calcSignature(sender); + registerNameTransaction.sign(sender); assertTrue(registerNameTransaction.isSignatureValid()); assertEquals(ValidationResult.OK, registerNameTransaction.isValid()); @@ -283,7 +283,7 @@ public class TransactionTests { nameReference, fee, timestamp, reference); Transaction updateNameTransaction = new UpdateNameTransaction(repository, updateNameTransactionData); - updateNameTransaction.calcSignature(sender); + updateNameTransaction.sign(sender); assertTrue(updateNameTransaction.isSignatureValid()); assertEquals(ValidationResult.OK, updateNameTransaction.isValid()); @@ -328,7 +328,7 @@ public class TransactionTests { SellNameTransactionData sellNameTransactionData = new SellNameTransactionData(sender.getPublicKey(), name, amount, fee, timestamp, reference); Transaction sellNameTransaction = new SellNameTransaction(repository, sellNameTransactionData); - sellNameTransaction.calcSignature(sender); + sellNameTransaction.sign(sender); assertTrue(sellNameTransaction.isSignatureValid()); assertEquals(ValidationResult.OK, sellNameTransaction.isValid()); @@ -379,7 +379,7 @@ public class TransactionTests { CancelSellNameTransactionData cancelSellNameTransactionData = new CancelSellNameTransactionData(sender.getPublicKey(), name, fee, timestamp, reference); Transaction cancelSellNameTransaction = new CancelSellNameTransaction(repository, cancelSellNameTransactionData); - cancelSellNameTransaction.calcSignature(sender); + cancelSellNameTransaction.sign(sender); assertTrue(cancelSellNameTransaction.isSignatureValid()); assertEquals(ValidationResult.OK, cancelSellNameTransaction.isValid()); @@ -445,7 +445,7 @@ public class TransactionTests { nameReference, fee, timestamp, buyersReference); Transaction buyNameTransaction = new BuyNameTransaction(repository, buyNameTransactionData); - buyNameTransaction.calcSignature(buyer); + buyNameTransaction.sign(buyer); assertTrue(buyNameTransaction.isSignatureValid()); assertEquals(ValidationResult.OK, buyNameTransaction.isValid()); @@ -498,7 +498,7 @@ public class TransactionTests { description, pollOptions, fee, timestamp, reference); Transaction createPollTransaction = new CreatePollTransaction(repository, createPollTransactionData); - createPollTransaction.calcSignature(sender); + createPollTransaction.sign(sender); assertTrue(createPollTransaction.isSignatureValid()); assertEquals(ValidationResult.OK, createPollTransaction.isValid()); @@ -552,7 +552,7 @@ public class TransactionTests { reference); Transaction voteOnPollTransaction = new VoteOnPollTransaction(repository, voteOnPollTransactionData); - voteOnPollTransaction.calcSignature(sender); + voteOnPollTransaction.sign(sender); assertTrue(voteOnPollTransaction.isSignatureValid()); if (optionIndex == pollOptionsSize) { @@ -624,7 +624,7 @@ public class TransactionTests { quantity, isDivisible, fee, timestamp, reference); Transaction issueAssetTransaction = new IssueAssetTransaction(repository, issueAssetTransactionData); - issueAssetTransaction.calcSignature(sender); + issueAssetTransaction.sign(sender); assertTrue(issueAssetTransaction.isSignatureValid()); assertEquals(ValidationResult.OK, issueAssetTransaction.isValid()); @@ -714,7 +714,7 @@ public class TransactionTests { assetId, fee, timestamp, reference); Transaction transferAssetTransaction = new TransferAssetTransaction(repository, transferAssetTransactionData); - transferAssetTransaction.calcSignature(sender); + transferAssetTransaction.sign(sender); assertTrue(transferAssetTransaction.isSignatureValid()); assertEquals(ValidationResult.OK, transferAssetTransaction.isValid()); @@ -818,7 +818,7 @@ public class TransactionTests { CreateOrderTransactionData createOrderTransactionData = new CreateOrderTransactionData(buyer.getPublicKey(), haveAssetId, wantAssetId, amount, price, fee, timestamp, buyersReference); Transaction createOrderTransaction = new CreateOrderTransaction(this.repository, createOrderTransactionData); - createOrderTransaction.calcSignature(buyer); + createOrderTransaction.sign(buyer); assertTrue(createOrderTransaction.isSignatureValid()); assertEquals(ValidationResult.OK, createOrderTransaction.isValid()); @@ -899,7 +899,7 @@ public class TransactionTests { CancelOrderTransactionData cancelOrderTransactionData = new CancelOrderTransactionData(buyer.getPublicKey(), orderId, fee, timestamp, buyersReference); Transaction cancelOrderTransaction = new CancelOrderTransaction(this.repository, cancelOrderTransactionData); - cancelOrderTransaction.calcSignature(buyer); + cancelOrderTransaction.sign(buyer); assertTrue(cancelOrderTransaction.isSignatureValid()); assertEquals(ValidationResult.OK, cancelOrderTransaction.isValid()); @@ -970,7 +970,7 @@ public class TransactionTests { CreateOrderTransactionData createOrderTransactionData = new CreateOrderTransactionData(sender.getPublicKey(), haveAssetId, wantAssetId, amount, price, fee, timestamp, reference); Transaction createOrderTransaction = new CreateOrderTransaction(this.repository, createOrderTransactionData); - createOrderTransaction.calcSignature(sender); + createOrderTransaction.sign(sender); assertTrue(createOrderTransaction.isSignatureValid()); assertEquals(ValidationResult.OK, createOrderTransaction.isValid()); @@ -1073,7 +1073,7 @@ public class TransactionTests { MultiPaymentTransactionData multiPaymentTransactionData = new MultiPaymentTransactionData(sender.getPublicKey(), payments, fee, timestamp, reference); Transaction multiPaymentTransaction = new MultiPaymentTransaction(repository, multiPaymentTransactionData); - multiPaymentTransaction.calcSignature(sender); + multiPaymentTransaction.sign(sender); assertTrue(multiPaymentTransaction.isSignatureValid()); assertEquals(ValidationResult.OK, multiPaymentTransaction.isValid()); @@ -1143,7 +1143,7 @@ public class TransactionTests { data, isText, isEncrypted, fee, timestamp, reference); Transaction messageTransaction = new MessageTransaction(repository, messageTransactionData); - messageTransaction.calcSignature(sender); + messageTransaction.sign(sender); assertTrue(messageTransaction.isSignatureValid()); assertEquals(ValidationResult.OK, messageTransaction.isValid()); diff --git a/src/transform/transaction/ArbitraryTransactionTransformer.java b/src/transform/transaction/ArbitraryTransactionTransformer.java new file mode 100644 index 00000000..1f46dac5 --- /dev/null +++ b/src/transform/transaction/ArbitraryTransactionTransformer.java @@ -0,0 +1,155 @@ +package transform.transaction; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; + +import com.google.common.hash.HashCode; +import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; + +import data.transaction.TransactionData; +import qora.account.PublicKeyAccount; +import qora.transaction.ArbitraryTransaction; +import data.PaymentData; +import data.transaction.ArbitraryTransactionData; +import data.transaction.ArbitraryTransactionData.DataType; +import transform.PaymentTransformer; +import transform.TransformationException; +import utils.Base58; +import utils.Serialization; + +public class ArbitraryTransactionTransformer extends TransactionTransformer { + + // Property lengths + private static final int SENDER_LENGTH = PUBLIC_KEY_LENGTH; + private static final int SERVICE_LENGTH = INT_LENGTH; + private static final int DATA_SIZE_LENGTH = INT_LENGTH; + private static final int PAYMENTS_COUNT_LENGTH = INT_LENGTH; + + private static final int TYPELESS_DATALESS_LENGTH_V1 = BASE_TYPELESS_LENGTH + SENDER_LENGTH + SERVICE_LENGTH + DATA_SIZE_LENGTH; + private static final int TYPELESS_DATALESS_LENGTH_V3 = BASE_TYPELESS_LENGTH + SENDER_LENGTH + PAYMENTS_COUNT_LENGTH + SERVICE_LENGTH + DATA_SIZE_LENGTH; + + static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + long timestamp = byteBuffer.getLong(); + + int version = ArbitraryTransaction.getVersionByTimestamp(timestamp); + + byte[] reference = new byte[REFERENCE_LENGTH]; + byteBuffer.get(reference); + + byte[] senderPublicKey = Serialization.deserializePublicKey(byteBuffer); + + // V3+ allows payments + List payments = null; + if (version != 1) { + int paymentsCount = byteBuffer.getInt(); + + payments = new ArrayList(); + for (int i = 0; i < paymentsCount; ++i) + payments.add(PaymentTransformer.fromByteBuffer(byteBuffer)); + } + + int service = byteBuffer.getInt(); + + int dataSize = byteBuffer.getInt(); + // Don't allow invalid dataSize here to avoid run-time issues + if (dataSize > ArbitraryTransaction.MAX_DATA_SIZE) + throw new TransformationException("ArbitraryTransaction data size too large"); + + byte[] data = new byte[dataSize]; + byteBuffer.get(data); + + BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); + + byte[] signature = new byte[SIGNATURE_LENGTH]; + byteBuffer.get(signature); + + return new ArbitraryTransactionData(version, senderPublicKey, service, data, DataType.RAW_DATA, payments, fee, timestamp, reference, signature); + } + + public static int getDataLength(TransactionData transactionData) throws TransformationException { + ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) transactionData; + + if (arbitraryTransactionData.getVersion() == 1) + return TYPE_LENGTH + TYPELESS_DATALESS_LENGTH_V1 + arbitraryTransactionData.getData().length; + else + return TYPE_LENGTH + TYPELESS_DATALESS_LENGTH_V3 + arbitraryTransactionData.getData().length + + arbitraryTransactionData.getPayments().size() * PaymentTransformer.getDataLength(); + } + + public static byte[] toBytes(TransactionData transactionData) throws TransformationException { + try { + ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(arbitraryTransactionData.getType().value)); + bytes.write(Longs.toByteArray(arbitraryTransactionData.getTimestamp())); + bytes.write(arbitraryTransactionData.getReference()); + + bytes.write(arbitraryTransactionData.getSenderPublicKey()); + + if (arbitraryTransactionData.getVersion() != 1) { + List payments = arbitraryTransactionData.getPayments(); + bytes.write(Ints.toByteArray(payments.size())); + + for (PaymentData paymentData : payments) + bytes.write(PaymentTransformer.toBytes(paymentData)); + } + + bytes.write(Ints.toByteArray(arbitraryTransactionData.getService())); + + bytes.write(Ints.toByteArray(arbitraryTransactionData.getData().length)); + bytes.write(arbitraryTransactionData.getData()); + + Serialization.serializeBigDecimal(bytes, arbitraryTransactionData.getFee()); + + if (arbitraryTransactionData.getSignature() != null) + bytes.write(arbitraryTransactionData.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 { + ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) transactionData; + + byte[] senderPublicKey = arbitraryTransactionData.getSenderPublicKey(); + + json.put("version", arbitraryTransactionData.getVersion()); + json.put("sender", PublicKeyAccount.getAddress(senderPublicKey)); + json.put("senderPublicKey", HashCode.fromBytes(senderPublicKey).toString()); + + json.put("service", arbitraryTransactionData.getService()); + json.put("data", Base58.encode(arbitraryTransactionData.getData())); + + if (arbitraryTransactionData.getVersion() != 1) { + List payments = arbitraryTransactionData.getPayments(); + JSONArray paymentsJson = new JSONArray(); + + for (PaymentData paymentData : payments) + paymentsJson.add(PaymentTransformer.toJSON(paymentData)); + + json.put("payments", paymentsJson); + } + } catch (ClassCastException e) { + throw new TransformationException(e); + } + + return json; + } + +} diff --git a/src/transform/transaction/BuyNameTransactionTransformer.java b/src/transform/transaction/BuyNameTransactionTransformer.java index a99d52b0..87ed0966 100644 --- a/src/transform/transaction/BuyNameTransactionTransformer.java +++ b/src/transform/transaction/BuyNameTransactionTransformer.java @@ -30,9 +30,6 @@ public class BuyNameTransactionTransformer extends TransactionTransformer { private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + BUYER_LENGTH + NAME_SIZE_LENGTH + AMOUNT_LENGTH + SELLER_LENGTH; static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { - if (byteBuffer.remaining() < TYPELESS_DATALESS_LENGTH) - throw new TransformationException("Byte data too short for BuyNameTransaction"); - long timestamp = byteBuffer.getLong(); byte[] reference = new byte[REFERENCE_LENGTH]; @@ -42,10 +39,6 @@ public class BuyNameTransactionTransformer extends TransactionTransformer { String name = Serialization.deserializeSizedString(byteBuffer, Name.MAX_NAME_SIZE); - // Still need to make sure there are enough bytes left for remaining fields - if (byteBuffer.remaining() < AMOUNT_LENGTH + SELLER_LENGTH + FEE_LENGTH + SIGNATURE_LENGTH) - throw new TransformationException("Byte data too short for BuyNameTransaction"); - BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer); String seller = Serialization.deserializeAddress(byteBuffer); diff --git a/src/transform/transaction/CancelOrderTransactionTransformer.java b/src/transform/transaction/CancelOrderTransactionTransformer.java index 6f02945d..8946c140 100644 --- a/src/transform/transaction/CancelOrderTransactionTransformer.java +++ b/src/transform/transaction/CancelOrderTransactionTransformer.java @@ -27,9 +27,6 @@ public class CancelOrderTransactionTransformer extends TransactionTransformer { private static final int TYPELESS_LENGTH = BASE_TYPELESS_LENGTH + CREATOR_LENGTH + ORDER_ID_LENGTH; static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { - if (byteBuffer.remaining() < TYPELESS_LENGTH) - throw new TransformationException("Byte data too short for CancelOrderTransaction"); - long timestamp = byteBuffer.getLong(); byte[] reference = new byte[REFERENCE_LENGTH]; diff --git a/src/transform/transaction/CancelSellNameTransactionTransformer.java b/src/transform/transaction/CancelSellNameTransactionTransformer.java index 542b9c01..fa9330af 100644 --- a/src/transform/transaction/CancelSellNameTransactionTransformer.java +++ b/src/transform/transaction/CancelSellNameTransactionTransformer.java @@ -28,9 +28,6 @@ public class CancelSellNameTransactionTransformer extends TransactionTransformer private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + OWNER_LENGTH + NAME_SIZE_LENGTH; static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { - if (byteBuffer.remaining() < TYPELESS_DATALESS_LENGTH) - throw new TransformationException("Byte data too short for CancelSellNameTransaction"); - long timestamp = byteBuffer.getLong(); byte[] reference = new byte[REFERENCE_LENGTH]; @@ -40,10 +37,6 @@ public class CancelSellNameTransactionTransformer extends TransactionTransformer String name = Serialization.deserializeSizedString(byteBuffer, Name.MAX_NAME_SIZE); - // Still need to make sure there are enough bytes left for remaining fields - if (byteBuffer.remaining() < FEE_LENGTH + SIGNATURE_LENGTH) - throw new TransformationException("Byte data too short for CancelSellNameTransaction"); - BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); byte[] signature = new byte[SIGNATURE_LENGTH]; diff --git a/src/transform/transaction/CreateOrderTransactionTransformer.java b/src/transform/transaction/CreateOrderTransactionTransformer.java index 1ef5a7d4..d8d37827 100644 --- a/src/transform/transaction/CreateOrderTransactionTransformer.java +++ b/src/transform/transaction/CreateOrderTransactionTransformer.java @@ -13,6 +13,7 @@ import com.google.common.primitives.Longs; import data.transaction.TransactionData; import qora.account.PublicKeyAccount; +import qora.block.BlockChain; import data.transaction.CreateOrderTransactionData; import transform.TransformationException; import utils.Serialization; @@ -27,9 +28,6 @@ public class CreateOrderTransactionTransformer extends TransactionTransformer { private static final int TYPELESS_LENGTH = BASE_TYPELESS_LENGTH + CREATOR_LENGTH + (ASSET_ID_LENGTH + AMOUNT_LENGTH) * 2; static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { - if (byteBuffer.remaining() < TYPELESS_LENGTH) - throw new TransformationException("Byte data too short for CreateOrderTransaction"); - long timestamp = byteBuffer.getLong(); byte[] reference = new byte[REFERENCE_LENGTH]; @@ -38,9 +36,11 @@ public class CreateOrderTransactionTransformer extends TransactionTransformer { byte[] creatorPublicKey = Serialization.deserializePublicKey(byteBuffer); long haveAssetId = byteBuffer.getLong(); + long wantAssetId = byteBuffer.getLong(); BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer, AMOUNT_LENGTH); + BigDecimal price = Serialization.deserializeBigDecimal(byteBuffer, AMOUNT_LENGTH); BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); @@ -82,6 +82,43 @@ public class CreateOrderTransactionTransformer extends TransactionTransformer { } } + /** + * In Qora v1, the bytes used for verification have mangled price so we need to test for v1-ness and adjust the bytes accordingly. + * + * @param transactionData + * @return byte[] + * @throws TransformationException + */ + public static byte[] toBytesForSigningImpl(TransactionData transactionData) throws TransformationException { + if (transactionData.getTimestamp() >= BlockChain.getCreateOrderV2Timestamp()) + return TransactionTransformer.toBytesForSigningImpl(transactionData); + + // Special v1 version + try { + CreateOrderTransactionData createOrderTransactionData = (CreateOrderTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(createOrderTransactionData.getType().value)); + bytes.write(Longs.toByteArray(createOrderTransactionData.getTimestamp())); + bytes.write(createOrderTransactionData.getReference()); + + bytes.write(createOrderTransactionData.getCreatorPublicKey()); + bytes.write(Longs.toByteArray(createOrderTransactionData.getHaveAssetId())); + bytes.write(Longs.toByteArray(createOrderTransactionData.getWantAssetId())); + Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getAmount(), AMOUNT_LENGTH); + + // This is the crucial difference + Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getPrice(), FEE_LENGTH); + + Serialization.serializeBigDecimal(bytes, createOrderTransactionData.getFee()); + + 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); diff --git a/src/transform/transaction/CreatePollTransactionTransformer.java b/src/transform/transaction/CreatePollTransactionTransformer.java index 3338a018..de18d78a 100644 --- a/src/transform/transaction/CreatePollTransactionTransformer.java +++ b/src/transform/transaction/CreatePollTransactionTransformer.java @@ -19,6 +19,8 @@ import data.transaction.CreatePollTransactionData; import data.transaction.TransactionData; import data.voting.PollOptionData; import qora.account.PublicKeyAccount; +import qora.block.BlockChain; +import qora.transaction.Transaction.TransactionType; import qora.voting.Poll; import transform.TransformationException; import utils.Serialization; @@ -36,9 +38,6 @@ public class CreatePollTransactionTransformer extends TransactionTransformer { + OPTIONS_SIZE_LENGTH; static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { - if (byteBuffer.remaining() < TYPELESS_DATALESS_LENGTH) - throw new TransformationException("Byte data too short for CreatePollTransaction"); - long timestamp = byteBuffer.getLong(); byte[] reference = new byte[REFERENCE_LENGTH]; @@ -49,11 +48,8 @@ public class CreatePollTransactionTransformer extends TransactionTransformer { String owner = Serialization.deserializeAddress(byteBuffer); String pollName = Serialization.deserializeSizedString(byteBuffer, Poll.MAX_NAME_SIZE); - String description = Serialization.deserializeSizedString(byteBuffer, Poll.MAX_DESCRIPTION_SIZE); - // Make sure there are enough bytes left for poll options - if (byteBuffer.remaining() < OPTIONS_SIZE_LENGTH) - throw new TransformationException("Byte data too short for CreatePollTransaction"); + String description = Serialization.deserializeSizedString(byteBuffer, Poll.MAX_DESCRIPTION_SIZE); int optionsCount = byteBuffer.getInt(); if (optionsCount < 1 || optionsCount > Poll.MAX_OPTIONS) @@ -64,11 +60,14 @@ public class CreatePollTransactionTransformer extends TransactionTransformer { String optionName = Serialization.deserializeSizedString(byteBuffer, Poll.MAX_NAME_SIZE); pollOptions.add(new PollOptionData(optionName)); - } - // Still need to make sure there are enough bytes left for remaining fields - if (byteBuffer.remaining() < FEE_LENGTH + SIGNATURE_LENGTH) - throw new TransformationException("Byte data too short for CreatePollTransaction"); + // V1 only: voter count also present + if (timestamp < BlockChain.getCreatePollV2Timestamp()) { + int voterCount = byteBuffer.getInt(); + if (voterCount != 0) + throw new TransformationException("Unexpected voter count in byte data for CreatePollTransaction"); + } + } BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); @@ -85,9 +84,15 @@ public class CreatePollTransactionTransformer extends TransactionTransformer { + Utf8.encodedLength(createPollTransactionData.getDescription()); // Add lengths for each poll options - for (PollOptionData pollOptionData : createPollTransactionData.getPollOptions()) + for (PollOptionData pollOptionData : createPollTransactionData.getPollOptions()) { + // option-string-length, option-string dataLength += INT_LENGTH + Utf8.encodedLength(pollOptionData.getOptionName()); + if (transactionData.getTimestamp() < BlockChain.getCreatePollV2Timestamp()) + // v1 only: voter-count (should always be zero) + dataLength += INT_LENGTH; + } + return dataLength; } @@ -98,10 +103,13 @@ public class CreatePollTransactionTransformer extends TransactionTransformer { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); bytes.write(Ints.toByteArray(createPollTransactionData.getType().value)); + bytes.write(Longs.toByteArray(createPollTransactionData.getTimestamp())); + bytes.write(createPollTransactionData.getReference()); bytes.write(createPollTransactionData.getCreatorPublicKey()); + Serialization.serializeAddress(bytes, createPollTransactionData.getOwner()); Serialization.serializeSizedString(bytes, createPollTransactionData.getPollName()); Serialization.serializeSizedString(bytes, createPollTransactionData.getDescription()); @@ -109,9 +117,16 @@ public class CreatePollTransactionTransformer extends TransactionTransformer { List pollOptions = createPollTransactionData.getPollOptions(); bytes.write(Ints.toByteArray(pollOptions.size())); - for (PollOptionData pollOptionData : pollOptions) + for (PollOptionData pollOptionData : pollOptions) { Serialization.serializeSizedString(bytes, pollOptionData.getOptionName()); + if (transactionData.getTimestamp() < BlockChain.getCreatePollV2Timestamp()) { + // In v1, CreatePollTransaction uses Poll.toBytes which serializes voters too. + // Zero voters as this is a new poll. + bytes.write(Ints.toByteArray(0)); + } + } + Serialization.serializeBigDecimal(bytes, createPollTransactionData.getFee()); if (createPollTransactionData.getSignature() != null) @@ -123,6 +138,30 @@ public class CreatePollTransactionTransformer extends TransactionTransformer { } } + /** + * In Qora v1, the bytes used for verification have transaction type set to REGISTER_NAME_TRANSACTION so we need to test for v1-ness and adjust the bytes + * accordingly. + * + * @param transactionData + * @return byte[] + * @throws TransformationException + */ + public static byte[] toBytesForSigningImpl(TransactionData transactionData) throws TransformationException { + byte[] bytes = TransactionTransformer.toBytesForSigningImpl(transactionData); + + if (transactionData.getTimestamp() >= BlockChain.getCreatePollV2Timestamp()) + return bytes; + + // Special v1 version + + // Replace transaction type with incorrect Register Name value + System.arraycopy(Ints.toByteArray(TransactionType.REGISTER_NAME.value), 0, bytes, 0, TransactionTransformer.INT_LENGTH); + + System.out.println(HashCode.fromBytes(bytes).toString()); + + return bytes; + } + @SuppressWarnings("unchecked") public static JSONObject toJSON(TransactionData transactionData) throws TransformationException { JSONObject json = TransactionTransformer.getBaseJSON(transactionData); diff --git a/src/transform/transaction/GenesisTransactionTransformer.java b/src/transform/transaction/GenesisTransactionTransformer.java index 2cf1132d..208c39ea 100644 --- a/src/transform/transaction/GenesisTransactionTransformer.java +++ b/src/transform/transaction/GenesisTransactionTransformer.java @@ -25,11 +25,10 @@ public class GenesisTransactionTransformer extends TransactionTransformer { private static final int TYPELESS_LENGTH = TIMESTAMP_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH; static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { - if (byteBuffer.remaining() < TYPELESS_LENGTH) - throw new TransformationException("Byte data too short for GenesisTransaction"); - long timestamp = byteBuffer.getLong(); + String recipient = Serialization.deserializeAddress(byteBuffer); + BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer); return new GenesisTransactionData(recipient, amount, timestamp); diff --git a/src/transform/transaction/IssueAssetTransactionTransformer.java b/src/transform/transaction/IssueAssetTransactionTransformer.java index b8e29b95..dd4bf959 100644 --- a/src/transform/transaction/IssueAssetTransactionTransformer.java +++ b/src/transform/transaction/IssueAssetTransactionTransformer.java @@ -4,6 +4,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.math.BigDecimal; import java.nio.ByteBuffer; +import java.util.Arrays; import org.json.simple.JSONObject; @@ -14,6 +15,7 @@ import com.google.common.primitives.Longs; import data.transaction.TransactionData; import qora.account.PublicKeyAccount; +import qora.block.BlockChain; import qora.transaction.IssueAssetTransaction; import data.transaction.IssueAssetTransactionData; import transform.TransformationException; @@ -28,32 +30,34 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer { private static final int DESCRIPTION_SIZE_LENGTH = INT_LENGTH; private static final int QUANTITY_LENGTH = LONG_LENGTH; private static final int IS_DIVISIBLE_LENGTH = BOOLEAN_LENGTH; + private static final int ASSET_REFERENCE_LENGTH = REFERENCE_LENGTH; private static final int TYPELESS_LENGTH = BASE_TYPELESS_LENGTH + ISSUER_LENGTH + OWNER_LENGTH + NAME_SIZE_LENGTH + DESCRIPTION_SIZE_LENGTH + QUANTITY_LENGTH + IS_DIVISIBLE_LENGTH; static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { - if (byteBuffer.remaining() < TYPELESS_LENGTH) - throw new TransformationException("Byte data too short for IssueAssetTransaction"); - long timestamp = byteBuffer.getLong(); byte[] reference = new byte[REFERENCE_LENGTH]; byteBuffer.get(reference); byte[] issuerPublicKey = Serialization.deserializePublicKey(byteBuffer); + String owner = Serialization.deserializeAddress(byteBuffer); String assetName = Serialization.deserializeSizedString(byteBuffer, IssueAssetTransaction.MAX_NAME_SIZE); + String description = Serialization.deserializeSizedString(byteBuffer, IssueAssetTransaction.MAX_DESCRIPTION_SIZE); - // Still need to make sure there are enough bytes left for remaining fields - if (byteBuffer.remaining() < QUANTITY_LENGTH + IS_DIVISIBLE_LENGTH + FEE_LENGTH + SIGNATURE_LENGTH) - throw new TransformationException("Byte data too short for IssueAssetTransaction"); - long quantity = byteBuffer.getLong(); + boolean isDivisible = byteBuffer.get() != 0; + byte[] assetReference = new byte[ASSET_REFERENCE_LENGTH]; + // In v1, IssueAssetTransaction uses Asset.parse which also deserializes reference. + if (timestamp < BlockChain.getIssueAssetV2Timestamp()) + byteBuffer.get(assetReference); + BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); byte[] signature = new byte[SIGNATURE_LENGTH]; @@ -65,8 +69,14 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer { public static int getDataLength(TransactionData transactionData) throws TransformationException { IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) transactionData; - return TYPE_LENGTH + TYPELESS_LENGTH + Utf8.encodedLength(issueAssetTransactionData.getAssetName()) + int dataLength = TYPE_LENGTH + TYPELESS_LENGTH + Utf8.encodedLength(issueAssetTransactionData.getAssetName()) + Utf8.encodedLength(issueAssetTransactionData.getDescription()); + + // In v1, IssueAssetTransaction uses Asset.toBytes which also serializes reference. + if (transactionData.getTimestamp() < BlockChain.getIssueAssetV2Timestamp()) + dataLength += ASSET_REFERENCE_LENGTH; + + return dataLength; } public static byte[] toBytes(TransactionData transactionData) throws TransformationException { @@ -80,12 +90,19 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer { bytes.write(issueAssetTransactionData.getReference()); bytes.write(issueAssetTransactionData.getIssuerPublicKey()); + Serialization.serializeAddress(bytes, issueAssetTransactionData.getOwner()); + Serialization.serializeSizedString(bytes, issueAssetTransactionData.getAssetName()); Serialization.serializeSizedString(bytes, issueAssetTransactionData.getDescription()); + bytes.write(Longs.toByteArray(issueAssetTransactionData.getQuantity())); bytes.write((byte) (issueAssetTransactionData.getIsDivisible() ? 1 : 0)); + // In v1, IssueAssetTransaction uses Asset.toBytes which also serializes reference. + if (transactionData.getTimestamp() < BlockChain.getIssueAssetV2Timestamp()) + bytes.write(issueAssetTransactionData.getSignature()); + Serialization.serializeBigDecimal(bytes, issueAssetTransactionData.getFee()); if (issueAssetTransactionData.getSignature() != null) @@ -97,6 +114,30 @@ public class IssueAssetTransactionTransformer extends TransactionTransformer { } } + /** + * In Qora v1, the bytes used for verification have transaction type set to REGISTER_NAME_TRANSACTION so we need to test for v1-ness and adjust the bytes + * accordingly. + * + * @param transactionData + * @return byte[] + * @throws TransformationException + */ + public static byte[] toBytesForSigningImpl(TransactionData transactionData) throws TransformationException { + byte[] bytes = TransactionTransformer.toBytesForSigningImpl(transactionData); + + if (transactionData.getTimestamp() >= BlockChain.getIssueAssetV2Timestamp()) + return bytes; + + // Special v1 version + + // Zero duplicate signature/reference + int start = bytes.length - TransactionTransformer.SIGNATURE_LENGTH - TransactionTransformer.BIG_DECIMAL_LENGTH; + int end = start + TransactionTransformer.SIGNATURE_LENGTH; + Arrays.fill(bytes, start, end, (byte) 0); + + return bytes; + } + @SuppressWarnings("unchecked") public static JSONObject toJSON(TransactionData transactionData) throws TransformationException { JSONObject json = TransactionTransformer.getBaseJSON(transactionData); diff --git a/src/transform/transaction/MessageTransactionTransformer.java b/src/transform/transaction/MessageTransactionTransformer.java index c3f60801..2363e700 100644 --- a/src/transform/transaction/MessageTransactionTransformer.java +++ b/src/transform/transaction/MessageTransactionTransformer.java @@ -37,22 +37,15 @@ public class MessageTransactionTransformer extends TransactionTransformer { + DATA_SIZE_LENGTH + IS_TEXT_LENGTH + IS_ENCRYPTED_LENGTH; static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { - if (byteBuffer.remaining() < TYPELESS_DATALESS_LENGTH_V1) - throw new TransformationException("Byte data too short for MessageTransaction"); - long timestamp = byteBuffer.getLong(); + int version = MessageTransaction.getVersionByTimestamp(timestamp); - int minimumRemaining = version == 1 ? TYPELESS_DATALESS_LENGTH_V1 : TYPELESS_DATALESS_LENGTH_V3; - minimumRemaining -= TIMESTAMP_LENGTH; // Already read above - - if (byteBuffer.remaining() < minimumRemaining) - throw new TransformationException("Byte data too short for MessageTransaction"); - byte[] reference = new byte[REFERENCE_LENGTH]; byteBuffer.get(reference); byte[] senderPublicKey = Serialization.deserializePublicKey(byteBuffer); + String recipient = Serialization.deserializeAddress(byteBuffer); long assetId; @@ -71,11 +64,8 @@ public class MessageTransactionTransformer extends TransactionTransformer { byte[] data = new byte[dataSize]; byteBuffer.get(data); - // Still need to make sure there are enough bytes left for remaining fields - if (byteBuffer.remaining() < IS_ENCRYPTED_LENGTH + IS_TEXT_LENGTH + FEE_LENGTH + SIGNATURE_LENGTH) - throw new TransformationException("Byte data too short for MessageTransaction"); - boolean isEncrypted = byteBuffer.get() != 0; + boolean isText = byteBuffer.get() != 0; BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); diff --git a/src/transform/transaction/MultiPaymentTransactionTransformer.java b/src/transform/transaction/MultiPaymentTransactionTransformer.java index 59d853c8..a143ab31 100644 --- a/src/transform/transaction/MultiPaymentTransactionTransformer.java +++ b/src/transform/transaction/MultiPaymentTransactionTransformer.java @@ -31,21 +31,14 @@ public class MultiPaymentTransactionTransformer extends TransactionTransformer { private static final int TYPELESS_LENGTH = BASE_TYPELESS_LENGTH + SENDER_LENGTH + PAYMENTS_COUNT_LENGTH; static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { - if (byteBuffer.remaining() < TYPELESS_LENGTH) - throw new TransformationException("Byte data too short for MultiPaymentTransaction"); - long timestamp = byteBuffer.getLong(); byte[] reference = new byte[REFERENCE_LENGTH]; byteBuffer.get(reference); byte[] senderPublicKey = Serialization.deserializePublicKey(byteBuffer); - int paymentsCount = byteBuffer.getInt(); - // Check remaining buffer size - int minRemaining = paymentsCount * PaymentTransformer.getDataLength() + FEE_LENGTH + SIGNATURE_LENGTH; - if (byteBuffer.remaining() < minRemaining) - throw new TransformationException("Byte data too short for MultiPaymentTransaction"); + int paymentsCount = byteBuffer.getInt(); List payments = new ArrayList(); for (int i = 0; i < paymentsCount; ++i) diff --git a/src/transform/transaction/PaymentTransactionTransformer.java b/src/transform/transaction/PaymentTransactionTransformer.java index 7c39c791..6e940be0 100644 --- a/src/transform/transaction/PaymentTransactionTransformer.java +++ b/src/transform/transaction/PaymentTransactionTransformer.java @@ -27,16 +27,15 @@ public class PaymentTransactionTransformer extends TransactionTransformer { private static final int TYPELESS_LENGTH = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH; static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { - if (byteBuffer.remaining() < TYPELESS_LENGTH) - throw new TransformationException("Byte data too short for PaymentTransaction"); - long timestamp = byteBuffer.getLong(); byte[] reference = new byte[REFERENCE_LENGTH]; byteBuffer.get(reference); byte[] senderPublicKey = Serialization.deserializePublicKey(byteBuffer); + String recipient = Serialization.deserializeAddress(byteBuffer); + BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer); BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); diff --git a/src/transform/transaction/RegisterNameTransactionTransformer.java b/src/transform/transaction/RegisterNameTransactionTransformer.java index 4f136f44..912a16ce 100644 --- a/src/transform/transaction/RegisterNameTransactionTransformer.java +++ b/src/transform/transaction/RegisterNameTransactionTransformer.java @@ -30,9 +30,6 @@ public class RegisterNameTransactionTransformer extends TransactionTransformer { private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + REGISTRANT_LENGTH + OWNER_LENGTH + NAME_SIZE_LENGTH + DATA_SIZE_LENGTH; static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { - if (byteBuffer.remaining() < TYPELESS_DATALESS_LENGTH) - throw new TransformationException("Byte data too short for RegisterNameTransaction"); - long timestamp = byteBuffer.getLong(); byte[] reference = new byte[REFERENCE_LENGTH]; @@ -43,11 +40,8 @@ public class RegisterNameTransactionTransformer extends TransactionTransformer { String owner = Serialization.deserializeAddress(byteBuffer); String name = Serialization.deserializeSizedString(byteBuffer, Name.MAX_NAME_SIZE); - String data = Serialization.deserializeSizedString(byteBuffer, Name.MAX_DATA_SIZE); - // Still need to make sure there are enough bytes left for remaining fields - if (byteBuffer.remaining() < FEE_LENGTH + SIGNATURE_LENGTH) - throw new TransformationException("Byte data too short for RegisterNameTransaction"); + String data = Serialization.deserializeSizedString(byteBuffer, Name.MAX_DATA_SIZE); BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); diff --git a/src/transform/transaction/SellNameTransactionTransformer.java b/src/transform/transaction/SellNameTransactionTransformer.java index b8156fd2..b9e7bdf9 100644 --- a/src/transform/transaction/SellNameTransactionTransformer.java +++ b/src/transform/transaction/SellNameTransactionTransformer.java @@ -29,9 +29,6 @@ public class SellNameTransactionTransformer extends TransactionTransformer { private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + OWNER_LENGTH + NAME_SIZE_LENGTH + AMOUNT_LENGTH; static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { - if (byteBuffer.remaining() < TYPELESS_DATALESS_LENGTH) - throw new TransformationException("Byte data too short for SellNameTransaction"); - long timestamp = byteBuffer.getLong(); byte[] reference = new byte[REFERENCE_LENGTH]; @@ -41,10 +38,6 @@ public class SellNameTransactionTransformer extends TransactionTransformer { String name = Serialization.deserializeSizedString(byteBuffer, Name.MAX_NAME_SIZE); - // Still need to make sure there are enough bytes left for remaining fields - if (byteBuffer.remaining() < AMOUNT_LENGTH + FEE_LENGTH + SIGNATURE_LENGTH) - throw new TransformationException("Byte data too short for SellNameTransaction"); - BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer); BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); diff --git a/src/transform/transaction/TransactionTransformer.java b/src/transform/transaction/TransactionTransformer.java index 85c28f17..1149d67f 100644 --- a/src/transform/transaction/TransactionTransformer.java +++ b/src/transform/transaction/TransactionTransformer.java @@ -1,10 +1,16 @@ package transform.transaction; +import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; +import java.util.Arrays; import org.json.simple.JSONObject; +import com.google.common.hash.HashCode; + import data.transaction.TransactionData; +import qora.account.PrivateKeyAccount; +import qora.transaction.Transaction; import qora.transaction.Transaction.TransactionType; import transform.TransformationException; import transform.Transformer; @@ -24,60 +30,69 @@ public class TransactionTransformer extends Transformer { if (bytes.length < TYPE_LENGTH) throw new TransformationException("Byte data too short to determine transaction type"); + System.out.println("v1 tx hex: " + HashCode.fromBytes(bytes).toString()); + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); TransactionType type = TransactionType.valueOf(byteBuffer.getInt()); if (type == null) return null; - switch (type) { - case GENESIS: - return GenesisTransactionTransformer.fromByteBuffer(byteBuffer); + try { + switch (type) { + case GENESIS: + return GenesisTransactionTransformer.fromByteBuffer(byteBuffer); - case PAYMENT: - return PaymentTransactionTransformer.fromByteBuffer(byteBuffer); + case PAYMENT: + return PaymentTransactionTransformer.fromByteBuffer(byteBuffer); - case REGISTER_NAME: - return RegisterNameTransactionTransformer.fromByteBuffer(byteBuffer); + case REGISTER_NAME: + return RegisterNameTransactionTransformer.fromByteBuffer(byteBuffer); - case UPDATE_NAME: - return UpdateNameTransactionTransformer.fromByteBuffer(byteBuffer); + case UPDATE_NAME: + return UpdateNameTransactionTransformer.fromByteBuffer(byteBuffer); - case SELL_NAME: - return SellNameTransactionTransformer.fromByteBuffer(byteBuffer); + case SELL_NAME: + return SellNameTransactionTransformer.fromByteBuffer(byteBuffer); - case CANCEL_SELL_NAME: - return CancelSellNameTransactionTransformer.fromByteBuffer(byteBuffer); + case CANCEL_SELL_NAME: + return CancelSellNameTransactionTransformer.fromByteBuffer(byteBuffer); - case BUY_NAME: - return BuyNameTransactionTransformer.fromByteBuffer(byteBuffer); + case BUY_NAME: + return BuyNameTransactionTransformer.fromByteBuffer(byteBuffer); - case CREATE_POLL: - return CreatePollTransactionTransformer.fromByteBuffer(byteBuffer); + case CREATE_POLL: + return CreatePollTransactionTransformer.fromByteBuffer(byteBuffer); - case VOTE_ON_POLL: - return VoteOnPollTransactionTransformer.fromByteBuffer(byteBuffer); + case VOTE_ON_POLL: + return VoteOnPollTransactionTransformer.fromByteBuffer(byteBuffer); - case ISSUE_ASSET: - return IssueAssetTransactionTransformer.fromByteBuffer(byteBuffer); + case ARBITRARY: + return ArbitraryTransactionTransformer.fromByteBuffer(byteBuffer); - case TRANSFER_ASSET: - return TransferAssetTransactionTransformer.fromByteBuffer(byteBuffer); + case ISSUE_ASSET: + return IssueAssetTransactionTransformer.fromByteBuffer(byteBuffer); - case CREATE_ASSET_ORDER: - return CreateOrderTransactionTransformer.fromByteBuffer(byteBuffer); + case TRANSFER_ASSET: + return TransferAssetTransactionTransformer.fromByteBuffer(byteBuffer); - case CANCEL_ASSET_ORDER: - return CancelOrderTransactionTransformer.fromByteBuffer(byteBuffer); + case CREATE_ASSET_ORDER: + return CreateOrderTransactionTransformer.fromByteBuffer(byteBuffer); - case MULTIPAYMENT: - return MultiPaymentTransactionTransformer.fromByteBuffer(byteBuffer); + case CANCEL_ASSET_ORDER: + return CancelOrderTransactionTransformer.fromByteBuffer(byteBuffer); - case MESSAGE: - return MessageTransactionTransformer.fromByteBuffer(byteBuffer); + case MULTIPAYMENT: + return MultiPaymentTransactionTransformer.fromByteBuffer(byteBuffer); - default: - throw new TransformationException("Unsupported transaction type [" + type.value + "] during conversion from bytes"); + case MESSAGE: + return MessageTransactionTransformer.fromByteBuffer(byteBuffer); + + default: + throw new TransformationException("Unsupported transaction type [" + type.value + "] during conversion from bytes"); + } + } catch (BufferUnderflowException e) { + throw new TransformationException("Byte data too short for transaction type [" + type.value + "]"); } } @@ -110,6 +125,9 @@ public class TransactionTransformer extends Transformer { case VOTE_ON_POLL: return VoteOnPollTransactionTransformer.getDataLength(transactionData); + case ARBITRARY: + return ArbitraryTransactionTransformer.getDataLength(transactionData); + case ISSUE_ASSET: return IssueAssetTransactionTransformer.getDataLength(transactionData); @@ -162,6 +180,9 @@ public class TransactionTransformer extends Transformer { case VOTE_ON_POLL: return VoteOnPollTransactionTransformer.toBytes(transactionData); + case ARBITRARY: + return ArbitraryTransactionTransformer.toBytes(transactionData); + case ISSUE_ASSET: return IssueAssetTransactionTransformer.toBytes(transactionData); @@ -185,6 +206,91 @@ public class TransactionTransformer extends Transformer { } } + /** + * Serialize transaction as byte[], stripping off trailing signature ready for signing/verification. + *

+ * Used by signature-related methods such as {@link Transaction#sign(PrivateKeyAccount)} and {@link Transaction#isSignatureValid()} + * + * @param transactionData + * @return byte[] of transaction, without trailing signature + * @throws TransformationException + */ + public static byte[] toBytesForSigning(TransactionData transactionData) throws TransformationException { + switch (transactionData.getType()) { + case GENESIS: + return GenesisTransactionTransformer.toBytesForSigningImpl(transactionData); + + case PAYMENT: + return PaymentTransactionTransformer.toBytesForSigningImpl(transactionData); + + case REGISTER_NAME: + return RegisterNameTransactionTransformer.toBytesForSigningImpl(transactionData); + + case UPDATE_NAME: + return UpdateNameTransactionTransformer.toBytesForSigningImpl(transactionData); + + case SELL_NAME: + return SellNameTransactionTransformer.toBytesForSigningImpl(transactionData); + + case CANCEL_SELL_NAME: + return CancelSellNameTransactionTransformer.toBytesForSigningImpl(transactionData); + + case BUY_NAME: + return BuyNameTransactionTransformer.toBytesForSigningImpl(transactionData); + + case CREATE_POLL: + return CreatePollTransactionTransformer.toBytesForSigningImpl(transactionData); + + case VOTE_ON_POLL: + return VoteOnPollTransactionTransformer.toBytesForSigningImpl(transactionData); + + case ARBITRARY: + return ArbitraryTransactionTransformer.toBytesForSigningImpl(transactionData); + + case ISSUE_ASSET: + return IssueAssetTransactionTransformer.toBytesForSigningImpl(transactionData); + + case TRANSFER_ASSET: + return TransferAssetTransactionTransformer.toBytesForSigningImpl(transactionData); + + case CREATE_ASSET_ORDER: + return CreateOrderTransactionTransformer.toBytesForSigningImpl(transactionData); + + case CANCEL_ASSET_ORDER: + return CancelOrderTransactionTransformer.toBytesForSigningImpl(transactionData); + + case MULTIPAYMENT: + return MultiPaymentTransactionTransformer.toBytesForSigningImpl(transactionData); + + case MESSAGE: + return MessageTransactionTransformer.toBytesForSigningImpl(transactionData); + + default: + throw new TransformationException( + "Unsupported transaction type [" + transactionData.getType().value + "] during conversion to bytes for signing"); + } + } + + /** + * Generic serialization of transaction as byte[], stripping off trailing signature ready for signing/verification. + * + * @param transactionData + * @return byte[] of transaction, without trailing signature + * @throws TransformationException + */ + protected static byte[] toBytesForSigningImpl(TransactionData transactionData) throws TransformationException { + try { + byte[] bytes = TransactionTransformer.toBytes(transactionData); + + if (transactionData.getSignature() == null) + return bytes; + + return Arrays.copyOf(bytes, bytes.length - Transformer.SIGNATURE_LENGTH); + } catch (TransformationException e) { + throw new RuntimeException("Unable to transform transaction to byte array for signing", e); + } + } + public static JSONObject toJSON(TransactionData transactionData) throws TransformationException { switch (transactionData.getType()) { case GENESIS: @@ -214,6 +320,9 @@ public class TransactionTransformer extends Transformer { case VOTE_ON_POLL: return VoteOnPollTransactionTransformer.toJSON(transactionData); + case ARBITRARY: + return ArbitraryTransactionTransformer.toJSON(transactionData); + case ISSUE_ASSET: return IssueAssetTransactionTransformer.toJSON(transactionData); diff --git a/src/transform/transaction/TransferAssetTransactionTransformer.java b/src/transform/transaction/TransferAssetTransactionTransformer.java index 8e9afb5c..f652a649 100644 --- a/src/transform/transaction/TransferAssetTransactionTransformer.java +++ b/src/transform/transaction/TransferAssetTransactionTransformer.java @@ -28,17 +28,17 @@ public class TransferAssetTransactionTransformer extends TransactionTransformer private static final int TYPELESS_LENGTH = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + ASSET_ID_LENGTH + AMOUNT_LENGTH; static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { - if (byteBuffer.remaining() < TYPELESS_LENGTH) - throw new TransformationException("Byte data too short for TransferAssetTransaction"); - long timestamp = byteBuffer.getLong(); byte[] reference = new byte[REFERENCE_LENGTH]; byteBuffer.get(reference); byte[] senderPublicKey = Serialization.deserializePublicKey(byteBuffer); + String recipient = Serialization.deserializeAddress(byteBuffer); + long assetId = byteBuffer.getLong(); + BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer, AMOUNT_LENGTH); BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); diff --git a/src/transform/transaction/UpdateNameTransactionTransformer.java b/src/transform/transaction/UpdateNameTransactionTransformer.java index 5f3bab84..e3f00980 100644 --- a/src/transform/transaction/UpdateNameTransactionTransformer.java +++ b/src/transform/transaction/UpdateNameTransactionTransformer.java @@ -30,9 +30,6 @@ public class UpdateNameTransactionTransformer extends TransactionTransformer { private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + REGISTRANT_LENGTH + OWNER_LENGTH + NAME_SIZE_LENGTH + DATA_SIZE_LENGTH; static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { - if (byteBuffer.remaining() < TYPELESS_DATALESS_LENGTH) - throw new TransformationException("Byte data too short for UpdateNameTransaction"); - long timestamp = byteBuffer.getLong(); byte[] reference = new byte[REFERENCE_LENGTH]; @@ -43,11 +40,8 @@ public class UpdateNameTransactionTransformer extends TransactionTransformer { String newOwner = Serialization.deserializeAddress(byteBuffer); String name = Serialization.deserializeSizedString(byteBuffer, Name.MAX_NAME_SIZE); - String newData = Serialization.deserializeSizedString(byteBuffer, Name.MAX_DATA_SIZE); - // Still need to make sure there are enough bytes left for remaining fields - if (byteBuffer.remaining() < FEE_LENGTH + SIGNATURE_LENGTH) - throw new TransformationException("Byte data too short for UpdateNameTransaction"); + String newData = Serialization.deserializeSizedString(byteBuffer, Name.MAX_DATA_SIZE); BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); diff --git a/src/transform/transaction/VoteOnPollTransactionTransformer.java b/src/transform/transaction/VoteOnPollTransactionTransformer.java index 6c45d828..d9f9c4f3 100644 --- a/src/transform/transaction/VoteOnPollTransactionTransformer.java +++ b/src/transform/transaction/VoteOnPollTransactionTransformer.java @@ -24,14 +24,10 @@ public class VoteOnPollTransactionTransformer extends TransactionTransformer { // Property lengths private static final int VOTER_LENGTH = ADDRESS_LENGTH; private static final int NAME_SIZE_LENGTH = INT_LENGTH; - private static final int OPTION_LENGTH = INT_LENGTH; private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + VOTER_LENGTH + NAME_SIZE_LENGTH; static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { - if (byteBuffer.remaining() < TYPELESS_DATALESS_LENGTH) - throw new TransformationException("Byte data too short for VoteOnPollTransaction"); - long timestamp = byteBuffer.getLong(); byte[] reference = new byte[REFERENCE_LENGTH]; @@ -41,18 +37,10 @@ public class VoteOnPollTransactionTransformer extends TransactionTransformer { String pollName = Serialization.deserializeSizedString(byteBuffer, Poll.MAX_NAME_SIZE); - // Make sure there are enough bytes left for poll options - if (byteBuffer.remaining() < OPTION_LENGTH) - throw new TransformationException("Byte data too short for VoteOnPollTransaction"); - int optionIndex = byteBuffer.getInt(); if (optionIndex < 0 || optionIndex >= Poll.MAX_OPTIONS) throw new TransformationException("Invalid option number for VoteOnPollTransaction"); - // Still need to make sure there are enough bytes left for remaining fields - if (byteBuffer.remaining() < FEE_LENGTH + SIGNATURE_LENGTH) - throw new TransformationException("Byte data too short for VoteOnPollTransaction"); - BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer); byte[] signature = new byte[SIGNATURE_LENGTH]; @@ -81,7 +69,7 @@ public class VoteOnPollTransactionTransformer extends TransactionTransformer { bytes.write(voteOnPollTransactionData.getVoterPublicKey()); Serialization.serializeSizedString(bytes, voteOnPollTransactionData.getPollName()); - bytes.write(voteOnPollTransactionData.getOptionIndex()); + bytes.write(Ints.toByteArray(voteOnPollTransactionData.getOptionIndex())); Serialization.serializeBigDecimal(bytes, voteOnPollTransactionData.getFee()); diff --git a/src/txhex.java b/src/txhex.java new file mode 100644 index 00000000..e221f19a --- /dev/null +++ b/src/txhex.java @@ -0,0 +1,51 @@ +import com.google.common.hash.HashCode; + +import data.transaction.TransactionData; +import qora.block.BlockChain; +import repository.DataException; +import repository.Repository; +import repository.RepositoryManager; +import transform.TransformationException; +import transform.transaction.TransactionTransformer; +import utils.Base58; + +public class txhex { + + public static void main(String[] args) { + if (args.length == 0) { + System.err.println("usage: txhex "); + System.exit(1); + } + + byte[] signature = Base58.decode(args[0]); + + try { + test.Common.setRepository(); + } catch (DataException e) { + System.err.println("Couldn't connect to repository: " + e.getMessage()); + System.exit(2); + } + + try { + BlockChain.validate(); + } catch (DataException e) { + System.err.println("Couldn't validate repository: " + e.getMessage()); + System.exit(2); + } + + try (final Repository repository = RepositoryManager.getRepository()) { + TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature); + byte[] bytes = TransactionTransformer.toBytes(transactionData); + System.out.println(HashCode.fromBytes(bytes).toString()); + } catch (DataException | TransformationException e) { + e.printStackTrace(); + } + + try { + test.Common.closeRepository(); + } catch (DataException e) { + e.printStackTrace(); + } + } + +} diff --git a/src/v1feeder.java b/src/v1feeder.java new file mode 100644 index 00000000..6c6b0d63 --- /dev/null +++ b/src/v1feeder.java @@ -0,0 +1,433 @@ +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.google.common.primitives.Ints; + +import data.block.BlockData; +import data.transaction.TransactionData; +import qora.block.Block; +import qora.block.Block.ValidationResult; +import qora.block.BlockChain; +import qora.crypto.Crypto; +import repository.DataException; +import repository.Repository; +import repository.RepositoryManager; +import transform.TransformationException; +import transform.block.BlockTransformer; +import utils.Pair; + +public class v1feeder extends Thread { + + private static final int INACTIVITY_TIMEOUT = 60 * 1000; // milliseconds + private static final int CONNECTION_TIMEOUT = 2 * 1000; // milliseconds + private static final int PING_INTERVAL = 10 * 1000; // milliseconds + private static final int DEFAULT_PORT = 9084; + + private static final int MAGIC_LENGTH = 4; + private static final int TYPE_LENGTH = 4; + private static final int HAS_ID_LENGTH = 1; + private static final int ID_LENGTH = 4; + private static final int DATA_SIZE_LENGTH = 4; + private static final int CHECKSUM_LENGTH = 4; + + private static final int SIGNATURE_LENGTH = 128; + + private static final byte[] MAINNET_MAGIC = { 0x12, 0x34, 0x56, 0x78 }; + + private static final int GET_PEERS_TYPE = 1; + private static final int PEERS_TYPE = 2; + private static final int HEIGHT_TYPE = 3; + private static final int GET_SIGNATURES_TYPE = 4; + private static final int SIGNATURES_TYPE = 5; + private static final int GET_BLOCK_TYPE = 6; + private static final int BLOCK_TYPE = 7; + private static final int TRANSACTION_TYPE = 8; + private static final int PING_TYPE = 9; + private static final int VERSION_TYPE = 10; + private static final int FIND_MYSELF_TYPE = 11; + + private Socket socket; + private OutputStream out; + + private static final int IDLE_STATE = 0; + private static final int AWAITING_HEADERS_STATE = 1; + private static final int HAVE_HEADERS_STATE = 2; + private static final int AWAITING_BLOCK_STATE = 3; + private static final int HAVE_BLOCK_STATE = 4; + private int feederState = IDLE_STATE; + private int messageId = -1; + + private long lastPingTimestamp = System.currentTimeMillis(); + private List signatures = new ArrayList(); + + private v1feeder(String address, int port) throws InterruptedException { + try { + for (int i = 0; i < 10; ++i) + try { + // Create new socket for connection to peer + this.socket = new Socket(); + + // Collate this.address and destination port + InetSocketAddress socketAddress = new InetSocketAddress(address, port); + + // Attempt to connect, with timeout from settings + this.socket.connect(socketAddress, CONNECTION_TIMEOUT); + break; + } catch (SocketTimeoutException e) { + System.err.println("Timed out trying to connect to " + address + " - retrying"); + Thread.sleep(1000); + this.socket = null; + } catch (Exception e) { + System.err.println("Failed to connect to " + address + ": " + e.getMessage()); + return; + } + + // No connection after retries? + if (this.socket == null) + return; + + // Enable TCP keep-alive packets + this.socket.setKeepAlive(true); + + // Inactivity timeout + this.socket.setSoTimeout(INACTIVITY_TIMEOUT); + + // Grab reference to output stream + this.out = socket.getOutputStream(); + + // Start main communication thread + this.start(); + } catch (SocketException e) { + System.err.println("Failed to set socket timeout for address " + address + ": " + e.getMessage()); + } catch (IOException e) { + System.err.println("Failed to get output stream for address " + address + ": " + e.getMessage()); + } + } + + private byte[] createMessage(int type, boolean hasId, Integer id, byte[] data) throws IOException { + if (data == null) + data = new byte[0]; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(MAINNET_MAGIC); + + bytes.write(Ints.toByteArray(type)); + + byte[] hasIdBytes = new byte[] { (byte) (hasId ? 1 : 0) }; + bytes.write(hasIdBytes); + + if (hasId) { + if (id == null) + id = (int) ((Math.random() * 1000000) + 1); + + bytes.write(Ints.toByteArray(id)); + } + + bytes.write(Ints.toByteArray(data.length)); + + if (data.length > 0) { + byte[] checksum = Crypto.digest(data); + bytes.write(checksum, 0, CHECKSUM_LENGTH); + + bytes.write(data); + } + + // System.out.println("Creating message type [" + type + "] with " + (hasId ? "id [" + id + "]" : "no id") + " and data length " + data.length); + + return bytes.toByteArray(); + } + + private void sendMessage(byte[] message) throws IOException { + synchronized (this.out) { + this.out.write(message); + this.out.flush(); + } + } + + private void processMessage(int type, int id, byte[] data) throws IOException { + // System.out.println("Received message type [" + type + "] with id [" + id + "] and data length " + data.length); + + ByteBuffer byteBuffer = ByteBuffer.wrap(data); + switch (type) { + case HEIGHT_TYPE: + int height = byteBuffer.getInt(); + + System.out.println("Peer height: " + height); + break; + + case SIGNATURES_TYPE: + // shove into list + int numSignatures = byteBuffer.getInt(); + + while (numSignatures-- > 0) { + byte[] signature = new byte[SIGNATURE_LENGTH]; + byteBuffer.get(signature); + signatures.add(signature); + } + + // System.out.println("We now have " + signatures.size() + " signature(s) to process"); + + feederState = HAVE_HEADERS_STATE; + break; + + case BLOCK_TYPE: + // If messageId doesn't match then discard + if (id != this.messageId) + break; + + // read block and process + int claimedHeight = byteBuffer.getInt(); + + System.out.println("Received block allegedly at height " + claimedHeight); + + byte[] blockBytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(blockBytes); + + Pair> blockInfo = null; + + try { + blockInfo = BlockTransformer.fromBytes(blockBytes); + } catch (TransformationException e) { + System.err.println("Couldn't parse block bytes from peer: " + e.getMessage()); + System.exit(3); + } + + try (final Repository repository = RepositoryManager.getRepository()) { + Block block = new Block(repository, blockInfo.getA(), blockInfo.getB()); + + if (!block.isSignatureValid()) { + System.err.println("Invalid block signature"); + System.exit(4); + } + + ValidationResult result = block.isValid(); + + if (result != ValidationResult.OK) { + System.err.println("Invalid block, validation result code: " + result.value); + System.exit(4); + } + + block.process(); + repository.saveChanges(); + } catch (DataException e) { + System.err.println("Unable to process block: " + e.getMessage()); + e.printStackTrace(); + } + + feederState = HAVE_BLOCK_STATE; + break; + + case PING_TYPE: + // System.out.println("Sending pong for ping [" + id + "]"); + byte[] pongMessage = createMessage(PING_TYPE, true, id, null); + sendMessage(pongMessage); + break; + + case VERSION_TYPE: + long timestamp = byteBuffer.getLong(); + int versionLength = byteBuffer.getInt(); + byte[] versionBytes = new byte[versionLength]; + byteBuffer.get(versionBytes); + String version = new String(versionBytes, Charset.forName("UTF-8")); + + System.out.println("Peer version info: " + version); + break; + + default: + System.out.println("Discarding message type [" + type + "] with id [" + id + "] and data length " + data.length); + } + } + + private int parseBuffer(byte[] buffer, int bufferEnd) throws IOException { + int newBufferEnd = bufferEnd; + + try { + ByteBuffer byteBuffer = ByteBuffer.wrap(buffer, 0, bufferEnd); + + // Check magic + byte[] magic = new byte[MAGIC_LENGTH]; + byteBuffer.get(magic); + if (!Arrays.equals(magic, MAINNET_MAGIC)) { + // bad data - discard whole buffer + return 0; + } + + int type = byteBuffer.getInt(); + + byte[] hasId = new byte[HAS_ID_LENGTH]; + byteBuffer.get(hasId); + + int id = -1; + if (hasId[0] == (byte) 1) + id = byteBuffer.getInt(); + + int dataSize = byteBuffer.getInt(); + byte[] data = new byte[dataSize]; + if (dataSize > 0) { + byte[] checksum = new byte[CHECKSUM_LENGTH]; + byteBuffer.get(checksum); + + byteBuffer.get(data); + } + + // We have a full message - remove from buffer + int nextMessageOffset = byteBuffer.position(); + newBufferEnd = bufferEnd - nextMessageOffset; + byteBuffer = null; + + System.arraycopy(buffer, nextMessageOffset, buffer, 0, newBufferEnd); + + // Process message + processMessage(type, id, data); + } catch (BufferUnderflowException e) { + // Not enough data + } + + return newBufferEnd; + } + + public void run() { + try { + DataInputStream in = new DataInputStream(socket.getInputStream()); + byte[] buffer = new byte[2 * 1024 * 1024]; // 2MB + int bufferEnd = 0; + + // Send our height + try (final Repository repository = RepositoryManager.getRepository()) { + int height = repository.getBlockRepository().getBlockchainHeight(); + System.out.println("Sending our height " + height + " to peer"); + byte[] heightMessage = createMessage(HEIGHT_TYPE, false, null, Ints.toByteArray(height)); + sendMessage(heightMessage); + } + + while (true) { + // Anything to read? + if (in.available() > 0) { + // read message + int numRead = in.read(buffer, bufferEnd, in.available()); + if (numRead == -1) { + // input EOF + System.out.println("Socket EOF"); + return; + } + + bufferEnd += numRead; + } + + if (bufferEnd > 0) { + // attempt to parse + bufferEnd = parseBuffer(buffer, bufferEnd); + } + + // Do we need to send a ping message? + if (System.currentTimeMillis() - lastPingTimestamp >= PING_INTERVAL) { + byte[] pingMessage = createMessage(PING_TYPE, true, null, null); + sendMessage(pingMessage); + lastPingTimestamp = System.currentTimeMillis(); + } + + byte[] signature = null; + switch (feederState) { + case IDLE_STATE: + // Get signature from our highest block + try (final Repository repository = RepositoryManager.getRepository()) { + BlockData blockData = repository.getBlockRepository().getLastBlock(); + + if (blockData != null) + signature = blockData.getSignature(); + } + + // done? + if (signature == null) { + System.out.println("No last block in repository?"); + return; + } + + System.out.println("Requesting more signatures..."); + byte[] getSignaturesMessage = createMessage(GET_SIGNATURES_TYPE, true, null, signature); + sendMessage(getSignaturesMessage); + feederState = AWAITING_HEADERS_STATE; + break; + + case HAVE_HEADERS_STATE: + case HAVE_BLOCK_STATE: + // request next block? + if (signatures.size() == 0) { + feederState = IDLE_STATE; + break; + } + + System.out.println("Requesting next block..."); + signature = signatures.remove(0); + this.messageId = (int) ((Math.random() * 1000000) + 1); + byte[] getBlockMessage = createMessage(GET_BLOCK_TYPE, true, this.messageId, signature); + sendMessage(getBlockMessage); + feederState = AWAITING_BLOCK_STATE; + break; + } + } + } catch (IOException | DataException e) { + // give up + System.err.println("Exiting due to: " + e.getMessage()); + } + + try { + this.socket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) { + if (args.length == 0) { + System.err.println("usage: v1feeder v1-node-address [port]"); + System.exit(1); + } + + try { + test.Common.setRepository(); + } catch (DataException e) { + System.err.println("Couldn't connect to repository: " + e.getMessage()); + System.exit(2); + } + + try { + BlockChain.validate(); + } catch (DataException e) { + System.err.println("Couldn't validate repository: " + e.getMessage()); + System.exit(2); + } + + // connect to v1 node + String address = args[0]; + int port = args.length > 1 ? Integer.valueOf(args[1]) : DEFAULT_PORT; + + try { + new v1feeder(address, port).join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + System.out.println("Exiting v1feeder"); + + try { + test.Common.closeRepository(); + } catch (DataException e) { + e.printStackTrace(); + } + } + +}