Merge branch 'master' into master

This commit is contained in:
catbref 2018-10-31 09:40:27 +00:00 committed by GitHub
commit 24ae771867
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1350 additions and 350 deletions

Binary file not shown.

View File

@ -1 +1 @@
1d6f5d634a2c4e570a5a8af260a51653
ab1560171ae5c6c15b0dfa8e6cccc7f8

View File

@ -1 +1 @@
c6387380bc5db1f0a98ecbb480b17bd89b564401
c293c9656f43b432a08053f19ec5aa0de1cd10ea

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>org.ciyam</groupId>
<artifactId>at</artifactId>
<versioning>
<release>1.0</release>
<versions>
<version>1.0</version>
</versions>
<lastUpdated>20181015085522</lastUpdated>
</versioning>
</metadata>

View File

@ -7,6 +7,6 @@
<versions>
<version>1.0</version>
</versions>
<lastUpdated>20181003154752</lastUpdated>
<lastUpdated>20181015081124</lastUpdated>
</versioning>
</metadata>

View File

@ -1 +1 @@
bc81bc1f9b74a4eececd5dd8b29e47d8
2369bf36c52580a89d5ea71a0f037a82

View File

@ -1 +1 @@
feefde4343bda4d6e13159e5c01f8b4f8963a1bc
6bc38899b93ffce2286ae26f7af0b2d8b69db3cf

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

@ -13,18 +13,18 @@ public class BlockData implements Serializable {
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;
private BlockData() {} // necessary for JAX-RS serialization
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;
@ -35,7 +35,7 @@ public class BlockData implements Serializable {
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)
@ -44,6 +44,26 @@ public class BlockData implements Serializable {
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;
}
@ -68,31 +88,11 @@ public class BlockData implements Serializable {
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;
}
@ -116,12 +116,20 @@ public class BlockData implements Serializable {
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,36 +2,40 @@ 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;
private byte[] message;
// Constructors
public ATTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, byte[] message, BigDecimal fee, long timestamp, byte[] reference,
byte[] signature) {
super(TransactionType.AT, fee, senderPublicKey, timestamp, reference, signature);
public ATTransactionData(String atAddress, String recipient, BigDecimal amount, Long assetId, byte[] message, BigDecimal fee, long timestamp,
byte[] reference, byte[] 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, byte[] message, BigDecimal fee, long timestamp, byte[] reference) {
this(senderPublicKey, recipient, amount, message, fee, timestamp, reference, null);
public ATTransactionData(String atAddress, String recipient, BigDecimal amount, Long assetId, byte[] message, BigDecimal fee, long timestamp,
byte[] reference) {
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() {
@ -42,6 +46,10 @@ public class ATTransactionData extends TransactionData {
return this.amount;
}
public Long getAssetId() {
return this.assetId;
}
public byte[] getMessage() {
return this.message;
}

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,10 +1,15 @@
package qora.at;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
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;
@ -23,26 +28,61 @@ public class AT {
this.atStateData = atStateData;
}
/** Deploying AT */
public AT(Repository repository, DeployATTransactionData deployATTransactionData) throws DataException {
this.repository = repository;
MachineState machineState = new MachineState(deployATTransactionData.getCreationBytes());
String atAddress = deployATTransactionData.getATAddress();
int height = this.repository.getBlockRepository().getBlockchainHeight() + 1;
String creator = new PublicKeyAccount(repository, deployATTransactionData.getCreatorPublicKey()).getAddress();
long creation = deployATTransactionData.getTimestamp();
this.atData = new ATData(deployATTransactionData.getATAddress(), machineState.version, machineState.codeByteBuffer.array(), machineState.isSleeping,
machineState.sleepUntilHeight, machineState.isFinished, machineState.hadFatalError, machineState.isFrozen, machineState.frozenBalance,
deployATTransactionData.getSignature());
byte[] creationBytes = deployATTransactionData.getCreationBytes();
short version = (short) (creationBytes[0] | (creationBytes[1] << 8)); // Little-endian
String atAddress = this.atData.getATAddress();
if (version >= 2) {
MachineState machineState = new MachineState(deployATTransactionData.getCreationBytes());
int height = this.repository.getBlockRepository().getBlockchainHeight();
byte[] stateData = machineState.toBytes();
this.atData = new ATData(atAddress, creator, creation, machineState.version, machineState.getCodeBytes(), machineState.getIsSleeping(),
machineState.getSleepUntilHeight(), machineState.getIsFinished(), machineState.getHadFatalError(), machineState.getIsFrozen(),
machineState.getFrozenBalance());
this.atStateData = new ATStateData(atAddress, height, stateData);
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
ByteBuffer byteBuffer = ByteBuffer.wrap(deployATTransactionData.getCreationBytes());
short numCodePages = byteBuffer.get(2 + 2);
byteBuffer.position(6 * 2 + 8);
int codeLen = 0;
if (numCodePages * 256 < 257) {
codeLen = (int) (byteBuffer.get() & 0xff);
} else if (numCodePages * 256 < Short.MAX_VALUE + 1) {
codeLen = byteBuffer.getShort() & 0xffff;
} else if (numCodePages * 256 <= Integer.MAX_VALUE) {
codeLen = byteBuffer.getInt();
}
// Extract code bytes
byte[] codeBytes = new byte[codeLen];
byteBuffer.get(codeBytes);
this.atData = new ATData(atAddress, creator, creation, 1, codeBytes, false, null, true, false, false, (Long) null);
this.atStateData = new ATStateData(atAddress, height, creation, null, null, BigDecimal.ZERO.setScale(8));
}
}
// Processing
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
@ -612,7 +737,7 @@ public class Block {
Transaction.ValidationResult validationResult = transaction.isValid();
if (validationResult != Transaction.ValidationResult.OK) {
LOGGER.error("Error during transaction validation, tx " + Base58.encode(transaction.getTransactionData().getSignature()) + ": "
+ validationResult.value);
+ validationResult.name());
return ValidationResult.TRANSACTION_INVALID;
}
@ -643,8 +768,47 @@ public class Block {
return ValidationResult.OK;
}
/**
* Execute CIYAM ATs for this block.
* <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

@ -0,0 +1,179 @@
package qora.transaction;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import data.assets.AssetData;
import data.transaction.ATTransactionData;
import data.transaction.TransactionData;
import qora.account.Account;
import qora.assets.Asset;
import qora.crypto.Crypto;
import repository.DataException;
import repository.Repository;
public class ATTransaction extends Transaction {
// Properties
private ATTransactionData atTransactionData;
// Other useful constants
public static final int MAX_DATA_SIZE = 256;
// Constructors
public ATTransaction(Repository repository, TransactionData transactionData) {
super(repository, transactionData);
this.atTransactionData = (ATTransactionData) this.transactionData;
}
// More information
@Override
public List<Account> getRecipientAccounts() throws DataException {
return Collections.singletonList(new Account(this.repository, this.atTransactionData.getRecipient()));
}
@Override
public boolean isInvolved(Account account) throws DataException {
String address = account.getAddress();
if (address.equals(this.atTransactionData.getATAddress()))
return true;
if (address.equals(this.atTransactionData.getRecipient()))
return true;
return false;
}
@Override
public BigDecimal getAmount(Account account) throws DataException {
String address = account.getAddress();
BigDecimal amount = BigDecimal.ZERO.setScale(8);
String atAddress = this.atTransactionData.getATAddress();
if (address.equals(atAddress)) {
amount = amount.subtract(this.atTransactionData.getFee());
if (this.atTransactionData.getAmount() != null && this.atTransactionData.getAssetId() == Asset.QORA)
amount = amount.subtract(this.atTransactionData.getAmount());
}
if (address.equals(this.atTransactionData.getRecipient()) && this.atTransactionData.getAmount() != null)
amount = amount.add(this.atTransactionData.getAmount());
return amount;
}
// Navigation
public Account getATAccount() throws DataException {
return new Account(this.repository, this.atTransactionData.getATAddress());
}
public Account getRecipient() throws DataException {
return new Account(this.repository, this.atTransactionData.getRecipient());
}
// Processing
@Override
public ValidationResult isValid() throws DataException {
// Check reference is correct
Account atAccount = getATAccount();
if (!Arrays.equals(atAccount.getLastReference(), atTransactionData.getReference()))
return ValidationResult.INVALID_REFERENCE;
if (this.atTransactionData.getMessage().length > MAX_DATA_SIZE)
return ValidationResult.INVALID_DATA_LENGTH;
BigDecimal amount = this.atTransactionData.getAmount();
// If we have no payment then we're done
if (amount == null)
return ValidationResult.OK;
// Check amount is zero or positive
if (amount.compareTo(BigDecimal.ZERO) < 0)
return ValidationResult.NEGATIVE_AMOUNT;
// Check recipient address is valid
if (!Crypto.isValidAddress(this.atTransactionData.getRecipient()))
return ValidationResult.INVALID_ADDRESS;
long assetId = this.atTransactionData.getAssetId();
AssetData assetData = this.repository.getAssetRepository().fromAssetId(assetId);
// Check asset even exists
if (assetData == null)
return ValidationResult.ASSET_DOES_NOT_EXIST;
// Check asset amount is integer if asset is not divisible
if (!assetData.getIsDivisible() && amount.stripTrailingZeros().scale() > 0)
return ValidationResult.INVALID_AMOUNT;
Account sender = getATAccount();
// Check sender has enough of asset
if (sender.getConfirmedBalance(assetId).compareTo(amount) < 0)
return ValidationResult.NO_BALANCE;
return ValidationResult.OK;
}
@Override
public void process() throws DataException {
// Save this transaction itself
this.repository.getTransactionRepository().save(this.transactionData);
if (this.atTransactionData.getAmount() != null) {
Account sender = getATAccount();
Account recipient = getRecipient();
long assetId = this.atTransactionData.getAssetId();
BigDecimal amount = this.atTransactionData.getAmount();
// Update sender's balance due to amount
sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).subtract(amount));
// Update recipient's balance
recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).add(amount));
// For QORA amounts only: if recipient has no reference yet, then this is their starting reference
if (assetId == Asset.QORA && recipient.getLastReference() == null)
// In Qora1 last reference was set to 64-bytes of zero
// In Qora2 we use AT-Transction's signature, which makes more sense
recipient.setLastReference(this.atTransactionData.getSignature());
}
}
@Override
public void orphan() throws DataException {
// Delete this transaction
this.repository.getTransactionRepository().delete(this.transactionData);
if (this.atTransactionData.getAmount() != null) {
Account sender = getATAccount();
Account recipient = getRecipient();
long assetId = this.atTransactionData.getAssetId();
BigDecimal amount = this.atTransactionData.getAmount();
// Update sender's balance due to amount
sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).add(amount));
// Update recipient's balance
recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).subtract(amount));
/*
* For QORA amounts only: If recipient's last reference is this transaction's signature, then they can't have made any transactions of their own
* (which would have changed their last reference) thus this is their first reference so remove it.
*/
if (assetId == Asset.QORA && Arrays.equals(recipient.getLastReference(), this.atTransactionData.getSignature()))
recipient.setLastReference(null);
}
}
}

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

@ -75,6 +75,13 @@ public class DeployATTransaction extends Transaction {
return amount;
}
/** Returns AT version from the header bytes */
private short getVersion() {
byte[] creationBytes = deployATTransactionData.getCreationBytes();
short version = (short) (creationBytes[0] | (creationBytes[1] << 8)); // Little-endian
return version;
}
/** Make sure deployATTransactionData has an ATAddress */
private void ensureATAddress() throws DataException {
if (this.deployATTransactionData.getATAddress() != null)
@ -82,7 +89,7 @@ public class DeployATTransaction extends Transaction {
int blockHeight = this.getHeight();
if (blockHeight == 0)
blockHeight = this.repository.getBlockRepository().getBlockchainHeight();
blockHeight = this.repository.getBlockRepository().getBlockchainHeight() + 1;
try {
byte[] name = this.deployATTransactionData.getName().getBytes("UTF-8");
@ -163,11 +170,8 @@ public class DeployATTransaction extends Transaction {
if (creator.getConfirmedBalance(Asset.QORA).compareTo(minimumBalance) < 0)
return ValidationResult.NO_BALANCE;
// Check creation bytes are valid (for v3+)
byte[] creationBytes = deployATTransactionData.getCreationBytes();
short version = (short) (creationBytes[0] | (creationBytes[1] << 8)); // Little-endian
if (version >= 3) {
// Check creation bytes are valid (for v2+)
if (this.getVersion() >= 2) {
// Do actual validation
} else {
// Skip validation for old, dead ATs
@ -194,6 +198,13 @@ public class DeployATTransaction extends Transaction {
// Update creator's reference
creator.setLastReference(deployATTransactionData.getSignature());
// Update AT's reference, which also creates AT account
Account atAccount = this.getATAccount();
atAccount.setLastReference(deployATTransactionData.getSignature());
// Update AT's balance
atAccount.setConfirmedBalance(Asset.QORA, deployATTransactionData.getAmount());
}
@Override
@ -212,6 +223,9 @@ public class DeployATTransaction extends Transaction {
// Update creator's reference
creator.setLastReference(deployATTransactionData.getReference());
// Delete AT's account
this.repository.getAccountRepository().delete(this.deployATTransactionData.getATAddress());
}
}

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

@ -193,11 +193,14 @@ public abstract class Transaction {
case MULTIPAYMENT:
return new MultiPaymentTransaction(repository, transactionData);
case DEPLOY_AT:
return new DeployATTransaction(repository, transactionData);
case MESSAGE:
return new MessageTransaction(repository, transactionData);
case DEPLOY_AT:
return new DeployATTransaction(repository, transactionData);
case AT:
return new ATTransaction(repository, transactionData);
default:
throw new IllegalStateException("Unsupported transaction type [" + transactionData.getType().value + "] during fetch from repository");

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,10 +7,14 @@ public interface AccountRepository {
// General account
public void create(String address) throws DataException;
public AccountData getAccount(String address) throws DataException;
public void save(AccountData accountData) throws DataException;
public void delete(String address) throws DataException;
// Account balances
public AccountBalanceData getBalance(String address, long assetId) throws DataException;

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);
@ -69,7 +75,7 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public void delete(String atAddress) throws DataException {
try {
this.repository.delete("ATs", "atAddress = ?", atAddress);
this.repository.delete("ATs", "AT_address = ?", atAddress);
// AT States also deleted via ON DELETE CASCADE
} catch (SQLException e) {
throw new DataException("Unable to delete AT from repository", e);
@ -80,28 +86,63 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public ATStateData getATState(String atAddress, int height) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT state_data FROM ATStates WHERE AT_address = ? AND height = ?", atAddress, height)) {
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT creation, state_data, state_hash, fees FROM ATStates WHERE AT_address = ? AND height = ?", atAddress, height)) {
if (resultSet == null)
return null;
byte[] stateData = resultSet.getBytes(1); // Actually BLOB
long creation = resultSet.getTimestamp(1, Calendar.getInstance(HSQLDBRepository.UTC)).getTime();
byte[] stateData = resultSet.getBytes(2); // Actually BLOB
byte[] stateHash = resultSet.getBytes(3);
BigDecimal fees = resultSet.getBigDecimal(4);
return new ATStateData(atAddress, height, stateData);
return new ATStateData(atAddress, height, creation, stateData, stateHash, fees);
} catch (SQLException e) {
throw new DataException("Unable to fetch AT State from repository", e);
throw new DataException("Unable to fetch AT state from repository", e);
}
}
@Override
public List<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

@ -17,6 +17,21 @@ public class HSQLDBAccountRepository implements AccountRepository {
this.repository = repository;
}
// General account
@Override
public void create(String address) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts");
saveHelper.bind("account", address);
try {
saveHelper.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to create account in repository", e);
}
}
@Override
public AccountData getAccount(String address) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT reference FROM Accounts WHERE account = ?", address)) {
@ -32,6 +47,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
@Override
public void save(AccountData accountData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts");
saveHelper.bind("account", accountData.getAddress()).bind("reference", accountData.getReference());
try {
@ -41,6 +57,19 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
}
@Override
public void delete(String address) throws DataException {
// NOTE: Account balances are deleted automatically by the database thanks to "ON DELETE CASCADE" in AccountBalances' FOREIGN KEY
// definition.
try {
this.repository.delete("Accounts", "account = ?", address);
} catch (SQLException e) {
throw new DataException("Unable to delete account from repository", e);
}
}
// Account balances
@Override
public AccountBalanceData getBalance(String address, long assetId) throws DataException {
try (ResultSet resultSet = this.repository.checkedExecute("SELECT balance FROM AccountBalances WHERE account = ? and asset_id = ?", address, assetId)) {
@ -58,6 +87,7 @@ public class HSQLDBAccountRepository implements AccountRepository {
@Override
public void save(AccountBalanceData accountBalanceData) throws DataException {
HSQLDBSaver saveHelper = new HSQLDBSaver("AccountBalances");
saveHelper.bind("account", accountBalanceData.getAddress()).bind("asset_id", accountBalanceData.getAssetId()).bind("balance",
accountBalanceData.getBalance());

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");
@ -99,6 +99,7 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE TYPE ATType AS VARCHAR(200) COLLATE SQL_TEXT_UCC_NO_PAD");
stmt.execute("CREATE TYPE ATCode AS BLOB(64K)"); // 16bit * 1
stmt.execute("CREATE TYPE ATState AS BLOB(1M)"); // 16bit * 8 + 16bit * 4 + 16bit * 4
stmt.execute("CREATE TYPE ATStateHash as VARBINARY(32)");
stmt.execute("CREATE TYPE ATMessage AS VARBINARY(256)");
break;
@ -107,7 +108,7 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE TABLE Blocks (signature BlockSignature PRIMARY KEY, version TINYINT NOT NULL, reference BlockSignature, "
+ "transaction_count INTEGER NOT NULL, total_fees QoraAmount NOT NULL, transactions_signature Signature NOT NULL, "
+ "height INTEGER NOT NULL, generation TIMESTAMP WITH TIME ZONE NOT NULL, generating_balance QoraAmount NOT NULL, "
+ "generator QoraPublicKey NOT NULL, generator_signature Signature NOT NULL, AT_data VARBINARY(20000), AT_fees QoraAmount)");
+ "generator QoraPublicKey NOT NULL, generator_signature Signature NOT NULL, AT_count INTEGER NOT NULL, AT_fees QoraAmount NOT NULL)");
// For finding blocks by height.
stmt.execute("CREATE INDEX BlockHeightIndex ON Blocks (height)");
// For finding blocks by the account that generated them.
@ -302,7 +303,7 @@ public class HSQLDBDatabaseUpdates {
// Accounts
stmt.execute("CREATE TABLE Accounts (account QoraAddress, reference Signature, PRIMARY KEY (account))");
stmt.execute("CREATE TABLE AccountBalances (account QoraAddress, asset_id AssetID, balance QoraAmount NOT NULL, "
+ "PRIMARY KEY (account, asset_id))");
+ "PRIMARY KEY (account, asset_id), FOREIGN KEY (account) REFERENCES Accounts (account) ON DELETE CASCADE)");
break;
case 23:
@ -352,17 +353,21 @@ public class HSQLDBDatabaseUpdates {
case 27:
// CIYAM Automated Transactions
stmt.execute("CREATE TABLE ATs (AT_address QoraAddress, version INTEGER NOT NULL, code_bytes ATCode NOT NULL, "
+ "is_sleeping BOOLEAN NOT NULL, sleep_until_height INTEGER, is_finished BOOLEAN NOT NULL, had_fatal_error BOOLEAN NOT NULL, "
+ "is_frozen BOOLEAN NOT NULL, frozen_balance QoraAmount, deploy_signature Signature NOT NULL, PRIMARY key (AT_address))");
// For finding executable ATs
stmt.execute("CREATE INDEX ATIndex on ATs (is_finished, AT_address)");
stmt.execute("CREATE TABLE ATs (AT_address QoraAddress, creator QoraAddress, creation TIMESTAMP WITH TIME ZONE, version INTEGER NOT NULL, "
+ "code_bytes ATCode NOT NULL, is_sleeping BOOLEAN NOT NULL, sleep_until_height INTEGER, "
+ "is_finished BOOLEAN NOT NULL, had_fatal_error BOOLEAN NOT NULL, is_frozen BOOLEAN NOT NULL, frozen_balance QoraAmount, "
+ "PRIMARY key (AT_address))");
// For finding executable ATs, ordered by creation timestamp
stmt.execute("CREATE INDEX ATIndex on ATs (is_finished, creation, AT_address)");
// AT state on a per-block basis
stmt.execute("CREATE TABLE ATStates (AT_address QoraAddress, height INTEGER NOT NULL, state_data ATState, "
stmt.execute("CREATE TABLE ATStates (AT_address QoraAddress, height INTEGER NOT NULL, creation TIMESTAMP WITH TIME ZONE, "
+ "state_data ATState, state_hash ATStateHash NOT NULL, fees QoraAmount NOT NULL, "
+ "PRIMARY KEY (AT_address, height), FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
// For finding per-block AT states, ordered by creation timestamp
stmt.execute("CREATE INDEX BlockATStateIndex on ATStates (height, creation, AT_address)");
// Generated AT Transactions
stmt.execute(
"CREATE TABLE ATTransactions (signature Signature, sender QoraPublicKey NOT NULL, recipient QoraAddress, amount QoraAmount NOT NULL, message ATMessage, "
"CREATE TABLE ATTransactions (signature Signature, AT_address QoraAddress NOT NULL, recipient QoraAddress, amount QoraAmount, asset_id AssetID, message ATMessage, "
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
break;

View File

@ -0,0 +1,63 @@
package repository.hsqldb.transaction;
import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import data.transaction.ATTransactionData;
import data.transaction.TransactionData;
import repository.DataException;
import repository.hsqldb.HSQLDBRepository;
import repository.hsqldb.HSQLDBSaver;
public class HSQLDBATTransactionRepository extends HSQLDBTransactionRepository {
public HSQLDBATTransactionRepository(HSQLDBRepository repository) {
this.repository = repository;
}
TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException {
try (ResultSet resultSet = this.repository
.checkedExecute("SELECT AT_address, recipient, amount, asset_id, message FROM ATTransactions WHERE signature = ?", signature)) {
if (resultSet == null)
return null;
String atAddress = resultSet.getString(1);
String recipient = resultSet.getString(2);
BigDecimal amount = resultSet.getBigDecimal(3);
if (resultSet.wasNull())
amount = null;
Long assetId = resultSet.getLong(4);
if (resultSet.wasNull())
assetId = null;
byte[] message = resultSet.getBytes(5);
if (resultSet.wasNull())
message = null;
return new ATTransactionData(atAddress, recipient, amount, assetId, message, fee, timestamp, reference, signature);
} catch (SQLException e) {
throw new DataException("Unable to fetch AT transaction from repository", e);
}
}
@Override
public void save(TransactionData transactionData) throws DataException {
ATTransactionData atTransactionData = (ATTransactionData) transactionData;
HSQLDBSaver saveHelper = new HSQLDBSaver("ATTransactions");
saveHelper.bind("signature", atTransactionData.getSignature()).bind("AT_address", atTransactionData.getATAddress())
.bind("recipient", atTransactionData.getRecipient()).bind("amount", atTransactionData.getAmount())
.bind("asset_id", atTransactionData.getAssetId()).bind("message", atTransactionData.getMessage());
try {
saveHelper.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to save AT transaction into repository", e);
}
}
}

View File

@ -37,6 +37,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
private HSQLDBMultiPaymentTransactionRepository multiPaymentTransactionRepository;
private HSQLDBDeployATTransactionRepository deployATTransactionRepository;
private HSQLDBMessageTransactionRepository messageTransactionRepository;
private HSQLDBATTransactionRepository atTransactionRepository;
public HSQLDBTransactionRepository(HSQLDBRepository repository) {
this.repository = repository;
@ -57,6 +58,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
this.multiPaymentTransactionRepository = new HSQLDBMultiPaymentTransactionRepository(repository);
this.deployATTransactionRepository = new HSQLDBDeployATTransactionRepository(repository);
this.messageTransactionRepository = new HSQLDBMessageTransactionRepository(repository);
this.atTransactionRepository = new HSQLDBATTransactionRepository(repository);
}
protected HSQLDBTransactionRepository() {
@ -154,18 +156,30 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
case MESSAGE:
return this.messageTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee);
case AT:
return this.atTransactionRepository.fromBase(signature, reference, creatorPublicKey, timestamp, fee);
default:
throw new DataException("Unsupported transaction type [" + type.value + "] during fetch from HSQLDB repository");
throw new DataException("Unsupported transaction type [" + type.name() + "] during fetch from HSQLDB repository");
}
}
/**
* Returns payments associated with a transaction's signature.
* <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 {
@ -317,8 +331,12 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
this.messageTransactionRepository.save(transactionData);
break;
case AT:
this.atTransactionRepository.save(transactionData);
break;
default:
throw new DataException("Unsupported transaction type [" + transactionData.getType().value + "] during save into HSQLDB repository");
throw new DataException("Unsupported transaction type [" + transactionData.getType().name() + "] during save into HSQLDB repository");
}
}

View File

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

View File

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

View File

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

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

@ -97,7 +97,9 @@ public class DeployATTransactionTransformer extends TransactionTransformer {
Serialization.serializeSizedString(bytes, deployATTransactionData.getTags());
bytes.write(deployATTransactionData.getCreationBytes());
byte[] creationBytes = deployATTransactionData.getCreationBytes();
bytes.write(Ints.toByteArray(creationBytes.length));
bytes.write(creationBytes);
Serialization.serializeBigDecimal(bytes, deployATTransactionData.getAmount());
@ -146,15 +148,14 @@ public class DeployATTransactionTransformer extends TransactionTransformer {
// Omitted: Serialization.serializeSizedString(bytes, deployATTransactionData.getTags());
bytes.write(deployATTransactionData.getCreationBytes());
byte[] creationBytes = deployATTransactionData.getCreationBytes();
bytes.write(Ints.toByteArray(creationBytes.length));
bytes.write(creationBytes);
Serialization.serializeBigDecimal(bytes, deployATTransactionData.getAmount());
Serialization.serializeBigDecimal(bytes, deployATTransactionData.getFee());
if (deployATTransactionData.getSignature() != null)
bytes.write(deployATTransactionData.getSignature());
return bytes.toByteArray();
} catch (IOException | ClassCastException e) {
throw new TransformationException(e);

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");
@ -221,8 +266,8 @@ public class v1feeder extends Thread {
ValidationResult result = block.isValid();
if (result != ValidationResult.OK) {
LOGGER.error("Invalid block, validation result code: " + result.value);
throw new RuntimeException("Invalid block, validation result code: " + result.value);
LOGGER.error("Invalid block, validation result: " + result.name());
throw new RuntimeException("Invalid block, validation result: " + result.name());
}
block.process();
@ -398,12 +443,88 @@ public class v1feeder extends Thread {
}
}
private static void readLegacyATs(String legacyATPathname) {
legacyATFees = new HashMap<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();