Finally syncs with qora1 chain!

ATData no longer needs deploySignature as a link back to DeployATTransaction,
but does need creator and creation [timestamp].
"creation" is critical for ordering ATs when creating/validating blocks.

Similar changes to ATStateData, adding creation, stateHash (for quicker
comparison with blocks received over the network), and fees incurred
by running AT on that block.
Also added more explicit constructors for different scenarios.

BlockData upgraded from simplistic "atBytes" to use ATStateData (above)
which has details on ATs run for that block, fees incurred, and a hash
of the AT's state. atCount added to keep track of how many ATs ran.

ATTransactions essentially reuse the GenesisAccount's publickey as
creator/sender as they're brought into existence by the Qora code
rather than an end user. ATTransactionData updated to reflect this
and the AT's address used as a "sender" field.

Account tidied up with respect to CIYAM ATs and setConfirmedBalance
ensures there is a corresponding record in Accounts (DB table).

Account, and subclasses, don't need "throws DataException" on
constructor any more.

Fixed bug in Asset Order matching where the matching engine
would give up after first potential match instead of trying others.

Lots more work on CIYAM AT, albeit mainly blind importing of old
v1 ATs from static JSON file as they're all dead and new
v2 implementation is not backwards compatible.

More work on Blocks, mostly AT stuff, but also fork-based corruption
prevention using fix from Qora v1.

Payment-related transactions (multipayment, etc.) always expect/use
non-null (albeit maybe empty) list of PaymentData when validating,
processing or orphaning.
Mainly a change in HSQLDBTransactionRepository.getPayments()

Payment.isValid(byte[], PaymentData, BigDecimal, boolean isZeroAmountValid)
didn't pass on isZeroAmountValid to called method - whoops!

Lots of work on ATTransactions themselves.

MessageTransactions incorrectly assumed the optional payment was always
in Qora. Now fixed to use the transaction's provided assetId.

Mass of fixes/additions to HSQLDBATRepository, especially fixing
incorrect reference to Assets DB table!

In HSQLDBDatabaseUpdates, bump QoraAmount type from DECIMAL(19,8)
to DECIMAL(27,8) to allow for huge asset quantities.
You WILL have to rebuild your database!
This commit is contained in:
catbref 2018-10-26 17:47:47 +01:00
parent 46eee3cbce
commit 2c51a0362b
36 changed files with 1075 additions and 365 deletions

View File

@ -6,6 +6,8 @@ public class ATData {
// Properties
private String ATAddress;
private String creator;
private long creation;
private int version;
private byte[] codeBytes;
private boolean isSleeping;
@ -14,13 +16,14 @@ public class ATData {
private boolean hadFatalError;
private boolean isFrozen;
private BigDecimal frozenBalance;
private byte[] deploySignature;
// Constructors
public ATData(String ATAddress, int version, byte[] codeBytes, boolean isSleeping, Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError,
boolean isFrozen, BigDecimal frozenBalance, byte[] deploySignature) {
public ATData(String ATAddress, String creator, long creation, int version, byte[] codeBytes, boolean isSleeping, Integer sleepUntilHeight,
boolean isFinished, boolean hadFatalError, boolean isFrozen, BigDecimal frozenBalance) {
this.ATAddress = ATAddress;
this.creator = creator;
this.creation = creation;
this.version = version;
this.codeBytes = codeBytes;
this.isSleeping = isSleeping;
@ -29,12 +32,11 @@ public class ATData {
this.hadFatalError = hadFatalError;
this.isFrozen = isFrozen;
this.frozenBalance = frozenBalance;
this.deploySignature = deploySignature;
}
public ATData(String ATAddress, int version, byte[] codeBytes, boolean isSleeping, Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError,
boolean isFrozen, Long frozenBalance, byte[] deploySignature) {
this(ATAddress, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, (BigDecimal) null, deploySignature);
public ATData(String ATAddress, String creator, long creation, int version, byte[] codeBytes, boolean isSleeping, Integer sleepUntilHeight,
boolean isFinished, boolean hadFatalError, boolean isFrozen, Long frozenBalance) {
this(ATAddress, creator, creation, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, (BigDecimal) null);
// Convert Long frozenBalance to BigDecimal
if (frozenBalance != null)
@ -47,6 +49,14 @@ public class ATData {
return this.ATAddress;
}
public String getCreator() {
return this.creator;
}
public long getCreation() {
return this.creation;
}
public int getVersion() {
return this.version;
}
@ -103,8 +113,4 @@ public class ATData {
this.frozenBalance = frozenBalance;
}
public byte[] getDeploySignature() {
return this.deploySignature;
}
}

View File

@ -1,18 +1,42 @@
package data.at;
import java.math.BigDecimal;
public class ATStateData {
// Properties
private String ATAddress;
private int height;
private Integer height;
private Long creation;
private byte[] stateData;
private byte[] stateHash;
private BigDecimal fees;
// Constructors
public ATStateData(String ATAddress, int height, byte[] stateData) {
/** Create new ATStateData */
public ATStateData(String ATAddress, Integer height, Long creation, byte[] stateData, byte[] stateHash, BigDecimal fees) {
this.ATAddress = ATAddress;
this.height = height;
this.creation = creation;
this.stateData = stateData;
this.stateHash = stateHash;
this.fees = fees;
}
/** For recreating per-block ATStateData from repository where not all info is needed */
public ATStateData(String ATAddress, int height, byte[] stateHash, BigDecimal fees) {
this(ATAddress, height, null, null, stateHash, fees);
}
/** For creating ATStateData from serialized bytes when we don't have all the info */
public ATStateData(String ATAddress, byte[] stateHash) {
this(ATAddress, null, null, null, stateHash, null);
}
/** For creating ATStateData from serialized bytes when we don't have all the info */
public ATStateData(String ATAddress, byte[] stateHash, BigDecimal fees) {
this(ATAddress, null, null, null, stateHash, fees);
}
// Getters / setters
@ -21,12 +45,29 @@ public class ATStateData {
return this.ATAddress;
}
public int getHeight() {
public Integer getHeight() {
return this.height;
}
// Likely to be used when block received over network is attached to blockchain
public void setHeight(Integer height) {
this.height = height;
}
public Long getCreation() {
return this.creation;
}
public byte[] getStateData() {
return this.stateData;
}
public byte[] getStateHash() {
return this.stateHash;
}
public BigDecimal getFees() {
return this.fees;
}
}

View File

@ -12,16 +12,16 @@ public class BlockData {
private int transactionCount;
private BigDecimal totalFees;
private byte[] transactionsSignature;
private int height;
private Integer height;
private long timestamp;
private BigDecimal generatingBalance;
private byte[] generatorPublicKey;
private byte[] generatorSignature;
private byte[] atBytes;
private int atCount;
private BigDecimal atFees;
public BlockData(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, int height, long timestamp,
BigDecimal generatingBalance, byte[] generatorPublicKey, byte[] generatorSignature, byte[] atBytes, BigDecimal atFees) {
public BlockData(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, Integer height, long timestamp,
BigDecimal generatingBalance, byte[] generatorPublicKey, byte[] generatorSignature, int atCount, BigDecimal atFees) {
this.version = version;
this.reference = reference;
this.transactionCount = transactionCount;
@ -32,7 +32,7 @@ public class BlockData {
this.generatingBalance = generatingBalance;
this.generatorPublicKey = generatorPublicKey;
this.generatorSignature = generatorSignature;
this.atBytes = atBytes;
this.atCount = atCount;
this.atFees = atFees;
if (this.generatorSignature != null && this.transactionsSignature != null)
@ -41,6 +41,26 @@ public class BlockData {
this.signature = null;
}
public byte[] getSignature() {
return this.signature;
}
public void setSignature(byte[] signature) {
this.signature = signature;
}
public int getVersion() {
return this.version;
}
public byte[] getReference() {
return this.reference;
}
public void setReference(byte[] reference) {
this.reference = reference;
}
public int getTransactionCount() {
return this.transactionCount;
}
@ -65,31 +85,11 @@ public class BlockData {
this.transactionsSignature = transactionsSignature;
}
public byte[] getSignature() {
return this.signature;
}
public void setSignature(byte[] signature) {
this.signature = signature;
}
public int getVersion() {
return this.version;
}
public byte[] getReference() {
return this.reference;
}
public void setReference(byte[] reference) {
this.reference = reference;
}
public int getHeight() {
public Integer getHeight() {
return this.height;
}
public void setHeight(int height) {
public void setHeight(Integer height) {
this.height = height;
}
@ -113,12 +113,20 @@ public class BlockData {
this.generatorSignature = generatorSignature;
}
public byte[] getAtBytes() {
return this.atBytes;
public int getATCount() {
return this.atCount;
}
public BigDecimal getAtFees() {
public void setATCount(int atCount) {
this.atCount = atCount;
}
public BigDecimal getATFees() {
return this.atFees;
}
public void setATFees(BigDecimal atFees) {
this.atFees = atFees;
}
}

View File

@ -3,9 +3,9 @@ package data.block;
public class BlockTransactionData {
// Properties
protected byte[] blockSignature;
protected int sequence;
protected byte[] transactionSignature;
private byte[] blockSignature;
private int sequence;
private byte[] transactionSignature;
// Constructors

View File

@ -2,12 +2,13 @@ package data.transaction;
import java.math.BigDecimal;
import qora.account.GenesisAccount;
import qora.transaction.Transaction.TransactionType;
public class ATTransactionData extends TransactionData {
// Properties
private byte[] senderPublicKey;
private String atAddress;
private String recipient;
private BigDecimal amount;
private Long assetId;
@ -15,26 +16,26 @@ public class ATTransactionData extends TransactionData {
// Constructors
public ATTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, Long assetId, byte[] message, BigDecimal fee, long timestamp,
public ATTransactionData(String atAddress, String recipient, BigDecimal amount, Long assetId, byte[] message, BigDecimal fee, long timestamp,
byte[] reference, byte[] signature) {
super(TransactionType.AT, fee, senderPublicKey, timestamp, reference, signature);
super(TransactionType.AT, fee, GenesisAccount.PUBLIC_KEY, timestamp, reference, signature);
this.senderPublicKey = senderPublicKey;
this.atAddress = atAddress;
this.recipient = recipient;
this.amount = amount;
this.assetId = assetId;
this.message = message;
}
public ATTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, Long assetId, byte[] message, BigDecimal fee, long timestamp,
public ATTransactionData(String atAddress, String recipient, BigDecimal amount, Long assetId, byte[] message, BigDecimal fee, long timestamp,
byte[] reference) {
this(senderPublicKey, recipient, amount, assetId, message, fee, timestamp, reference, null);
this(atAddress, recipient, amount, assetId, message, fee, timestamp, reference, null);
}
// Getters/Setters
public byte[] getSenderPublicKey() {
return this.senderPublicKey;
public String getATAddress() {
return this.atAddress;
}
public String getRecipient() {

View File

@ -28,7 +28,7 @@ public class Account {
protected Account() {
}
public Account(Repository repository, String address) throws DataException {
public Account(Repository repository, String address) {
this.repository = repository;
this.accountData = new AccountData(address);
}
@ -55,6 +55,7 @@ public class Account {
for (int i = 1; i < BlockChain.BLOCK_RETARGET_INTERVAL && blockData != null && blockData.getHeight() > 1; ++i) {
Block block = new Block(this.repository, blockData);
// CIYAM AT transactions should be fetched from repository so no special handling needed here
for (Transaction transaction : block.getTransactions()) {
if (transaction.isInvolved(this)) {
final BigDecimal amount = transaction.getAmount(this);
@ -65,19 +66,10 @@ public class Account {
}
}
// TODO - CIYAM AT support needed
/*
* LinkedHashMap<Tuple2<Integer, Integer>, AT_Transaction> atTxs = db.getATTransactionMap().getATTransactions(block.getHeight(db));
* Iterator<AT_Transaction> iter = atTxs.values().iterator(); while (iter.hasNext()) { AT_Transaction key = iter.next();
*
* if (key.getRecipient().equals(this.getAddress())) balance = balance.subtract(BigDecimal.valueOf(key.getAmount(), 8)); }
*/
blockData = block.getParent();
}
// Do not go below 0
// XXX: How would this even be possible?
balance = balance.max(BigDecimal.ZERO);
return balance;
@ -102,19 +94,11 @@ public class Account {
for (int i = 1; i < confirmations && blockData != null && blockData.getHeight() > 1; ++i) {
Block block = new Block(this.repository, blockData);
// CIYAM AT transactions should be fetched from repository so no special handling needed here
for (Transaction transaction : block.getTransactions())
if (transaction.isInvolved(this))
balance = balance.subtract(transaction.getAmount(this));
// TODO - CIYAM AT support
/*
* // Also check AT transactions for amounts received to this account LinkedHashMap<Tuple2<Integer, Integer>, AT_Transaction> atTxs =
* db.getATTransactionMap().getATTransactions(block.getHeight(db)); Iterator<AT_Transaction> iter = atTxs.values().iterator(); while
* (iter.hasNext()) { AT_Transaction key = iter.next();
*
* if (key.getRecipient().equals(this.getAddress())) balance = balance.subtract(BigDecimal.valueOf(key.getAmount(), 8)); }
*/
blockData = block.getParent();
}
@ -131,6 +115,9 @@ public class Account {
}
public void setConfirmedBalance(long assetId, BigDecimal balance) throws DataException {
// Can't have a balance without an account - make sure it exists!
this.repository.getAccountRepository().create(this.accountData.getAddress());
AccountBalanceData accountBalanceData = new AccountBalanceData(this.accountData.getAddress(), assetId, balance);
this.repository.getAccountRepository().save(accountBalanceData);

View File

@ -1,13 +1,12 @@
package qora.account;
import repository.DataException;
import repository.Repository;
public final class GenesisAccount extends PublicKeyAccount {
public static final byte[] PUBLIC_KEY = new byte[] { 1, 1, 1, 1, 1, 1, 1, 1 };
public GenesisAccount(Repository repository) throws DataException {
public GenesisAccount(Repository repository) {
super(repository, PUBLIC_KEY);
}

View File

@ -2,14 +2,13 @@ package qora.account;
import qora.crypto.Crypto;
import qora.crypto.Ed25519;
import repository.DataException;
import repository.Repository;
public class PublicKeyAccount extends Account {
protected byte[] publicKey;
public PublicKeyAccount(Repository repository, byte[] publicKey) throws DataException {
public PublicKeyAccount(Repository repository, byte[] publicKey) {
super(repository, Crypto.toAddress(publicKey));
this.publicKey = publicKey;

View File

@ -169,9 +169,9 @@ public class Order {
BigDecimal matchedAmount = ourAmountLeft.min(theirAmountLeft);
LOGGER.trace("matchedAmount: " + matchedAmount.toPlainString() + " " + wantAssetData.getName());
// If we can't buy anything then we're done
// If we can't buy anything then try another order
if (matchedAmount.compareTo(BigDecimal.ZERO) <= 0)
break;
continue;
// Calculate amount granularity based on both assets' divisibility
BigDecimal increment = this.calculateAmountGranularity(haveAssetData, wantAssetData, theirOrderData);
@ -179,9 +179,9 @@ public class Order {
matchedAmount = matchedAmount.subtract(matchedAmount.remainder(increment));
LOGGER.trace("matchedAmount adjusted for granularity: " + matchedAmount.toPlainString() + " " + wantAssetData.getName());
// If we can't buy anything then we're done
// If we can't buy anything then try another order
if (matchedAmount.compareTo(BigDecimal.ZERO) <= 0)
break;
continue;
// Trade can go ahead!

View File

@ -1,5 +1,6 @@
package qora.at;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import org.ciyam.at.MachineState;
@ -7,6 +8,8 @@ import org.ciyam.at.MachineState;
import data.at.ATData;
import data.at.ATStateData;
import data.transaction.DeployATTransactionData;
import qora.account.PublicKeyAccount;
import qora.crypto.Crypto;
import repository.DataException;
import repository.Repository;
@ -25,11 +28,14 @@ public class AT {
this.atStateData = atStateData;
}
/** Deploying AT */
public AT(Repository repository, DeployATTransactionData deployATTransactionData) throws DataException {
this.repository = repository;
String atAddress = deployATTransactionData.getATAddress();
int height = this.repository.getBlockRepository().getBlockchainHeight();
int height = this.repository.getBlockRepository().getBlockchainHeight() + 1;
String creator = new PublicKeyAccount(repository, deployATTransactionData.getCreatorPublicKey()).getAddress();
long creation = deployATTransactionData.getTimestamp();
byte[] creationBytes = deployATTransactionData.getCreationBytes();
short version = (short) (creationBytes[0] | (creationBytes[1] << 8)); // Little-endian
@ -37,11 +43,14 @@ public class AT {
if (version >= 2) {
MachineState machineState = new MachineState(deployATTransactionData.getCreationBytes());
this.atData = new ATData(atAddress, machineState.version, machineState.getCodeBytes(), machineState.getIsSleeping(),
this.atData = new ATData(atAddress, creator, creation, machineState.version, machineState.getCodeBytes(), machineState.getIsSleeping(),
machineState.getSleepUntilHeight(), machineState.getIsFinished(), machineState.getHadFatalError(), machineState.getIsFrozen(),
machineState.getFrozenBalance(), deployATTransactionData.getSignature());
machineState.getFrozenBalance());
this.atStateData = new ATStateData(atAddress, height, machineState.toBytes());
byte[] stateData = machineState.toBytes();
byte[] stateHash = Crypto.digest(stateData);
this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, BigDecimal.ZERO.setScale(8));
} else {
// Legacy v1 AT in 'dead' state
// Extract code bytes length
@ -64,10 +73,9 @@ public class AT {
byte[] codeBytes = new byte[codeLen];
byteBuffer.get(codeBytes);
this.atData = new ATData(deployATTransactionData.getATAddress(), 1, codeBytes, false, null, true, false, false, (Long) null,
deployATTransactionData.getSignature());
this.atData = new ATData(atAddress, creator, creation, 1, codeBytes, false, null, true, false, false, (Long) null);
this.atStateData = new ATStateData(deployATTransactionData.getATAddress(), height, null);
this.atStateData = new ATStateData(atAddress, height, creation, null, null, BigDecimal.ZERO.setScale(8));
}
}
@ -75,7 +83,6 @@ public class AT {
public void deploy() throws DataException {
this.repository.getATRepository().save(this.atData);
this.repository.getATRepository().save(this.atStateData);
}
public void undeploy() throws DataException {

View File

@ -15,6 +15,7 @@ import org.apache.logging.log4j.Logger;
import com.google.common.primitives.Bytes;
import data.at.ATStateData;
import data.block.BlockData;
import data.block.BlockTransactionData;
import data.transaction.TransactionData;
@ -25,6 +26,7 @@ import qora.assets.Asset;
import qora.crypto.Crypto;
import qora.transaction.GenesisTransaction;
import qora.transaction.Transaction;
import repository.ATRepository;
import repository.BlockRepository;
import repository.DataException;
import repository.Repository;
@ -64,6 +66,7 @@ public class Block {
REFERENCE_MISSING(10),
PARENT_DOES_NOT_EXIST(11),
BLOCKCHAIN_NOT_EMPTY(12),
PARENT_HAS_EXISTING_CHILD(13),
TIMESTAMP_OLDER_THAN_PARENT(20),
TIMESTAMP_IN_FUTURE(21),
TIMESTAMP_MS_INCORRECT(22),
@ -74,7 +77,8 @@ public class Block {
GENESIS_TRANSACTIONS_INVALID(50),
TRANSACTION_TIMESTAMP_INVALID(51),
TRANSACTION_INVALID(52),
TRANSACTION_PROCESSING_FAILED(53);
TRANSACTION_PROCESSING_FAILED(53),
AT_STATES_MISMATCH(61);
public final int value;
@ -97,6 +101,11 @@ public class Block {
// Other properties
private static final Logger LOGGER = LogManager.getLogger(Block.class);
protected List<Transaction> transactions;
protected List<ATStateData> atStates;
protected List<ATStateData> ourAtStates; // Generated locally
protected BigDecimal ourAtFees; // Generated locally
protected BigDecimal cachedNextGeneratingBalance;
// Other useful constants
@ -104,39 +113,92 @@ public class Block {
// Constructors
/**
* Constructs Block-handling object without loading transactions and AT states.
* <p>
* Transactions and AT states are loaded on first call to getTransactions() or getATStates() respectively.
*
* @param repository
* @param blockData
* @throws DataException
*/
public Block(Repository repository, BlockData blockData) throws DataException {
this.repository = repository;
this.blockData = blockData;
this.generator = new PublicKeyAccount(repository, blockData.getGeneratorPublicKey());
}
// When receiving a block over network?
public Block(Repository repository, BlockData blockData, List<TransactionData> transactions) throws DataException {
/**
* Constructs Block-handling object using passed transaction and AT states.
* <p>
* This constructor typically used when receiving a serialized block over the network.
*
* @param repository
* @param blockData
* @param transactions
* @param atStates
* @throws DataException
*/
public Block(Repository repository, BlockData blockData, List<TransactionData> transactions, List<ATStateData> atStates) throws DataException {
this(repository, blockData);
this.transactions = new ArrayList<Transaction>();
BigDecimal totalFees = BigDecimal.ZERO.setScale(8);
// We have to sum fees too
for (TransactionData transactionData : transactions) {
this.transactions.add(Transaction.fromData(repository, transactionData));
this.blockData.setTotalFees(this.blockData.getTotalFees().add(transactionData.getFee()));
totalFees = totalFees.add(transactionData.getFee());
}
this.atStates = atStates;
for (ATStateData atState : atStates)
totalFees = totalFees.add(atState.getFees());
this.blockData.setTotalFees(totalFees);
}
// For creating a new block?
public Block(Repository repository, int version, byte[] reference, long timestamp, BigDecimal generatingBalance, PrivateKeyAccount generator,
byte[] atBytes, BigDecimal atFees) {
/**
* Constructs Block-handling object with basic, initial values.
* <p>
* This constructor typically used when generating a new block.
* <p>
* Note that CIYAM ATs will be executed and AT-Transactions prepended to this block, along with AT state data and fees.
*
* @param repository
* @param version
* @param reference
* @param timestamp
* @param generatingBalance
* @param generator
* @throws DataException
*/
public Block(Repository repository, int version, byte[] reference, long timestamp, BigDecimal generatingBalance, PrivateKeyAccount generator)
throws DataException {
this.repository = repository;
this.generator = generator;
this.blockData = new BlockData(version, reference, 0, BigDecimal.ZERO.setScale(8), null, 0, timestamp, generatingBalance, generator.getPublicKey(),
null, atBytes, atFees);
int transactionCount = 0;
byte[] transactionsSignature = null;
Integer height = null;
byte[] generatorSignature = null;
this.executeATs();
int atCount = this.ourAtStates.size();
BigDecimal atFees = this.ourAtFees;
BigDecimal totalFees = atFees;
this.blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance,
generator.getPublicKey(), generatorSignature, atCount, atFees);
this.transactions = new ArrayList<Transaction>();
this.atStates = this.ourAtStates;
}
/** Construct a new block for use in tests */
public Block(Repository repository, BlockData parentBlockData, PrivateKeyAccount generator, byte[] atBytes, BigDecimal atFees) throws DataException {
public Block(Repository repository, BlockData parentBlockData, PrivateKeyAccount generator) throws DataException {
this.repository = repository;
this.generator = generator;
@ -155,11 +217,18 @@ public class Block {
}
long timestamp = parentBlock.calcNextBlockTimestamp(version, generatorSignature, generator);
int transactionCount = 0;
BigDecimal totalFees = BigDecimal.ZERO.setScale(8);
byte[] transactionsSignature = null;
int height = parentBlockData.getHeight() + 1;
int atCount = 0;
BigDecimal atFees = BigDecimal.ZERO.setScale(8);
this.blockData = new BlockData(version, reference, 0, BigDecimal.ZERO.setScale(8), null, 0, timestamp, generatingBalance, generator.getPublicKey(),
generatorSignature, atBytes, atFees);
this.blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance,
generator.getPublicKey(), generatorSignature, atCount, atFees);
this.transactions = new ArrayList<Transaction>();
this.atStates = new ArrayList<ATStateData>();
}
// Getters/setters
@ -192,12 +261,17 @@ public class Block {
* @return 1, 2 or 3
*/
public int getNextBlockVersion() {
if (this.blockData.getHeight() == null)
throw new IllegalStateException("Can't determine next block's version as this block has no height set");
if (this.blockData.getHeight() < BlockChain.getATReleaseHeight())
return 1;
else if (this.blockData.getTimestamp() < BlockChain.getPowFixReleaseTimestamp())
return 2;
else
else if (this.blockData.getTimestamp() < BlockChain.getDeployATV2Timestamp())
return 3;
else
return 4;
}
/**
@ -212,8 +286,8 @@ public class Block {
* @throws DataException
*/
public BigDecimal calcNextBlockGeneratingBalance() throws DataException {
if (this.blockData.getHeight() == 0)
throw new IllegalStateException("Block height is unset");
if (this.blockData.getHeight() == null)
throw new IllegalStateException("Can't calculate next block's generating balance as this block's height is unset");
// This block not at the start of an interval?
if (this.blockData.getHeight() % BlockChain.BLOCK_RETARGET_INTERVAL != 0)
@ -346,7 +420,7 @@ public class Block {
/**
* Return block's transactions.
* <p>
* If the block was loaded from repository then it's possible this method will call the repository to load the transactions if they are not already loaded.
* If the block was loaded from repository then it's possible this method will call the repository to fetch the transactions if not done already.
*
* @return
* @throws DataException
@ -371,6 +445,37 @@ public class Block {
return this.transactions;
}
/**
* Return block's AT states.
* <p>
* If the block was loaded from repository then it's possible this method will call the repository to fetch the AT states if not done already.
* <p>
* <b>Note:</b> AT states fetched from repository only contain summary info, not actual data like serialized state data or AT creation timestamps!
*
* @return
* @throws DataException
*/
public List<ATStateData> getATStates() throws DataException {
// Already loaded?
if (this.atStates != null)
return this.atStates;
// If loading from repository, this block must have a height
if (this.blockData.getHeight() == null)
throw new IllegalStateException("Can't fetch block's AT states from repository without a block height");
// Allocate cache for results
List<ATStateData> atStateData = this.repository.getATRepository().getBlockATStatesFromHeight(this.blockData.getHeight());
// The number of AT states fetched from repository should correspond with Block's atCount
if (atStateData.size() != this.blockData.getATCount())
throw new IllegalStateException("Block's AT states from repository do not match block's AT count");
this.atStates = atStateData;
return this.atStates;
}
// Navigation
/**
@ -531,8 +636,6 @@ public class Block {
* @throws DataException
*/
public ValidationResult isValid() throws DataException {
// TODO
// Check parent block exists
if (this.blockData.getReference() == null)
return ValidationResult.REFERENCE_MISSING;
@ -543,6 +646,10 @@ public class Block {
Block parentBlock = new Block(this.repository, parentBlockData);
// Check parent doesn't already have a child block
if (parentBlock.getChild() != null)
return ValidationResult.PARENT_HAS_EXISTING_CHILD;
// Check timestamp is newer than parent timestamp
if (this.blockData.getTimestamp() <= parentBlockData.getTimestamp())
return ValidationResult.TIMESTAMP_OLDER_THAN_PARENT;
@ -558,7 +665,7 @@ public class Block {
// Check block version
if (this.blockData.getVersion() != parentBlock.getNextBlockVersion())
return ValidationResult.VERSION_INCORRECT;
if (this.blockData.getVersion() < 2 && (this.blockData.getAtBytes() != null || this.blockData.getAtFees() != null))
if (this.blockData.getVersion() < 2 && this.blockData.getATCount() != 0)
return ValidationResult.FEATURE_NOT_YET_RELEASED;
// Check generating balance
@ -583,16 +690,34 @@ public class Block {
if (hashValue.compareTo(lowerTarget) < 0)
return ValidationResult.GENERATOR_NOT_ACCEPTED;
// Process CIYAM ATs, prepending AT-Transactions to block then compare post-execution checksums
// XXX We should pre-calculate, and cache, next block's AT-transactions after processing each block to save repeated work
if (this.blockData.getAtBytes() != null && this.blockData.getAtBytes().length > 0) {
// TODO
// try {
// AT_Block atBlock = AT_Controller.validateATs(this.getBlockATs(), BlockChain.getHeight() + 1);
// this.atFees = atBlock.getTotalFees();
// } catch (NoSuchAlgorithmException | AT_Exception e) {
// return false;
// }
// CIYAM ATs
if (this.blockData.getATCount() != 0) {
// Locally generated AT states should be valid so no need to re-execute them
if (this.ourAtStates != this.getATStates()) {
// Otherwise, check locally generated AT states against ones received from elsewhere?
this.executeATs();
if (this.ourAtStates.size() != this.blockData.getATCount())
return ValidationResult.AT_STATES_MISMATCH;
if (this.ourAtFees.compareTo(this.blockData.getATFees()) != 0)
return ValidationResult.AT_STATES_MISMATCH;
// Note: this.atStates fully loaded thanks to this.getATStates() call above
for (int s = 0; s < this.atStates.size(); ++s) {
ATStateData ourAtState = this.ourAtStates.get(s);
ATStateData theirAtState = this.atStates.get(s);
if (!ourAtState.getATAddress().equals(theirAtState.getATAddress()))
return ValidationResult.AT_STATES_MISMATCH;
if (!ourAtState.getStateHash().equals(theirAtState.getStateHash()))
return ValidationResult.AT_STATES_MISMATCH;
if (ourAtState.getFees().compareTo(theirAtState.getFees()) != 0)
return ValidationResult.AT_STATES_MISMATCH;
}
}
}
// Check transactions
@ -643,8 +768,47 @@ public class Block {
return ValidationResult.OK;
}
/**
* Execute CIYAM ATs for this block.
* <p>
* This needs to be done locally for all blocks, regardless of origin.<br>
* This method is called by <tt>isValid</tt>.
* <p>
* After calling, AT-generated transactions are prepended to the block's transactions and AT state data is generated.
* <p>
* This method is not needed if fetching an existing block from the repository.
* <p>
* Updates <tt>this.ourAtStates</tt> and <tt>this.ourAtFees</tt>.
*
* @see #isValid()
*
* @throws DataException
*
*/
public void executeATs() throws DataException {
// We're expecting a lack of AT state data at this point.
if (this.ourAtStates != null)
throw new IllegalStateException("Attempted to execute ATs when block's local AT state data already exists");
// For old v1 CIYAM ATs we blindly accept them
if (this.blockData.getVersion() < 4) {
this.ourAtStates = this.atStates;
this.ourAtFees = this.blockData.getATFees();
return;
}
// Find all executable ATs, ordered by earliest creation date first
// Run each AT, appends AT-Transactions and corresponding AT states, to our lists
// Finally prepend our entire AT-Transactions/states to block's transactions/states, adjust fees, etc.
// Note: store locally-calculated AT states separately to this.atStates so we can compare them in isValid()
}
public void process() throws DataException {
// Process transactions (we'll link them to this block after saving the block itself)
// AT-generated transactions are already added to our transactions so no special handling is needed here.
List<Transaction> transactions = this.getTransactions();
for (Transaction transaction : transactions)
transaction.process();
@ -654,6 +818,17 @@ public class Block {
if (blockFee.compareTo(BigDecimal.ZERO) > 0)
this.generator.setConfirmedBalance(Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).add(blockFee));
// Process AT fees and save AT states into repository
ATRepository atRepository = this.repository.getATRepository();
for (ATStateData atState : this.getATStates()) {
Account atAccount = new Account(this.repository, atState.getATAddress());
// Subtract AT-generated fees from AT accounts
atAccount.setConfirmedBalance(Asset.QORA, atAccount.getConfirmedBalance(Asset.QORA).subtract(atState.getFees()));
atRepository.save(atState);
}
// Link block into blockchain by fetching signature of highest block and setting that as our reference
int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight();
BlockData latestBlockData = this.repository.getBlockRepository().fromHeight(blockchainHeight);
@ -675,12 +850,8 @@ public class Block {
}
public void orphan() throws DataException {
// TODO
// Orphan block's CIYAM ATs
orphanAutomatedTransactions();
// Orphan transactions in reverse order, and unlink them from this block
// AT-generated transactions are already added to our transactions so no special handling is needed here.
List<Transaction> transactions = this.getTransactions();
for (int sequence = transactions.size() - 1; sequence >= 0; --sequence) {
Transaction transaction = transactions.get(sequence);
@ -696,25 +867,19 @@ public class Block {
if (blockFee.compareTo(BigDecimal.ZERO) > 0)
this.generator.setConfirmedBalance(Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).subtract(blockFee));
// Return AT fees and delete AT states from repository
ATRepository atRepository = this.repository.getATRepository();
for (ATStateData atState : this.getATStates()) {
Account atAccount = new Account(this.repository, atState.getATAddress());
// Return AT-generated fees to AT accounts
atAccount.setConfirmedBalance(Asset.QORA, atAccount.getConfirmedBalance(Asset.QORA).add(atState.getFees()));
}
// Delete ATStateData for this height
atRepository.deleteATStates(this.blockData.getHeight());
// Delete block from blockchain
this.repository.getBlockRepository().delete(this.blockData);
}
public void orphanAutomatedTransactions() throws DataException {
// TODO - CIYAM AT support
/*
* LinkedHashMap< Tuple2<Integer, Integer> , AT_Transaction > atTxs = DBSet.getInstance().getATTransactionMap().getATTransactions(this.getHeight(db));
*
* Iterator<AT_Transaction> iter = atTxs.values().iterator();
*
* while ( iter.hasNext() ) { AT_Transaction key = iter.next(); Long amount = key.getAmount(); if (key.getRecipientId() != null &&
* !Arrays.equals(key.getRecipientId(), new byte[ AT_Constants.AT_ID_SIZE ]) && !key.getRecipient().equalsIgnoreCase("1") ) { Account recipient = new
* Account( key.getRecipient() ); recipient.setConfirmedBalance( recipient.getConfirmedBalance( db ).subtract( BigDecimal.valueOf( amount, 8 ) ) , db );
* if ( Arrays.equals(recipient.getLastReference(db),new byte[64])) { recipient.removeReference(db); } } Account sender = new Account( key.getSender()
* ); sender.setConfirmedBalance( sender.getConfirmedBalance( db ).add( BigDecimal.valueOf( amount, 8 ) ) , db );
*
* }
*/
}
}

View File

@ -34,7 +34,7 @@ public class GenesisBlock extends Block {
public GenesisBlock(Repository repository) throws DataException {
super(repository, new BlockData(GENESIS_BLOCK_VERSION, GENESIS_REFERENCE, 0, BigDecimal.ZERO.setScale(8), GENESIS_TRANSACTIONS_SIGNATURE, 1,
Settings.getInstance().getGenesisTimestamp(), GENESIS_GENERATING_BALANCE, GENESIS_GENERATOR_PUBLIC_KEY, GENESIS_GENERATOR_SIGNATURE, null, null));
Settings.getInstance().getGenesisTimestamp(), GENESIS_GENERATING_BALANCE, GENESIS_GENERATOR_PUBLIC_KEY, GENESIS_GENERATOR_SIGNATURE, 0, BigDecimal.ZERO.setScale(8)));
this.transactions = new ArrayList<Transaction>();

View File

@ -46,31 +46,31 @@ public class Payment {
amountsByAssetId.put(Asset.QORA, fee);
// Check payments, and calculate amount total by assetId
if (payments != null)
for (PaymentData paymentData : payments) {
// Check amount is positive
if (paymentData.getAmount().compareTo(BigDecimal.ZERO) < 0)
return ValidationResult.NEGATIVE_AMOUNT;
for (PaymentData paymentData : payments) {
// Check amount is zero or positive
if (paymentData.getAmount().compareTo(BigDecimal.ZERO) < 0)
return ValidationResult.NEGATIVE_AMOUNT;
// Optional zero-amount check
if (!isZeroAmountValid && paymentData.getAmount().compareTo(BigDecimal.ZERO) <= 0)
return ValidationResult.NEGATIVE_AMOUNT;
// Optional zero-amount check
if (!isZeroAmountValid && paymentData.getAmount().compareTo(BigDecimal.ZERO) <= 0)
return ValidationResult.NEGATIVE_AMOUNT;
// Check recipient address is valid
if (!Crypto.isValidAddress(paymentData.getRecipient()))
return ValidationResult.INVALID_ADDRESS;
// Check recipient address is valid
if (!Crypto.isValidAddress(paymentData.getRecipient()))
return ValidationResult.INVALID_ADDRESS;
AssetData assetData = assetRepository.fromAssetId(paymentData.getAssetId());
// Check asset even exists
if (assetData == null)
return ValidationResult.ASSET_DOES_NOT_EXIST;
AssetData assetData = assetRepository.fromAssetId(paymentData.getAssetId());
// Check asset even exists
if (assetData == null)
return ValidationResult.ASSET_DOES_NOT_EXIST;
// Check asset amount is integer if asset is not divisible
if (!assetData.getIsDivisible() && paymentData.getAmount().stripTrailingZeros().scale() > 0)
return ValidationResult.INVALID_AMOUNT;
// Check asset amount is integer if asset is not divisible
if (!assetData.getIsDivisible() && paymentData.getAmount().stripTrailingZeros().scale() > 0)
return ValidationResult.INVALID_AMOUNT;
amountsByAssetId.compute(paymentData.getAssetId(), (assetId, amount) -> amount == null ? amount : amount.add(paymentData.getAmount()));
}
// Set or add amount into amounts-by-asset map
amountsByAssetId.compute(paymentData.getAssetId(), (assetId, amount) -> amount == null ? amount : amount.add(paymentData.getAmount()));
}
// Check sender has enough of each asset
Account sender = new PublicKeyAccount(this.repository, senderPublicKey);
@ -87,7 +87,7 @@ public class Payment {
// Single payment forms
public ValidationResult isValid(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee, boolean isZeroAmountValid) throws DataException {
return isValid(senderPublicKey, Collections.singletonList(paymentData), fee);
return isValid(senderPublicKey, Collections.singletonList(paymentData), fee, isZeroAmountValid);
}
public ValidationResult isValid(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee) throws DataException {
@ -105,22 +105,22 @@ public class Payment {
sender.setLastReference(signature);
// Process all payments
if (payments != null)
for (PaymentData paymentData : payments) {
Account recipient = new Account(this.repository, paymentData.getRecipient());
long assetId = paymentData.getAssetId();
BigDecimal amount = paymentData.getAmount();
for (PaymentData paymentData : payments) {
Account recipient = new Account(this.repository, paymentData.getRecipient());
// Update sender's balance due to amount
sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).subtract(amount));
long assetId = paymentData.getAssetId();
BigDecimal amount = paymentData.getAmount();
// Update recipient's balance
recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).add(amount));
// Update sender's balance due to amount
sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).subtract(amount));
// For QORA amounts only: if recipient has no reference yet, then this is their starting reference
if ((alwaysInitializeRecipientReference || assetId == Asset.QORA) && recipient.getLastReference() == null)
recipient.setLastReference(signature);
}
// Update recipient's balance
recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).add(amount));
// For QORA amounts only: if recipient has no reference yet, then this is their starting reference
if ((alwaysInitializeRecipientReference || assetId == Asset.QORA) && recipient.getLastReference() == null)
recipient.setLastReference(signature);
}
}
public void process(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee, byte[] signature, boolean alwaysInitializeRecipientReference)
@ -139,25 +139,24 @@ public class Payment {
sender.setLastReference(reference);
// Orphan all payments
if (payments != null)
for (PaymentData paymentData : payments) {
Account recipient = new Account(this.repository, paymentData.getRecipient());
long assetId = paymentData.getAssetId();
BigDecimal amount = paymentData.getAmount();
for (PaymentData paymentData : payments) {
Account recipient = new Account(this.repository, paymentData.getRecipient());
long assetId = paymentData.getAssetId();
BigDecimal amount = paymentData.getAmount();
// Update sender's balance due to amount
sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).add(amount));
// Update sender's balance due to amount
sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).add(amount));
// Update recipient's balance
recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).subtract(amount));
// Update recipient's balance
recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).subtract(amount));
/*
* For QORA amounts only: If recipient's last reference is this transaction's signature, then they can't have made any transactions of their own
* (which would have changed their last reference) thus this is their first reference so remove it.
*/
if ((alwaysUninitializeRecipientReference || assetId == Asset.QORA) && Arrays.equals(recipient.getLastReference(), signature))
recipient.setLastReference(null);
}
/*
* For QORA amounts only: If recipient's last reference is this transaction's signature, then they can't have made any transactions of their own
* (which would have changed their last reference) thus this is their first reference so remove it.
*/
if ((alwaysUninitializeRecipientReference || assetId == Asset.QORA) && Arrays.equals(recipient.getLastReference(), signature))
recipient.setLastReference(null);
}
}
public void orphan(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee, byte[] signature, byte[] reference,

View File

@ -5,13 +5,12 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import data.PaymentData;
import data.assets.AssetData;
import data.transaction.ATTransactionData;
import data.transaction.TransactionData;
import qora.account.Account;
import qora.account.PublicKeyAccount;
import qora.assets.Asset;
import qora.payment.Payment;
import qora.crypto.Crypto;
import repository.DataException;
import repository.Repository;
@ -35,17 +34,17 @@ public class ATTransaction extends Transaction {
@Override
public List<Account> getRecipientAccounts() throws DataException {
return Collections.singletonList(new Account(this.repository, atTransactionData.getRecipient()));
return Collections.singletonList(new Account(this.repository, this.atTransactionData.getRecipient()));
}
@Override
public boolean isInvolved(Account account) throws DataException {
String address = account.getAddress();
if (address.equals(this.getSender().getAddress()))
if (address.equals(this.atTransactionData.getATAddress()))
return true;
if (address.equals(atTransactionData.getRecipient()))
if (address.equals(this.atTransactionData.getRecipient()))
return true;
return false;
@ -55,52 +54,73 @@ public class ATTransaction extends Transaction {
public BigDecimal getAmount(Account account) throws DataException {
String address = account.getAddress();
BigDecimal amount = BigDecimal.ZERO.setScale(8);
String senderAddress = this.getSender().getAddress();
String atAddress = this.atTransactionData.getATAddress();
if (address.equals(senderAddress)) {
if (address.equals(atAddress)) {
amount = amount.subtract(this.atTransactionData.getFee());
if (atTransactionData.getAmount() != null && atTransactionData.getAssetId() == Asset.QORA)
amount = amount.subtract(atTransactionData.getAmount());
if (this.atTransactionData.getAmount() != null && this.atTransactionData.getAssetId() == Asset.QORA)
amount = amount.subtract(this.atTransactionData.getAmount());
}
if (address.equals(atTransactionData.getRecipient()) && atTransactionData.getAmount() != null)
amount = amount.add(atTransactionData.getAmount());
if (address.equals(this.atTransactionData.getRecipient()) && this.atTransactionData.getAmount() != null)
amount = amount.add(this.atTransactionData.getAmount());
return amount;
}
// Navigation
public Account getSender() throws DataException {
return new PublicKeyAccount(this.repository, this.atTransactionData.getSenderPublicKey());
public Account getATAccount() throws DataException {
return new Account(this.repository, this.atTransactionData.getATAddress());
}
public Account getRecipient() throws DataException {
return new Account(this.repository, this.atTransactionData.getRecipient());
}
// Processing
private PaymentData getPaymentData() {
if (atTransactionData.getAmount() == null)
return null;
return new PaymentData(atTransactionData.getRecipient(), atTransactionData.getAssetId(), atTransactionData.getAmount());
}
@Override
public ValidationResult isValid() throws DataException {
// Check reference is correct
Account sender = getSender();
if (!Arrays.equals(sender.getLastReference(), atTransactionData.getReference()))
Account atAccount = getATAccount();
if (!Arrays.equals(atAccount.getLastReference(), atTransactionData.getReference()))
return ValidationResult.INVALID_REFERENCE;
if (this.atTransactionData.getMessage().length > MAX_DATA_SIZE)
return ValidationResult.INVALID_DATA_LENGTH;
BigDecimal amount = this.atTransactionData.getAmount();
// If we have no payment then we're done
if (this.atTransactionData.getAmount() == null)
if (amount == null)
return ValidationResult.OK;
// Wrap and delegate final payment checks to Payment class
return new Payment(this.repository).isValid(atTransactionData.getSenderPublicKey(), getPaymentData(), atTransactionData.getFee());
// Check amount is zero or positive
if (amount.compareTo(BigDecimal.ZERO) < 0)
return ValidationResult.NEGATIVE_AMOUNT;
// Check recipient address is valid
if (!Crypto.isValidAddress(this.atTransactionData.getRecipient()))
return ValidationResult.INVALID_ADDRESS;
long assetId = this.atTransactionData.getAssetId();
AssetData assetData = this.repository.getAssetRepository().fromAssetId(assetId);
// Check asset even exists
if (assetData == null)
return ValidationResult.ASSET_DOES_NOT_EXIST;
// Check asset amount is integer if asset is not divisible
if (!assetData.getIsDivisible() && amount.stripTrailingZeros().scale() > 0)
return ValidationResult.INVALID_AMOUNT;
Account sender = getATAccount();
// Check sender has enough of asset
if (sender.getConfirmedBalance(assetId).compareTo(amount) < 0)
return ValidationResult.NO_BALANCE;
return ValidationResult.OK;
}
@Override
@ -108,10 +128,25 @@ public class ATTransaction extends Transaction {
// Save this transaction itself
this.repository.getTransactionRepository().save(this.transactionData);
if (this.atTransactionData.getAmount() != null)
// Wrap and delegate payment processing to Payment class. Only update recipient's last reference if transferring QORA.
new Payment(this.repository).process(atTransactionData.getSenderPublicKey(), getPaymentData(), atTransactionData.getFee(),
atTransactionData.getSignature(), false);
if (this.atTransactionData.getAmount() != null) {
Account sender = getATAccount();
Account recipient = getRecipient();
long assetId = this.atTransactionData.getAssetId();
BigDecimal amount = this.atTransactionData.getAmount();
// Update sender's balance due to amount
sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).subtract(amount));
// Update recipient's balance
recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).add(amount));
// For QORA amounts only: if recipient has no reference yet, then this is their starting reference
if (assetId == Asset.QORA && recipient.getLastReference() == null)
// In Qora1 last reference was set to 64-bytes of zero
// In Qora2 we use AT-Transction's signature, which makes more sense
recipient.setLastReference(this.atTransactionData.getSignature());
}
}
@Override
@ -119,10 +154,26 @@ public class ATTransaction extends Transaction {
// Delete this transaction
this.repository.getTransactionRepository().delete(this.transactionData);
if (this.atTransactionData.getAmount() != null)
// Wrap and delegate payment processing to Payment class. Only revert recipient's last reference if transferring QORA.
new Payment(this.repository).orphan(atTransactionData.getSenderPublicKey(), getPaymentData(), atTransactionData.getFee(),
atTransactionData.getSignature(), atTransactionData.getReference(), false);
if (this.atTransactionData.getAmount() != null) {
Account sender = getATAccount();
Account recipient = getRecipient();
long assetId = this.atTransactionData.getAssetId();
BigDecimal amount = this.atTransactionData.getAmount();
// Update sender's balance due to amount
sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).add(amount));
// Update recipient's balance
recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).subtract(amount));
/*
* For QORA amounts only: If recipient's last reference is this transaction's signature, then they can't have made any transactions of their own
* (which would have changed their last reference) thus this is their first reference so remove it.
*/
if (assetId == Asset.QORA && Arrays.equals(recipient.getLastReference(), this.atTransactionData.getSignature()))
recipient.setLastReference(null);
}
}
}

View File

@ -149,9 +149,9 @@ public class ArbitraryTransaction extends Transaction {
// Make sure directory structure exists
try {
Files.createDirectories(dataPath.getParent());
} catch (IOException e1) {
} catch (IOException e) {
// TODO Auto-generated catch block
e1.printStackTrace();
e.printStackTrace();
}
// Output actual transaction data

View File

@ -205,7 +205,6 @@ public class DeployATTransaction extends Transaction {
// Update AT's balance
atAccount.setConfirmedBalance(Asset.QORA, deployATTransactionData.getAmount());
}
@Override

View File

@ -133,12 +133,13 @@ public class GenesisTransaction extends Transaction {
// Save this transaction itself
this.repository.getTransactionRepository().save(this.transactionData);
// Update recipient's balance
Account recipient = new Account(repository, genesisTransactionData.getRecipient());
recipient.setConfirmedBalance(Asset.QORA, genesisTransactionData.getAmount());
// Set recipient's starting reference
// Set recipient's starting reference (also creates account)
recipient.setLastReference(genesisTransactionData.getSignature());
// Update recipient's balance
recipient.setConfirmedBalance(Asset.QORA, genesisTransactionData.getAmount());
}
@Override
@ -146,12 +147,8 @@ public class GenesisTransaction extends Transaction {
// Delete this transaction
this.repository.getTransactionRepository().delete(this.transactionData);
// Delete recipient's balance
Account recipient = new Account(repository, genesisTransactionData.getRecipient());
recipient.deleteBalance(Asset.QORA);
// Delete recipient's last reference
recipient.setLastReference(null);
// Delete recipient's account (and balance)
this.repository.getAccountRepository().delete(genesisTransactionData.getRecipient());
}
}

View File

@ -85,7 +85,7 @@ public class MessageTransaction extends Transaction {
// Processing
private PaymentData getPaymentData() {
return new PaymentData(messageTransactionData.getRecipient(), Asset.QORA, messageTransactionData.getAmount());
return new PaymentData(messageTransactionData.getRecipient(), messageTransactionData.getAssetId(), messageTransactionData.getAmount());
}
@Override

View File

@ -1,5 +1,7 @@
package repository;
import java.util.List;
import data.at.ATData;
import data.at.ATStateData;
@ -17,8 +19,14 @@ public interface ATRepository {
public ATStateData getATState(String atAddress, int height) throws DataException;
public List<ATStateData> getBlockATStatesFromHeight(int height) throws DataException;
public void save(ATStateData atStateData) throws DataException;
/** Delete AT's state data at this height */
public void delete(String atAddress, int height) throws DataException;
/** Delete state data for all ATs at this height */
public void deleteATStates(int height) throws DataException;
}

View File

@ -7,6 +7,8 @@ public interface AccountRepository {
// General account
public void create(String address) throws DataException;
public AccountData getAccount(String address) throws DataException;
public void save(AccountData accountData) throws DataException;

View File

@ -3,6 +3,10 @@ package repository.hsqldb;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import data.at.ATData;
import data.at.ATStateData;
@ -21,30 +25,32 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public ATData fromATAddress(String atAddress) throws DataException {
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT owner, asset_name, description, quantity, is_divisible, reference FROM Assets WHERE AT_address = ?", atAddress)) {
try (ResultSet resultSet = this.repository.checkedExecute(
"SELECT creator, creation, version, code_bytes, is_sleeping, sleep_until_height, is_finished, had_fatal_error, is_frozen, frozen_balance FROM ATs WHERE AT_address = ?",
atAddress)) {
if (resultSet == null)
return null;
int version = resultSet.getInt(1);
byte[] codeBytes = resultSet.getBytes(2); // Actually BLOB
boolean isSleeping = resultSet.getBoolean(3);
String creator = resultSet.getString(1);
long creation = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
int version = resultSet.getInt(3);
byte[] codeBytes = resultSet.getBytes(4); // Actually BLOB
boolean isSleeping = resultSet.getBoolean(5);
Integer sleepUntilHeight = resultSet.getInt(4);
Integer sleepUntilHeight = resultSet.getInt(6);
if (resultSet.wasNull())
sleepUntilHeight = null;
boolean isFinished = resultSet.getBoolean(5);
boolean hadFatalError = resultSet.getBoolean(6);
boolean isFrozen = resultSet.getBoolean(7);
boolean isFinished = resultSet.getBoolean(7);
boolean hadFatalError = resultSet.getBoolean(8);
boolean isFrozen = resultSet.getBoolean(9);
BigDecimal frozenBalance = resultSet.getBigDecimal(8);
BigDecimal frozenBalance = resultSet.getBigDecimal(10);
if (resultSet.wasNull())
frozenBalance = null;
byte[] deploySignature = resultSet.getBytes(9);
return new ATData(atAddress, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance, deploySignature);
return new ATData(atAddress, creator, creation, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen,
frozenBalance);
} catch (SQLException e) {
throw new DataException("Unable to fetch AT from repository", e);
}
@ -54,10 +60,10 @@ public class HSQLDBATRepository implements ATRepository {
public void save(ATData atData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("ATs");
saveHelper.bind("AT_address", atData.getATAddress()).bind("version", atData.getVersion()).bind("code_bytes", atData.getCodeBytes())
.bind("is_sleeping", atData.getIsSleeping()).bind("sleep_until_height", atData.getSleepUntilHeight())
.bind("is_finished", atData.getIsFinished()).bind("had_fatal_error", atData.getHadFatalError()).bind("is_frozen", atData.getIsFrozen())
.bind("frozen_balance", atData.getFrozenBalance()).bind("deploy_signature", atData.getDeploySignature());
saveHelper.bind("AT_address", atData.getATAddress()).bind("creator", atData.getCreator()).bind("creation", new Timestamp(atData.getCreation()))
.bind("version", atData.getVersion()).bind("code_bytes", atData.getCodeBytes()).bind("is_sleeping", atData.getIsSleeping())
.bind("sleep_until_height", atData.getSleepUntilHeight()).bind("is_finished", atData.getIsFinished())
.bind("had_fatal_error", atData.getHadFatalError()).bind("is_frozen", atData.getIsFrozen()).bind("frozen_balance", atData.getFrozenBalance());
try {
saveHelper.execute(this.repository);
@ -80,28 +86,63 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public ATStateData getATState(String atAddress, int height) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT state_data FROM ATStates WHERE AT_address = ? AND height = ?", atAddress, height)) {
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT creation, state_data, state_hash, fees FROM ATStates WHERE AT_address = ? AND height = ?", atAddress, height)) {
if (resultSet == null)
return null;
byte[] stateData = resultSet.getBytes(1); // Actually BLOB
long creation = resultSet.getTimestamp(1, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
byte[] stateData = resultSet.getBytes(2); // Actually BLOB
byte[] stateHash = resultSet.getBytes(3);
BigDecimal fees = resultSet.getBigDecimal(4);
return new ATStateData(atAddress, height, stateData);
return new ATStateData(atAddress, height, creation, stateData, stateHash, fees);
} catch (SQLException e) {
throw new DataException("Unable to fetch AT State from repository", e);
throw new DataException("Unable to fetch AT state from repository", e);
}
}
@Override
public List<ATStateData> getBlockATStatesFromHeight(int height) throws DataException {
List<ATStateData> atStates = new ArrayList<ATStateData>();
try (ResultSet resultSet = this.repository.checkedExecute("SELECT AT_address, state_hash, fees FROM ATStates WHERE height = ? ORDER BY creation ASC",
height)) {
if (resultSet == null)
return atStates; // No atStates in this block
// NB: do-while loop because .checkedExecute() implicitly calls ResultSet.next() for us
do {
String atAddress = resultSet.getString(1);
byte[] stateHash = resultSet.getBytes(2);
BigDecimal fees = resultSet.getBigDecimal(3);
ATStateData atStateData = new ATStateData(atAddress, height, stateHash, fees);
atStates.add(atStateData);
} while (resultSet.next());
} catch (SQLException e) {
throw new DataException("Unable to fetch AT states for this height from repository", e);
}
return atStates;
}
@Override
public void save(ATStateData atStateData) throws DataException {
// We shouldn't ever save partial ATStateData
if (atStateData.getCreation() == null || atStateData.getStateHash() == null || atStateData.getHeight() == null)
throw new IllegalArgumentException("Refusing to save partial AT state into repository!");
HSQLDBSaver saveHelper = new HSQLDBSaver("ATStates");
saveHelper.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight()).bind("state_data", atStateData.getStateData());
saveHelper.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
.bind("creation", new Timestamp(atStateData.getCreation())).bind("state_data", atStateData.getStateData())
.bind("state_hash", atStateData.getStateHash()).bind("fees", atStateData.getFees());
try {
saveHelper.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to save AT State into repository", e);
throw new DataException("Unable to save AT state into repository", e);
}
}
@ -110,7 +151,16 @@ public class HSQLDBATRepository implements ATRepository {
try {
this.repository.delete("ATStates", "AT_address = ? AND height = ?", atAddress, height);
} catch (SQLException e) {
throw new DataException("Unable to delete AT State from repository", e);
throw new DataException("Unable to delete AT state from repository", e);
}
}
@Override
public void deleteATStates(int height) throws DataException {
try {
this.repository.delete("ATStates", "height = ?", height);
} catch (SQLException e) {
throw new DataException("Unable to delete AT states from repository", e);
}
}

View File

@ -19,6 +19,19 @@ public class HSQLDBAccountRepository implements AccountRepository {
// General account
@Override
public void create(String address) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts");
saveHelper.bind("account", address);
try {
saveHelper.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to create account in repository", e);
}
}
@Override
public AccountData getAccount(String address) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT reference FROM Accounts WHERE account = ?", address)) {
@ -34,6 +47,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
@Override
public void save(AccountData accountData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts");
saveHelper.bind("account", accountData.getAddress()).bind("reference", accountData.getReference());
try {
@ -73,6 +87,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
@Override
public void save(AccountBalanceData accountBalanceData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("AccountBalances");
saveHelper.bind("account", accountBalanceData.getAddress()).bind("asset_id", accountBalanceData.getAssetId()).bind("balance",
accountBalanceData.getBalance());

View File

@ -18,7 +18,7 @@ import repository.TransactionRepository;
public class HSQLDBBlockRepository implements BlockRepository {
private static final String BLOCK_DB_COLUMNS = "version, reference, transaction_count, total_fees, "
+ "transactions_signature, height, generation, generating_balance, generator, generator_signature, AT_data, AT_fees";
+ "transactions_signature, height, generation, generating_balance, generator, generator_signature, AT_count, AT_fees";
protected HSQLDBRepository repository;
@ -41,11 +41,11 @@ public class HSQLDBBlockRepository implements BlockRepository {
BigDecimal generatingBalance = resultSet.getBigDecimal(8);
byte[] generatorPublicKey = resultSet.getBytes(9);
byte[] generatorSignature = resultSet.getBytes(10);
byte[] atBytes = resultSet.getBytes(11);
int atCount = resultSet.getInt(11);
BigDecimal atFees = resultSet.getBigDecimal(12);
return new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance,
generatorPublicKey, generatorSignature, atBytes, atFees);
generatorPublicKey, generatorSignature, atCount, atFees);
} catch (SQLException e) {
throw new DataException("Error extracting data from result set", e);
}
@ -62,7 +62,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
@Override
public BlockData fromReference(byte[] reference) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE height = ?", reference)) {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE reference = ?", reference)) {
return getBlockFromResultSet(resultSet);
} catch (SQLException e) {
throw new DataException("Error loading data from DB", e);
@ -123,7 +123,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
transactions.add(transactionRepo.fromSignature(transactionSignature));
} while (resultSet.next());
} catch (SQLException e) {
throw new DataException(e);
throw new DataException("Unable to fetch block's transactions from repository", e);
}
return transactions;
@ -138,7 +138,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
.bind("transactions_signature", blockData.getTransactionsSignature()).bind("height", blockData.getHeight())
.bind("generation", new Timestamp(blockData.getTimestamp())).bind("generating_balance", blockData.getGeneratingBalance())
.bind("generator", blockData.getGeneratorPublicKey()).bind("generator_signature", blockData.getGeneratorSignature())
.bind("AT_data", blockData.getAtBytes()).bind("AT_fees", blockData.getAtFees());
.bind("AT_count", blockData.getATCount()).bind("AT_fees", blockData.getATFees());
try {
saveHelper.execute(this.repository);
@ -159,6 +159,7 @@ public class HSQLDBBlockRepository implements BlockRepository {
@Override
public void save(BlockTransactionData blockTransactionData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("BlockTransactions");
saveHelper.bind("block_signature", blockTransactionData.getBlockSignature()).bind("sequence", blockTransactionData.getSequence())
.bind("transaction_signature", blockTransactionData.getTransactionSignature());

View File

@ -85,7 +85,7 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE TYPE Signature AS VARBINARY(64)");
stmt.execute("CREATE TYPE QoraAddress AS VARCHAR(36)");
stmt.execute("CREATE TYPE QoraPublicKey AS VARBINARY(32)");
stmt.execute("CREATE TYPE QoraAmount AS DECIMAL(19, 8)");
stmt.execute("CREATE TYPE QoraAmount AS DECIMAL(27, 8)");
stmt.execute("CREATE TYPE RegisteredName AS VARCHAR(400) COLLATE SQL_TEXT_NO_PAD");
stmt.execute("CREATE TYPE NameData AS VARCHAR(4000)");
stmt.execute("CREATE TYPE PollName AS VARCHAR(400) COLLATE SQL_TEXT_NO_PAD");
@ -108,7 +108,7 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE TABLE Blocks (signature BlockSignature PRIMARY KEY, version TINYINT NOT NULL, reference BlockSignature, "
+ "transaction_count INTEGER NOT NULL, total_fees QoraAmount NOT NULL, transactions_signature Signature NOT NULL, "
+ "height INTEGER NOT NULL, generation TIMESTAMP WITH TIME ZONE NOT NULL, generating_balance QoraAmount NOT NULL, "
+ "generator QoraPublicKey NOT NULL, generator_signature Signature NOT NULL, AT_data VARBINARY(20000), AT_fees QoraAmount)");
+ "generator QoraPublicKey NOT NULL, generator_signature Signature NOT NULL, AT_count INTEGER NOT NULL, AT_fees QoraAmount NOT NULL)");
// For finding blocks by height.
stmt.execute("CREATE INDEX BlockHeightIndex ON Blocks (height)");
// For finding blocks by the account that generated them.
@ -353,18 +353,21 @@ public class HSQLDBDatabaseUpdates {
case 27:
// CIYAM Automated Transactions
stmt.execute("CREATE TABLE ATs (AT_address QoraAddress, version INTEGER NOT NULL, code_bytes ATCode NOT NULL, "
+ "is_sleeping BOOLEAN NOT NULL, sleep_until_height INTEGER, is_finished BOOLEAN NOT NULL, had_fatal_error BOOLEAN NOT NULL, "
+ "is_frozen BOOLEAN NOT NULL, frozen_balance QoraAmount, deploy_signature Signature NOT NULL, PRIMARY key (AT_address))");
// For finding executable ATs
stmt.execute("CREATE INDEX ATIndex on ATs (is_finished, AT_address)");
stmt.execute("CREATE TABLE ATs (AT_address QoraAddress, creator QoraAddress, creation TIMESTAMP WITH TIME ZONE, version INTEGER NOT NULL, "
+ "code_bytes ATCode NOT NULL, is_sleeping BOOLEAN NOT NULL, sleep_until_height INTEGER, "
+ "is_finished BOOLEAN NOT NULL, had_fatal_error BOOLEAN NOT NULL, is_frozen BOOLEAN NOT NULL, frozen_balance QoraAmount, "
+ "PRIMARY key (AT_address))");
// For finding executable ATs, ordered by creation timestamp
stmt.execute("CREATE INDEX ATIndex on ATs (is_finished, creation, AT_address)");
// AT state on a per-block basis
stmt.execute(
"CREATE TABLE ATStates (AT_address QoraAddress, height INTEGER NOT NULL, state_data ATState, state_hash ATStateHash NOT NULL, fees QoraAmount NOT NULL, "
+ "PRIMARY KEY (AT_address, height), FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
stmt.execute("CREATE TABLE ATStates (AT_address QoraAddress, height INTEGER NOT NULL, creation TIMESTAMP WITH TIME ZONE, "
+ "state_data ATState, state_hash ATStateHash NOT NULL, fees QoraAmount NOT NULL, "
+ "PRIMARY KEY (AT_address, height), FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
// For finding per-block AT states, ordered by creation timestamp
stmt.execute("CREATE INDEX BlockATStateIndex on ATStates (height, creation, AT_address)");
// Generated AT Transactions
stmt.execute(
"CREATE TABLE ATTransactions (signature Signature, sender QoraPublicKey NOT NULL, recipient QoraAddress, amount QoraAmount, asset_id AssetID, message ATMessage, "
"CREATE TABLE ATTransactions (signature Signature, AT_address QoraAddress NOT NULL, recipient QoraAddress, amount QoraAmount, asset_id AssetID, message ATMessage, "
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
break;

View File

@ -17,12 +17,12 @@ public class HSQLDBATTransactionRepository extends HSQLDBTransactionRepository {
}
TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT sender, recipient, amount, asset_id, message FROM ATTransactions WHERE signature = ?",
signature)) {
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT AT_address, recipient, amount, asset_id, message FROM ATTransactions WHERE signature = ?", signature)) {
if (resultSet == null)
return null;
byte[] senderPublicKey = resultSet.getBytes(1);
String atAddress = resultSet.getString(1);
String recipient = resultSet.getString(2);
BigDecimal amount = resultSet.getBigDecimal(3);
@ -37,7 +37,7 @@ public class HSQLDBATTransactionRepository extends HSQLDBTransactionRepository {
if (resultSet.wasNull())
message = null;
return new ATTransactionData(senderPublicKey, recipient, amount, assetId, message, fee, timestamp, reference, signature);
return new ATTransactionData(atAddress, recipient, amount, assetId, message, fee, timestamp, reference, signature);
} catch (SQLException e) {
throw new DataException("Unable to fetch AT transaction from repository", e);
}
@ -49,7 +49,7 @@ public class HSQLDBATTransactionRepository extends HSQLDBTransactionRepository {
HSQLDBSaver saveHelper = new HSQLDBSaver("ATTransactions");
saveHelper.bind("signature", atTransactionData.getSignature()).bind("sender", atTransactionData.getSenderPublicKey())
saveHelper.bind("signature", atTransactionData.getSignature()).bind("AT_address", atTransactionData.getATAddress())
.bind("recipient", atTransactionData.getRecipient()).bind("amount", atTransactionData.getAmount())
.bind("asset_id", atTransactionData.getAssetId()).bind("message", atTransactionData.getMessage());

View File

@ -164,13 +164,22 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
}
/**
* Returns payments associated with a transaction's signature.
* <p>
* Used by various transaction types, like Payment, MultiPayment, ArbitraryTransaction.
*
* @param signature
* @return list of payments, empty if none found
* @throws DataException
*/
protected List<PaymentData> getPaymentsFromSignature(byte[] signature) throws DataException {
List<PaymentData> payments = new ArrayList<PaymentData>();
try (ResultSet resultSet = this.repository.checkedExecute("SELECT recipient, amount, asset_id FROM SharedTransactionPayments WHERE signature = ?",
signature)) {
if (resultSet == null)
return null;
List<PaymentData> payments = new ArrayList<PaymentData>();
return payments;
// NOTE: do-while because checkedExecute() above has already called rs.next() for us
do {

View File

@ -9,6 +9,7 @@ import org.junit.Test;
import com.google.common.hash.HashCode;
import data.at.ATStateData;
import data.block.BlockData;
import data.block.BlockTransactionData;
import data.transaction.DeployATTransactionData;
@ -70,13 +71,22 @@ public class ATTests extends Common {
long blockTimestamp = 1439997158336L;
BigDecimal generatingBalance = BigDecimal.valueOf(1440368826L).setScale(8);
byte[] generatorPublicKey = Base58.decode("X4s833bbtghh7gejmaBMbWqD44HrUobw93ANUuaNhFc");
byte[] atBytes = HashCode.fromString("17950a6c62d17ff0caa545651c054a105f1c464daca443df846cc6a3d58f764b78c09cff50f0fd9ec2").asBytes();
int atCount = 1;
BigDecimal atFees = BigDecimal.valueOf(50.0).setScale(8);
BlockData blockData = new BlockData(version, blockReference, transactionCount, totalFees, transactionsSignature, height, blockTimestamp,
generatingBalance, generatorPublicKey, generatorSignature, atBytes, atFees);
generatingBalance, generatorPublicKey, generatorSignature, atCount, atFees);
repository.getBlockRepository().save(blockData);
byte[] atBytes = HashCode.fromString("17950a6c62d17ff0caa545651c054a105f1c464daca443df846cc6a3d58f764b78c09cff50f0fd9ec2").asBytes();
String atAddress = Base58.encode(Arrays.copyOfRange(atBytes, 0, 25));
byte[] stateHash = Arrays.copyOfRange(atBytes, 25, atBytes.length);
ATStateData atStateData = new ATStateData(atAddress, height, timestamp, new byte[0], stateHash, atFees);
repository.getATRepository().save(atStateData);
}
int sequence = 0;

View File

@ -37,7 +37,7 @@ public class NavigationTests extends Common {
System.out.println("Block " + blockData.getHeight() + ", signature: " + Base58.encode(blockData.getSignature()));
assertEquals(49778, blockData.getHeight());
assertEquals((Integer) 49778, blockData.getHeight());
}
}

View File

@ -48,11 +48,7 @@ public class SignatureTests extends Common {
PrivateKeyAccount generator = new PrivateKeyAccount(repository,
new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 });
byte[] atBytes = null;
BigDecimal atFees = null;
Block block = new Block(repository, version, reference, timestamp, generatingBalance, generator, atBytes, atFees);
Block block = new Block(repository, version, reference, timestamp, generatingBalance, generator);
block.sign();
assertTrue(block.isSignatureValid());

View File

@ -172,7 +172,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, paymentTransaction.isValid());
// Forge new block with transaction
Block block = new Block(repository, parentBlockData, generator, null, null);
Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(paymentTransactionData);
block.sign();
@ -233,7 +233,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, registerNameTransaction.isValid());
// Forge new block with transaction
Block block = new Block(repository, parentBlockData, generator, null, null);
Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(registerNameTransactionData);
block.sign();
@ -289,7 +289,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, updateNameTransaction.isValid());
// Forge new block with transaction
Block block = new Block(repository, parentBlockData, generator, null, null);
Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(updateNameTransactionData);
block.sign();
@ -334,7 +334,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, sellNameTransaction.isValid());
// Forge new block with transaction
Block block = new Block(repository, parentBlockData, generator, null, null);
Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(sellNameTransactionData);
block.sign();
@ -385,7 +385,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, cancelSellNameTransaction.isValid());
// Forge new block with transaction
Block block = new Block(repository, parentBlockData, generator, null, null);
Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(cancelSellNameTransactionData);
block.sign();
@ -432,7 +432,7 @@ public class TransactionTests {
byte[] buyersReference = somePaymentTransaction.getTransactionData().getSignature();
// Forge new block with transaction
Block block = new Block(repository, parentBlockData, generator, null, null);
Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(somePaymentTransaction.getTransactionData());
block.sign();
@ -451,7 +451,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, buyNameTransaction.isValid());
// Forge new block with transaction
block = new Block(repository, parentBlockData, generator, null, null);
block = new Block(repository, parentBlockData, generator);
block.addTransaction(buyNameTransactionData);
block.sign();
@ -504,7 +504,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, createPollTransaction.isValid());
// Forge new block with transaction
Block block = new Block(repository, parentBlockData, generator, null, null);
Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(createPollTransactionData);
block.sign();
@ -563,7 +563,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, voteOnPollTransaction.isValid());
// Forge new block with transaction
Block block = new Block(repository, parentBlockData, generator, null, null);
Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(voteOnPollTransactionData);
block.sign();
@ -630,7 +630,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, issueAssetTransaction.isValid());
// Forge new block with transaction
Block block = new Block(repository, parentBlockData, generator, null, null);
Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(issueAssetTransactionData);
block.sign();
@ -720,7 +720,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, transferAssetTransaction.isValid());
// Forge new block with transaction
Block block = new Block(repository, parentBlockData, generator, null, null);
Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(transferAssetTransactionData);
block.sign();
@ -800,7 +800,7 @@ public class TransactionTests {
byte[] buyersReference = somePaymentTransaction.getTransactionData().getSignature();
// Forge new block with transaction
Block block = new Block(repository, parentBlockData, generator, null, null);
Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(somePaymentTransaction.getTransactionData());
block.sign();
@ -824,7 +824,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, createOrderTransaction.isValid());
// Forge new block with transaction
block = new Block(repository, parentBlockData, generator, null, null);
block = new Block(repository, parentBlockData, generator);
block.addTransaction(createOrderTransactionData);
block.sign();
@ -905,7 +905,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, cancelOrderTransaction.isValid());
// Forge new block with transaction
Block block = new Block(repository, parentBlockData, generator, null, null);
Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(cancelOrderTransactionData);
block.sign();
@ -980,7 +980,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, createOrderTransaction.isValid());
// Forge new block with transaction
Block block = new Block(repository, parentBlockData, generator, null, null);
Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(createOrderTransactionData);
block.sign();
@ -1089,7 +1089,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, multiPaymentTransaction.isValid());
// Forge new block with payment transaction
Block block = new Block(repository, parentBlockData, generator, null, null);
Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(multiPaymentTransactionData);
block.sign();
@ -1159,7 +1159,7 @@ public class TransactionTests {
assertEquals(ValidationResult.OK, messageTransaction.isValid());
// Forge new block with message transaction
Block block = new Block(repository, parentBlockData, generator, null, null);
Block block = new Block(repository, parentBlockData, generator);
block.addTransaction(messageTransactionData);
block.sign();

View File

@ -14,4 +14,7 @@ public abstract class Transformer {
public static final int SIGNATURE_LENGTH = 64;
public static final int TIMESTAMP_LENGTH = LONG_LENGTH;
public static final int MD5_LENGTH = 16;
public static final int SHA256_LENGTH = 32;
}

View File

@ -17,6 +17,7 @@ import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import data.assets.TradeData;
import data.at.ATStateData;
import data.block.BlockData;
import data.transaction.TransactionData;
import qora.account.PublicKeyAccount;
@ -24,12 +25,13 @@ import qora.assets.Order;
import qora.block.Block;
import qora.transaction.CreateOrderTransaction;
import qora.transaction.Transaction;
import qora.transaction.Transaction.TransactionType;
import repository.DataException;
import transform.TransformationException;
import transform.Transformer;
import transform.transaction.TransactionTransformer;
import utils.Base58;
import utils.Pair;
import utils.Triple;
import utils.Serialization;
public class BlockTransformer extends Transformer {
@ -52,6 +54,9 @@ public class BlockTransformer extends Transformer {
protected static final int AT_FEES_LENGTH = LONG_LENGTH;
protected static final int AT_LENGTH = AT_FEES_LENGTH + AT_BYTES_LENGTH;
protected static final int V2_AT_ENTRY_LENGTH = ADDRESS_LENGTH + MD5_LENGTH;
protected static final int V4_AT_ENTRY_LENGTH = ADDRESS_LENGTH + SHA256_LENGTH + BIG_DECIMAL_LENGTH;
/**
* Extract block data and transaction data from serialized bytes.
*
@ -59,7 +64,7 @@ public class BlockTransformer extends Transformer {
* @return BlockData and a List of transactions.
* @throws TransformationException
*/
public static Pair<BlockData, List<TransactionData>> fromBytes(byte[] bytes) throws TransformationException {
public static Triple<BlockData, List<TransactionData>, List<ATStateData>> fromBytes(byte[] bytes) throws TransformationException {
if (bytes == null)
return null;
@ -88,25 +93,74 @@ public class BlockTransformer extends Transformer {
byte[] generatorSignature = new byte[GENERATOR_SIGNATURE_LENGTH];
byteBuffer.get(generatorSignature);
byte[] atBytes = null;
BigDecimal atFees = null;
BigDecimal totalFees = BigDecimal.ZERO.setScale(8);
int atCount = 0;
BigDecimal atFees = BigDecimal.ZERO.setScale(8);
List<ATStateData> atStates = new ArrayList<ATStateData>();
if (version >= 2) {
int atBytesLength = byteBuffer.getInt();
if (atBytesLength > Block.MAX_BLOCK_BYTES)
throw new TransformationException("Byte data too long for Block's AT info");
atBytes = new byte[atBytesLength];
byteBuffer.get(atBytes);
ByteBuffer atByteBuffer = byteBuffer.slice();
atByteBuffer.limit(atBytesLength);
atFees = BigDecimal.valueOf(byteBuffer.getLong()).setScale(8);
if (version < 4) {
// For versions < 4, read AT-address & MD5 pairs
if (atBytesLength % V2_AT_ENTRY_LENGTH != 0)
throw new TransformationException("AT byte data not a multiple of version 2+ entries");
while (atByteBuffer.hasRemaining()) {
byte[] atAddressBytes = new byte[ADDRESS_LENGTH];
atByteBuffer.get(atAddressBytes);
String atAddress = Base58.encode(atAddressBytes);
byte[] stateHash = new byte[MD5_LENGTH];
atByteBuffer.get(stateHash);
atStates.add(new ATStateData(atAddress, stateHash));
}
// Bump byteBuffer over AT states just read in slice
byteBuffer.position(byteBuffer.position() + atBytesLength);
// AT fees follow in versions < 4
atFees = Serialization.deserializeBigDecimal(byteBuffer);
} else {
// For block versions >= 4, read AT-address, SHA256 hash and fees
if (atBytesLength % V4_AT_ENTRY_LENGTH != 0)
throw new TransformationException("AT byte data not a multiple of version 4+ entries");
while (atByteBuffer.hasRemaining()) {
byte[] atAddressBytes = new byte[ADDRESS_LENGTH];
atByteBuffer.get(atAddressBytes);
String atAddress = Base58.encode(atAddressBytes);
byte[] stateHash = new byte[SHA256_LENGTH];
atByteBuffer.get(stateHash);
BigDecimal fees = Serialization.deserializeBigDecimal(atByteBuffer);
// Add this AT's fees to our total
atFees = atFees.add(fees);
atStates.add(new ATStateData(atAddress, stateHash, fees));
}
}
// AT count to reflect the number of states we have
atCount = atStates.size();
// Add AT fees to totalFees
totalFees = totalFees.add(atFees);
}
int transactionCount = byteBuffer.getInt();
// Parse transactions now, compared to deferred parsing in Gen1, so we can throw ParseException if need be.
List<TransactionData> transactions = new ArrayList<TransactionData>();
BigDecimal totalFees = BigDecimal.ZERO.setScale(8);
for (int t = 0; t < transactionCount; ++t) {
if (byteBuffer.remaining() < TRANSACTION_SIZE_LENGTH)
@ -126,26 +180,28 @@ public class BlockTransformer extends Transformer {
TransactionData transactionData = TransactionTransformer.fromBytes(transactionBytes);
transactions.add(transactionData);
totalFees.add(transactionData.getFee());
totalFees = totalFees.add(transactionData.getFee());
}
if (byteBuffer.hasRemaining())
throw new TransformationException("Excess byte data found after parsing Block");
// XXX we don't know height!
int height = 0;
// We don't have a height!
Integer height = null;
BlockData blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance,
generatorPublicKey, generatorSignature, atBytes, atFees);
generatorPublicKey, generatorSignature, atCount, atFees);
return new Pair<BlockData, List<TransactionData>>(blockData, transactions);
return new Triple<BlockData, List<TransactionData>, List<ATStateData>>(blockData, transactions, atStates);
}
public static int getDataLength(Block block) throws TransformationException {
BlockData blockData = block.getBlockData();
int blockLength = BASE_LENGTH;
if (blockData.getVersion() >= 2 && blockData.getAtBytes() != null)
blockLength += AT_FEES_LENGTH + AT_BYTES_LENGTH + blockData.getAtBytes().length;
if (blockData.getVersion() >= 4)
blockLength += AT_BYTES_LENGTH + blockData.getATCount() * V4_AT_ENTRY_LENGTH;
else if (blockData.getVersion() >= 2)
blockLength += AT_FEES_LENGTH + AT_BYTES_LENGTH + blockData.getATCount() * V2_AT_ENTRY_LENGTH;
try {
// Short cut for no transactions
@ -177,18 +233,29 @@ public class BlockTransformer extends Transformer {
bytes.write(blockData.getTransactionsSignature());
bytes.write(blockData.getGeneratorSignature());
if (blockData.getVersion() >= 2) {
byte[] atBytes = blockData.getAtBytes();
if (blockData.getVersion() >= 4) {
int atBytesLength = blockData.getATCount() * V4_AT_ENTRY_LENGTH;
bytes.write(Ints.toByteArray(atBytesLength));
if (atBytes != null) {
bytes.write(Ints.toByteArray(atBytes.length));
bytes.write(atBytes);
// NOTE: atFees serialized as long value, not as BigDecimal, for historic compatibility
bytes.write(Longs.toByteArray(blockData.getAtFees().longValue()));
} else {
bytes.write(Ints.toByteArray(0));
bytes.write(Longs.toByteArray(0L));
for (ATStateData atStateData : block.getATStates()) {
bytes.write(Base58.decode(atStateData.getATAddress()));
bytes.write(atStateData.getStateHash());
Serialization.serializeBigDecimal(bytes, atStateData.getFees());
}
} else if (blockData.getVersion() >= 2) {
int atBytesLength = blockData.getATCount() * V2_AT_ENTRY_LENGTH;
bytes.write(Ints.toByteArray(atBytesLength));
for (ATStateData atStateData : block.getATStates()) {
bytes.write(Base58.decode(atStateData.getATAddress()));
bytes.write(atStateData.getStateHash());
}
if (blockData.getATFees() != null)
// NOTE: atFees serialized as long value, not as BigDecimal, for historic compatibility
bytes.write(Longs.toByteArray(blockData.getATFees().longValue()));
else
bytes.write(Longs.toByteArray(0));
}
// Transactions
@ -263,30 +330,39 @@ public class BlockTransformer extends Transformer {
json.put("assetTrades", tradesHappened);
// Add CIYAM AT info (if any)
if (blockData.getAtBytes() != null) {
json.put("blockATs", HashCode.fromBytes(blockData.getAtBytes()).toString());
json.put("atFees", blockData.getAtFees());
if (blockData.getATCount() > 0) {
JSONArray atsJson = new JSONArray();
try {
for (ATStateData atStateData : block.getATStates()) {
JSONObject atJson = new JSONObject();
atJson.put("AT", atStateData.getATAddress());
atJson.put("stateHash", HashCode.fromBytes(atStateData.getStateHash()).toString());
if (blockData.getVersion() >= 4)
atJson.put("fees", atStateData.getFees().toPlainString());
atsJson.add(atJson);
}
} catch (DataException e) {
throw new TransformationException("Unable to transform block into JSON", e);
}
json.put("ATs", atsJson);
if (blockData.getVersion() >= 2)
json.put("atFees", blockData.getATFees());
}
return json;
}
public static byte[] getBytesForGeneratorSignature(BlockData blockData) throws TransformationException {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH);
byte[] generatorSignature = Arrays.copyOf(blockData.getReference(), GENERATOR_SIGNATURE_LENGTH);
PublicKeyAccount generator = new PublicKeyAccount(null, blockData.getGeneratorPublicKey());
// Only copy the generator signature from reference, which is the first 64 bytes.
bytes.write(Arrays.copyOf(blockData.getReference(), GENERATOR_SIGNATURE_LENGTH));
bytes.write(Longs.toByteArray(blockData.getGeneratingBalance().longValue()));
// We're padding here just in case the generator is the genesis account whose public key is only 8 bytes long.
bytes.write(Bytes.ensureCapacity(blockData.getGeneratorPublicKey(), GENERATOR_LENGTH, 0));
return bytes.toByteArray();
} catch (IOException e) {
throw new TransformationException(e);
}
return getBytesForGeneratorSignature(generatorSignature, blockData.getGeneratingBalance(), generator);
}
public static byte[] getBytesForGeneratorSignature(byte[] generatorSignature, BigDecimal generatingBalance, PublicKeyAccount generator)
@ -308,13 +384,18 @@ public class BlockTransformer extends Transformer {
}
public static byte[] getBytesForTransactionsSignature(Block block) throws TransformationException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(
GENERATOR_SIGNATURE_LENGTH + block.getBlockData().getTransactionCount() * TransactionTransformer.SIGNATURE_LENGTH);
try {
List<Transaction> transactions = block.getTransactions();
ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + transactions.size() * TransactionTransformer.SIGNATURE_LENGTH);
bytes.write(block.getBlockData().getGeneratorSignature());
for (Transaction transaction : block.getTransactions()) {
for (Transaction transaction : transactions) {
// For legacy blocks, we don't include AT-Transactions
if (block.getBlockData().getVersion() < 4 && transaction.getTransactionData().getType() == TransactionType.AT)
continue;
if (!transaction.isSignatureValid())
throw new TransformationException("Transaction signature invalid when building block's transactions signature");

View File

@ -0,0 +1,92 @@
package transform.transaction;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.json.simple.JSONObject;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import data.transaction.TransactionData;
import data.transaction.ATTransactionData;
import transform.TransformationException;
import utils.Serialization;
public class ATTransactionTransformer extends TransactionTransformer {
// Property lengths
private static final int SENDER_LENGTH = ADDRESS_LENGTH;
private static final int RECIPIENT_LENGTH = ADDRESS_LENGTH;
private static final int AMOUNT_LENGTH = BIG_DECIMAL_LENGTH;
private static final int ASSET_ID_LENGTH = LONG_LENGTH;
private static final int DATA_SIZE_LENGTH = INT_LENGTH;
private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH + ASSET_ID_LENGTH + DATA_SIZE_LENGTH;
static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
throw new TransformationException("Serialized AT Transactions should not exist!");
}
public static int getDataLength(TransactionData transactionData) throws TransformationException {
ATTransactionData atTransactionData = (ATTransactionData) transactionData;
return TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + atTransactionData.getMessage().length;
}
public static byte[] toBytes(TransactionData transactionData) throws TransformationException {
try {
ATTransactionData atTransactionData = (ATTransactionData) transactionData;
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(Ints.toByteArray(atTransactionData.getType().value));
bytes.write(Longs.toByteArray(atTransactionData.getTimestamp()));
bytes.write(atTransactionData.getReference());
Serialization.serializeAddress(bytes, atTransactionData.getATAddress());
Serialization.serializeAddress(bytes, atTransactionData.getRecipient());
if (atTransactionData.getAssetId() != null) {
Serialization.serializeBigDecimal(bytes, atTransactionData.getAmount());
bytes.write(Longs.toByteArray(atTransactionData.getAssetId()));
}
byte[] message = atTransactionData.getMessage();
if (message.length > 0) {
bytes.write(Ints.toByteArray(message.length));
bytes.write(message);
} else {
bytes.write(Ints.toByteArray(0));
}
Serialization.serializeBigDecimal(bytes, atTransactionData.getFee());
if (atTransactionData.getSignature() != null)
bytes.write(atTransactionData.getSignature());
return bytes.toByteArray();
} catch (IOException | ClassCastException e) {
throw new TransformationException(e);
}
}
@SuppressWarnings("unchecked")
public static JSONObject toJSON(TransactionData transactionData) throws TransformationException {
JSONObject json = TransactionTransformer.getBaseJSON(transactionData);
try {
ATTransactionData atTransactionData = (ATTransactionData) transactionData;
json.put("sender", atTransactionData.getATAddress());
} catch (ClassCastException e) {
throw new TransformationException(e);
}
return json;
}
}

View File

@ -29,4 +29,22 @@ public class Pair<T, U> {
return b;
}
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Pair<?, ?>))
return false;
Pair<?, ?> other = (Pair<?, ?>) o;
return this.a.equals(other.getA()) && this.b.equals(other.getB());
}
@Override
public int hashCode() {
return this.a.hashCode() ^ this.b.hashCode();
}
}

42
src/utils/Triple.java Normal file
View File

@ -0,0 +1,42 @@
package utils;
public class Triple<T, U, V> {
private T a;
private U b;
private V c;
public Triple() {
}
public Triple(T a, U b, V c) {
this.a = a;
this.b = b;
this.c = c;
}
public void setA(T a) {
this.a = a;
}
public T getA() {
return a;
}
public void setB(U b) {
this.b = b;
}
public U getB() {
return b;
}
public void setC(V c) {
this.c = c;
}
public V getC() {
return c;
}
}

View File

@ -1,7 +1,9 @@
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
@ -9,17 +11,32 @@ import java.net.SocketTimeoutException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.json.simple.parser.ParseException;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Ints;
import data.at.ATData;
import data.at.ATStateData;
import data.block.BlockData;
import data.transaction.ATTransactionData;
import data.transaction.TransactionData;
import qora.assets.Asset;
import qora.block.Block;
import qora.block.Block.ValidationResult;
import qora.block.BlockChain;
@ -29,7 +46,10 @@ import repository.Repository;
import repository.RepositoryManager;
import transform.TransformationException;
import transform.block.BlockTransformer;
import transform.transaction.ATTransactionTransformer;
import utils.Base58;
import utils.Pair;
import utils.Triple;
public class v1feeder extends Thread {
@ -77,6 +97,9 @@ public class v1feeder extends Thread {
private long lastPingTimestamp = System.currentTimeMillis();
private List<byte[]> signatures = new ArrayList<byte[]>();
private static Map<Pair<String, Integer>, BigDecimal> legacyATFees;
private static Map<Integer, List<TransactionData>> legacyATTransactions;
private v1feeder(String address, int port) throws InterruptedException {
try {
for (int i = 0; i < 10; ++i)
@ -177,6 +200,9 @@ public class v1feeder extends Thread {
// shove into list
int numSignatures = byteBuffer.getInt();
if (numSignatures == 0)
throw new RuntimeException("No signatures from peer - are we up to date?");
while (numSignatures-- > 0) {
byte[] signature = new byte[SIGNATURE_LENGTH];
byteBuffer.get(signature);
@ -201,7 +227,7 @@ public class v1feeder extends Thread {
byte[] blockBytes = new byte[byteBuffer.remaining()];
byteBuffer.get(blockBytes);
Pair<BlockData, List<TransactionData>> blockInfo = null;
Triple<BlockData, List<TransactionData>, List<ATStateData>> blockInfo = null;
try {
blockInfo = BlockTransformer.fromBytes(blockBytes);
@ -211,7 +237,26 @@ public class v1feeder extends Thread {
}
try (final Repository repository = RepositoryManager.getRepository()) {
Block block = new Block(repository, blockInfo.getA(), blockInfo.getB());
BlockData blockData = blockInfo.getA();
// Adjust AT state data to include fees
List<ATStateData> atStates = new ArrayList<ATStateData>();
for (ATStateData atState : blockInfo.getC()) {
BigDecimal fees = legacyATFees.get(new Pair<String, Integer>(atState.getATAddress(), claimedHeight));
ATData atData = repository.getATRepository().fromATAddress(atState.getATAddress());
atStates.add(new ATStateData(atState.getATAddress(), claimedHeight, atData.getCreation(), null, atState.getStateHash(), fees));
}
// AT-Transaction injection goes here!
List<TransactionData> transactions = blockInfo.getB();
List<TransactionData> atTransactions = legacyATTransactions.get(claimedHeight);
if (atTransactions != null) {
transactions.addAll(0, atTransactions);
blockData.setTransactionCount(blockData.getTransactionCount() + atTransactions.size());
}
Block block = new Block(repository, blockData, transactions, atStates);
if (!block.isSignatureValid()) {
LOGGER.error("Invalid block signature");
@ -398,12 +443,88 @@ public class v1feeder extends Thread {
}
}
private static void readLegacyATs(String legacyATPathname) {
legacyATFees = new HashMap<Pair<String, Integer>, BigDecimal>();
legacyATTransactions = new HashMap<Integer, List<TransactionData>>();
Path path = Paths.get(legacyATPathname);
JSONArray json = null;
try (BufferedReader in = Files.newBufferedReader(path)) {
json = (JSONArray) JSONValue.parseWithException(in);
} catch (IOException | ParseException e) {
throw new RuntimeException("Couldn't read legacy AT JSON file");
}
for (Object o : json) {
JSONObject entry = (JSONObject) o;
int height = Integer.parseInt((String) entry.get("height"));
long timestamp = (Long) entry.get("timestamp");
JSONArray transactionEntries = (JSONArray) entry.get("transactions");
List<TransactionData> transactions = new ArrayList<TransactionData>();
for (Object t : transactionEntries) {
JSONObject transactionEntry = (JSONObject) t;
String recipient = (String) transactionEntry.get("recipient");
String sender = (String) transactionEntry.get("sender");
BigDecimal amount = new BigDecimal((String) transactionEntry.get("amount")).setScale(8);
if (recipient.equals("1111111111111111111111111")) {
// fee
legacyATFees.put(new Pair<String, Integer>(sender, height), amount);
} else {
// Actual AT Transaction
String messageString = (String) transactionEntry.get("message");
byte[] message = messageString.isEmpty() ? new byte[0] : HashCode.fromString(messageString).asBytes();
int sequence = ((Long) transactionEntry.get("seq")).intValue();
byte[] reference = Base58.decode((String) transactionEntry.get("reference"));
// reference is AT's deploy tx signature
// sender's public key is genesis account
// zero fee
// timestamp is block's timestamp
// signature = duplicated hash of transaction data
BigDecimal fee = BigDecimal.ZERO.setScale(8);
TransactionData transactionData = new ATTransactionData(sender, recipient, amount, Asset.QORA, message, fee, timestamp, reference);
byte[] digest;
try {
digest = Crypto.digest(ATTransactionTransformer.toBytes(transactionData));
byte[] signature = Bytes.concat(digest, digest);
transactionData = new ATTransactionData(sender, recipient, amount, Asset.QORA, message, fee, timestamp, reference, signature);
} catch (TransformationException e) {
throw new RuntimeException("Couldn't transform AT Transaction into bytes", e);
}
if (sequence > transactions.size())
transactions.add(transactionData);
else
transactions.add(sequence, transactionData);
}
}
if (!transactions.isEmpty())
legacyATTransactions.put(height, transactions);
}
}
public static void main(String[] args) {
if (args.length == 0) {
System.err.println("usage: v1feeder v1-node-address [port]");
if (args.length < 2 || args.length > 3) {
System.err.println("usage: v1feeder legacy-AT-json v1-node-address [port]");
System.err.println("example: v1feeder legacy-ATs.json 10.0.0.100 9084");
System.exit(1);
}
String legacyATPathname = args[0];
readLegacyATs(legacyATPathname);
try {
test.Common.setRepository();
} catch (DataException e) {
@ -419,8 +540,8 @@ public class v1feeder extends Thread {
}
// connect to v1 node
String address = args[0];
int port = args.length > 1 ? Integer.valueOf(args[1]) : DEFAULT_PORT;
String address = args[1];
int port = args.length > 2 ? Integer.valueOf(args[2]) : DEFAULT_PORT;
try {
new v1feeder(address, port).join();