forked from Qortal/qortal
Arbitrary Transaction support + various fixes
NOTE: requires HSQLDB built from svn rev 5836 or later Fixed BuyNameTransactionData constructors not picking up nameReference. Added new "orphan" tool to regress blockchain back to specified block. Added new Block constructor for when receiving a block from the network. Fixed Block generatingBalance/forging code to be compliant with v1. Added logging of transactions that fail validation during block validation. Fixed buyer/seller balances not being updated during name purchase. Generally replace BigDecimal.compareTo expressions with "<operator> 0" form. e.g. instead of someBigDecimal.compareTo(anotherBigDecimal) == 1 we now have someBigDecimal.compareTo(anotherBigDecimal) > 0 Fix amounts involved in BuyNameTransactions. Renamed Transaction.calcSignature to .sign Refactored Transaction.toBytesLessSignature to TransactionTransformer.toBytesForSigning, which itself calls subclass' toBytesForSigningImpl, which might override Transaction.toBytesForSigningImpl when special v1 mangling is required. Corrected more cases of NTP.getTime in transaction processing which should really be transaction's timestmap instead. Fixed HSQLDB-related issue where strings were padded with spaces during comparison. Some column types no longer case-insensitive as that mode of comparison is done during transaction validation. Added missing option_index column to CreatePollTransactionOptions which was causing out-of-order options during fetching from repository and hence signature failures. Added unit tests for v1-special mangled transaction signature checking. Removed checks for remaining bytes to ByteBuffer in various transaction transformers' fromByteBuffer() methods as the buffer underflow exception is now caught in TransactionTransformer.fromBytes. Corrected byte-related transformations of CreatePollTransactions that were missing voter counts (albeit always zero). Corrected byte-related transformations of IssueAssetTransactions that were missing duplicate signature/reference (v1-special). Added "txhex" tool to output transaction in hex form, given base58 tx signature. Added "v1feeder" tool to fetch blocks from v1 node and process them.
This commit is contained in:
parent
b401adcc55
commit
7da84b2b85
91
src/data/transaction/ArbitraryTransactionData.java
Normal file
91
src/data/transaction/ArbitraryTransactionData.java
Normal file
@ -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<PaymentData> payments;
|
||||
|
||||
// Constructors
|
||||
|
||||
/** Reconstructing a V3 arbitrary transaction with signature */
|
||||
public ArbitraryTransactionData(int version, byte[] senderPublicKey, int service, byte[] data, DataType dataType, List<PaymentData> 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<PaymentData> 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<PaymentData> getPayments() {
|
||||
return this.payments;
|
||||
}
|
||||
|
||||
}
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
52
src/orphan.java
Normal file
52
src/orphan.java
Normal file
@ -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 <new-blockchain-tip-height>");
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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<TransactionData> transactions) throws DataException {
|
||||
this(repository, blockData);
|
||||
|
||||
this.transactions = new ArrayList<Transaction>();
|
||||
|
||||
// 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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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<Long, BigDecimal> 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 {
|
||||
|
195
src/qora/transaction/ArbitraryTransaction.java
Normal file
195
src/qora/transaction/ArbitraryTransaction.java
Normal file
@ -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<Account> getRecipientAccounts() throws DataException {
|
||||
List<Account> recipients = new ArrayList<Account>();
|
||||
|
||||
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. <userpath>/arbitrary/<sender address>/<block height>/<tx-sig>-<service>.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());
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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<PaymentData> 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());
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
* <p>
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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<PaymentData> 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());
|
||||
}
|
||||
|
||||
}
|
@ -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<PollOptionData> 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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
|
67
src/test/CompatibilityTests.java
Normal file
67
src/test/CompatibilityTests.java
Normal file
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
|
||||
|
155
src/transform/transaction/ArbitraryTransactionTransformer.java
Normal file
155
src/transform/transaction/ArbitraryTransactionTransformer.java
Normal file
@ -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<PaymentData> payments = null;
|
||||
if (version != 1) {
|
||||
int paymentsCount = byteBuffer.getInt();
|
||||
|
||||
payments = new ArrayList<PaymentData>();
|
||||
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<PaymentData> 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<PaymentData> 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;
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
|
@ -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];
|
||||
|
@ -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];
|
||||
|
@ -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);
|
||||
|
@ -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<PollOptionData> 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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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<PaymentData> payments = new ArrayList<PaymentData>();
|
||||
for (int i = 0; i < paymentsCount; ++i)
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
* <p>
|
||||
* 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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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());
|
||||
|
||||
|
51
src/txhex.java
Normal file
51
src/txhex.java
Normal file
@ -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 <base58-tx-signature>");
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
433
src/v1feeder.java
Normal file
433
src/v1feeder.java
Normal file
@ -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<byte[]> signatures = new ArrayList<byte[]>();
|
||||
|
||||
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<BlockData, List<TransactionData>> 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user