diff --git a/src/data/at/ATData.java b/src/data/at/ATData.java index bf8272e6..41278c51 100644 --- a/src/data/at/ATData.java +++ b/src/data/at/ATData.java @@ -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; - } - } diff --git a/src/data/at/ATStateData.java b/src/data/at/ATStateData.java index 192c8095..7d0fc6f4 100644 --- a/src/data/at/ATStateData.java +++ b/src/data/at/ATStateData.java @@ -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; + } + } diff --git a/src/data/block/BlockData.java b/src/data/block/BlockData.java index 8c49b71b..838b347e 100644 --- a/src/data/block/BlockData.java +++ b/src/data/block/BlockData.java @@ -12,16 +12,16 @@ public class BlockData { private int transactionCount; private BigDecimal totalFees; private byte[] transactionsSignature; - private int height; + private Integer height; private long timestamp; private BigDecimal generatingBalance; private byte[] generatorPublicKey; private byte[] generatorSignature; - private byte[] atBytes; + private int atCount; private BigDecimal atFees; - public BlockData(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, int height, long timestamp, - BigDecimal generatingBalance, byte[] generatorPublicKey, byte[] generatorSignature, byte[] atBytes, BigDecimal atFees) { + public BlockData(int version, byte[] reference, int transactionCount, BigDecimal totalFees, byte[] transactionsSignature, Integer height, long timestamp, + BigDecimal generatingBalance, byte[] generatorPublicKey, byte[] generatorSignature, int atCount, BigDecimal atFees) { this.version = version; this.reference = reference; this.transactionCount = transactionCount; @@ -32,7 +32,7 @@ public class BlockData { this.generatingBalance = generatingBalance; this.generatorPublicKey = generatorPublicKey; this.generatorSignature = generatorSignature; - this.atBytes = atBytes; + this.atCount = atCount; this.atFees = atFees; if (this.generatorSignature != null && this.transactionsSignature != null) @@ -41,6 +41,26 @@ public class BlockData { this.signature = null; } + public byte[] getSignature() { + return this.signature; + } + + public void setSignature(byte[] signature) { + this.signature = signature; + } + + public int getVersion() { + return this.version; + } + + public byte[] getReference() { + return this.reference; + } + + public void setReference(byte[] reference) { + this.reference = reference; + } + public int getTransactionCount() { return this.transactionCount; } @@ -65,31 +85,11 @@ public class BlockData { this.transactionsSignature = transactionsSignature; } - public byte[] getSignature() { - return this.signature; - } - - public void setSignature(byte[] signature) { - this.signature = signature; - } - - public int getVersion() { - return this.version; - } - - public byte[] getReference() { - return this.reference; - } - - public void setReference(byte[] reference) { - this.reference = reference; - } - - public int getHeight() { + public Integer getHeight() { return this.height; } - public void setHeight(int height) { + public void setHeight(Integer height) { this.height = height; } @@ -113,12 +113,20 @@ public class BlockData { this.generatorSignature = generatorSignature; } - public byte[] getAtBytes() { - return this.atBytes; + public int getATCount() { + return this.atCount; } - public BigDecimal getAtFees() { + public void setATCount(int atCount) { + this.atCount = atCount; + } + + public BigDecimal getATFees() { return this.atFees; } + public void setATFees(BigDecimal atFees) { + this.atFees = atFees; + } + } diff --git a/src/data/block/BlockTransactionData.java b/src/data/block/BlockTransactionData.java index fb62079a..fe0410f3 100644 --- a/src/data/block/BlockTransactionData.java +++ b/src/data/block/BlockTransactionData.java @@ -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 diff --git a/src/data/transaction/ATTransactionData.java b/src/data/transaction/ATTransactionData.java index 8d9f99de..e81dc099 100644 --- a/src/data/transaction/ATTransactionData.java +++ b/src/data/transaction/ATTransactionData.java @@ -2,12 +2,13 @@ package data.transaction; import java.math.BigDecimal; +import qora.account.GenesisAccount; import qora.transaction.Transaction.TransactionType; public class ATTransactionData extends TransactionData { // Properties - private byte[] senderPublicKey; + private String atAddress; private String recipient; private BigDecimal amount; private Long assetId; @@ -15,26 +16,26 @@ public class ATTransactionData extends TransactionData { // Constructors - public ATTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, Long assetId, byte[] message, BigDecimal fee, long timestamp, + public ATTransactionData(String atAddress, String recipient, BigDecimal amount, Long assetId, byte[] message, BigDecimal fee, long timestamp, byte[] reference, byte[] signature) { - super(TransactionType.AT, fee, senderPublicKey, timestamp, reference, signature); + super(TransactionType.AT, fee, GenesisAccount.PUBLIC_KEY, timestamp, reference, signature); - this.senderPublicKey = senderPublicKey; + this.atAddress = atAddress; this.recipient = recipient; this.amount = amount; this.assetId = assetId; this.message = message; } - public ATTransactionData(byte[] senderPublicKey, String recipient, BigDecimal amount, Long assetId, byte[] message, BigDecimal fee, long timestamp, + public ATTransactionData(String atAddress, String recipient, BigDecimal amount, Long assetId, byte[] message, BigDecimal fee, long timestamp, byte[] reference) { - this(senderPublicKey, recipient, amount, assetId, message, fee, timestamp, reference, null); + this(atAddress, recipient, amount, assetId, message, fee, timestamp, reference, null); } // Getters/Setters - public byte[] getSenderPublicKey() { - return this.senderPublicKey; + public String getATAddress() { + return this.atAddress; } public String getRecipient() { diff --git a/src/qora/account/Account.java b/src/qora/account/Account.java index bbf03dfd..c7a14425 100644 --- a/src/qora/account/Account.java +++ b/src/qora/account/Account.java @@ -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, AT_Transaction> atTxs = db.getATTransactionMap().getATTransactions(block.getHeight(db)); - * Iterator 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, AT_Transaction> atTxs = - * db.getATTransactionMap().getATTransactions(block.getHeight(db)); Iterator 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); diff --git a/src/qora/account/GenesisAccount.java b/src/qora/account/GenesisAccount.java index a989ec4a..34b85b60 100644 --- a/src/qora/account/GenesisAccount.java +++ b/src/qora/account/GenesisAccount.java @@ -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); } diff --git a/src/qora/account/PublicKeyAccount.java b/src/qora/account/PublicKeyAccount.java index 2762cd6f..1f28e39b 100644 --- a/src/qora/account/PublicKeyAccount.java +++ b/src/qora/account/PublicKeyAccount.java @@ -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; diff --git a/src/qora/assets/Order.java b/src/qora/assets/Order.java index 56b15cea..e164b53c 100644 --- a/src/qora/assets/Order.java +++ b/src/qora/assets/Order.java @@ -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! diff --git a/src/qora/at/AT.java b/src/qora/at/AT.java index 292ff62b..1eb8e762 100644 --- a/src/qora/at/AT.java +++ b/src/qora/at/AT.java @@ -1,5 +1,6 @@ package qora.at; +import java.math.BigDecimal; import java.nio.ByteBuffer; import org.ciyam.at.MachineState; @@ -7,6 +8,8 @@ import org.ciyam.at.MachineState; import data.at.ATData; import data.at.ATStateData; import data.transaction.DeployATTransactionData; +import qora.account.PublicKeyAccount; +import qora.crypto.Crypto; import repository.DataException; import repository.Repository; @@ -25,11 +28,14 @@ public class AT { this.atStateData = atStateData; } + /** Deploying AT */ public AT(Repository repository, DeployATTransactionData deployATTransactionData) throws DataException { this.repository = repository; String atAddress = deployATTransactionData.getATAddress(); - int height = this.repository.getBlockRepository().getBlockchainHeight(); + int height = this.repository.getBlockRepository().getBlockchainHeight() + 1; + String creator = new PublicKeyAccount(repository, deployATTransactionData.getCreatorPublicKey()).getAddress(); + long creation = deployATTransactionData.getTimestamp(); byte[] creationBytes = deployATTransactionData.getCreationBytes(); short version = (short) (creationBytes[0] | (creationBytes[1] << 8)); // Little-endian @@ -37,11 +43,14 @@ public class AT { if (version >= 2) { MachineState machineState = new MachineState(deployATTransactionData.getCreationBytes()); - this.atData = new ATData(atAddress, machineState.version, machineState.getCodeBytes(), machineState.getIsSleeping(), + this.atData = new ATData(atAddress, creator, creation, machineState.version, machineState.getCodeBytes(), machineState.getIsSleeping(), machineState.getSleepUntilHeight(), machineState.getIsFinished(), machineState.getHadFatalError(), machineState.getIsFrozen(), - machineState.getFrozenBalance(), deployATTransactionData.getSignature()); + machineState.getFrozenBalance()); - this.atStateData = new ATStateData(atAddress, height, machineState.toBytes()); + byte[] stateData = machineState.toBytes(); + byte[] stateHash = Crypto.digest(stateData); + + this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, BigDecimal.ZERO.setScale(8)); } else { // Legacy v1 AT in 'dead' state // Extract code bytes length @@ -64,10 +73,9 @@ public class AT { byte[] codeBytes = new byte[codeLen]; byteBuffer.get(codeBytes); - this.atData = new ATData(deployATTransactionData.getATAddress(), 1, codeBytes, false, null, true, false, false, (Long) null, - deployATTransactionData.getSignature()); + this.atData = new ATData(atAddress, creator, creation, 1, codeBytes, false, null, true, false, false, (Long) null); - this.atStateData = new ATStateData(deployATTransactionData.getATAddress(), height, null); + this.atStateData = new ATStateData(atAddress, height, creation, null, null, BigDecimal.ZERO.setScale(8)); } } @@ -75,7 +83,6 @@ public class AT { public void deploy() throws DataException { this.repository.getATRepository().save(this.atData); - this.repository.getATRepository().save(this.atStateData); } public void undeploy() throws DataException { diff --git a/src/qora/block/Block.java b/src/qora/block/Block.java index 44fdea7d..1f000e1b 100644 --- a/src/qora/block/Block.java +++ b/src/qora/block/Block.java @@ -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 transactions; + + protected List atStates; + protected List ourAtStates; // Generated locally + protected BigDecimal ourAtFees; // Generated locally + protected BigDecimal cachedNextGeneratingBalance; // Other useful constants @@ -104,39 +113,92 @@ public class Block { // Constructors + /** + * Constructs Block-handling object without loading transactions and AT states. + *

+ * Transactions and AT states are loaded on first call to getTransactions() or getATStates() respectively. + * + * @param repository + * @param blockData + * @throws DataException + */ public Block(Repository repository, BlockData blockData) throws DataException { this.repository = repository; this.blockData = blockData; this.generator = new PublicKeyAccount(repository, blockData.getGeneratorPublicKey()); } - // When receiving a block over network? - public Block(Repository repository, BlockData blockData, List transactions) throws DataException { + /** + * Constructs Block-handling object using passed transaction and AT states. + *

+ * This constructor typically used when receiving a serialized block over the network. + * + * @param repository + * @param blockData + * @param transactions + * @param atStates + * @throws DataException + */ + public Block(Repository repository, BlockData blockData, List transactions, List atStates) throws DataException { this(repository, blockData); this.transactions = new ArrayList(); + BigDecimal totalFees = BigDecimal.ZERO.setScale(8); + // We have to sum fees too for (TransactionData transactionData : transactions) { this.transactions.add(Transaction.fromData(repository, transactionData)); - this.blockData.setTotalFees(this.blockData.getTotalFees().add(transactionData.getFee())); + totalFees = totalFees.add(transactionData.getFee()); } + + this.atStates = atStates; + for (ATStateData atState : atStates) + totalFees = totalFees.add(atState.getFees()); + + this.blockData.setTotalFees(totalFees); } - // For creating a new block? - public Block(Repository repository, int version, byte[] reference, long timestamp, BigDecimal generatingBalance, PrivateKeyAccount generator, - byte[] atBytes, BigDecimal atFees) { + /** + * Constructs Block-handling object with basic, initial values. + *

+ * This constructor typically used when generating a new block. + *

+ * Note that CIYAM ATs will be executed and AT-Transactions prepended to this block, along with AT state data and fees. + * + * @param repository + * @param version + * @param reference + * @param timestamp + * @param generatingBalance + * @param generator + * @throws DataException + */ + public Block(Repository repository, int version, byte[] reference, long timestamp, BigDecimal generatingBalance, PrivateKeyAccount generator) + throws DataException { this.repository = repository; this.generator = generator; - this.blockData = new BlockData(version, reference, 0, BigDecimal.ZERO.setScale(8), null, 0, timestamp, generatingBalance, generator.getPublicKey(), - null, atBytes, atFees); + int transactionCount = 0; + byte[] transactionsSignature = null; + Integer height = null; + byte[] generatorSignature = null; + + this.executeATs(); + + int atCount = this.ourAtStates.size(); + BigDecimal atFees = this.ourAtFees; + BigDecimal totalFees = atFees; + + this.blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance, + generator.getPublicKey(), generatorSignature, atCount, atFees); this.transactions = new ArrayList(); + this.atStates = this.ourAtStates; } /** Construct a new block for use in tests */ - public Block(Repository repository, BlockData parentBlockData, PrivateKeyAccount generator, byte[] atBytes, BigDecimal atFees) throws DataException { + public Block(Repository repository, BlockData parentBlockData, PrivateKeyAccount generator) throws DataException { this.repository = repository; this.generator = generator; @@ -155,11 +217,18 @@ public class Block { } long timestamp = parentBlock.calcNextBlockTimestamp(version, generatorSignature, generator); + int transactionCount = 0; + BigDecimal totalFees = BigDecimal.ZERO.setScale(8); + byte[] transactionsSignature = null; + int height = parentBlockData.getHeight() + 1; + int atCount = 0; + BigDecimal atFees = BigDecimal.ZERO.setScale(8); - this.blockData = new BlockData(version, reference, 0, BigDecimal.ZERO.setScale(8), null, 0, timestamp, generatingBalance, generator.getPublicKey(), - generatorSignature, atBytes, atFees); + this.blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance, + generator.getPublicKey(), generatorSignature, atCount, atFees); this.transactions = new ArrayList(); + this.atStates = new ArrayList(); } // Getters/setters @@ -192,12 +261,17 @@ public class Block { * @return 1, 2 or 3 */ public int getNextBlockVersion() { + if (this.blockData.getHeight() == null) + throw new IllegalStateException("Can't determine next block's version as this block has no height set"); + if (this.blockData.getHeight() < BlockChain.getATReleaseHeight()) return 1; else if (this.blockData.getTimestamp() < BlockChain.getPowFixReleaseTimestamp()) return 2; - else + else if (this.blockData.getTimestamp() < BlockChain.getDeployATV2Timestamp()) return 3; + else + return 4; } /** @@ -212,8 +286,8 @@ public class Block { * @throws DataException */ public BigDecimal calcNextBlockGeneratingBalance() throws DataException { - if (this.blockData.getHeight() == 0) - throw new IllegalStateException("Block height is unset"); + if (this.blockData.getHeight() == null) + throw new IllegalStateException("Can't calculate next block's generating balance as this block's height is unset"); // This block not at the start of an interval? if (this.blockData.getHeight() % BlockChain.BLOCK_RETARGET_INTERVAL != 0) @@ -346,7 +420,7 @@ public class Block { /** * Return block's transactions. *

- * If the block was loaded from repository then it's possible this method will call the repository to load the transactions if they are not already loaded. + * If the block was loaded from repository then it's possible this method will call the repository to fetch the transactions if not done already. * * @return * @throws DataException @@ -371,6 +445,37 @@ public class Block { return this.transactions; } + /** + * Return block's AT states. + *

+ * If the block was loaded from repository then it's possible this method will call the repository to fetch the AT states if not done already. + *

+ * Note: AT states fetched from repository only contain summary info, not actual data like serialized state data or AT creation timestamps! + * + * @return + * @throws DataException + */ + public List getATStates() throws DataException { + // Already loaded? + if (this.atStates != null) + return this.atStates; + + // If loading from repository, this block must have a height + if (this.blockData.getHeight() == null) + throw new IllegalStateException("Can't fetch block's AT states from repository without a block height"); + + // Allocate cache for results + List atStateData = this.repository.getATRepository().getBlockATStatesFromHeight(this.blockData.getHeight()); + + // The number of AT states fetched from repository should correspond with Block's atCount + if (atStateData.size() != this.blockData.getATCount()) + throw new IllegalStateException("Block's AT states from repository do not match block's AT count"); + + this.atStates = atStateData; + + return this.atStates; + } + // Navigation /** @@ -531,8 +636,6 @@ public class Block { * @throws DataException */ public ValidationResult isValid() throws DataException { - // TODO - // Check parent block exists if (this.blockData.getReference() == null) return ValidationResult.REFERENCE_MISSING; @@ -543,6 +646,10 @@ public class Block { Block parentBlock = new Block(this.repository, parentBlockData); + // Check parent doesn't already have a child block + if (parentBlock.getChild() != null) + return ValidationResult.PARENT_HAS_EXISTING_CHILD; + // Check timestamp is newer than parent timestamp if (this.blockData.getTimestamp() <= parentBlockData.getTimestamp()) return ValidationResult.TIMESTAMP_OLDER_THAN_PARENT; @@ -558,7 +665,7 @@ public class Block { // Check block version if (this.blockData.getVersion() != parentBlock.getNextBlockVersion()) return ValidationResult.VERSION_INCORRECT; - if (this.blockData.getVersion() < 2 && (this.blockData.getAtBytes() != null || this.blockData.getAtFees() != null)) + if (this.blockData.getVersion() < 2 && this.blockData.getATCount() != 0) return ValidationResult.FEATURE_NOT_YET_RELEASED; // Check generating balance @@ -583,16 +690,34 @@ public class Block { if (hashValue.compareTo(lowerTarget) < 0) return ValidationResult.GENERATOR_NOT_ACCEPTED; - // Process CIYAM ATs, prepending AT-Transactions to block then compare post-execution checksums - // XXX We should pre-calculate, and cache, next block's AT-transactions after processing each block to save repeated work - if (this.blockData.getAtBytes() != null && this.blockData.getAtBytes().length > 0) { - // TODO - // try { - // AT_Block atBlock = AT_Controller.validateATs(this.getBlockATs(), BlockChain.getHeight() + 1); - // this.atFees = atBlock.getTotalFees(); - // } catch (NoSuchAlgorithmException | AT_Exception e) { - // return false; - // } + // CIYAM ATs + if (this.blockData.getATCount() != 0) { + // Locally generated AT states should be valid so no need to re-execute them + if (this.ourAtStates != this.getATStates()) { + // Otherwise, check locally generated AT states against ones received from elsewhere? + this.executeATs(); + + if (this.ourAtStates.size() != this.blockData.getATCount()) + return ValidationResult.AT_STATES_MISMATCH; + + if (this.ourAtFees.compareTo(this.blockData.getATFees()) != 0) + return ValidationResult.AT_STATES_MISMATCH; + + // Note: this.atStates fully loaded thanks to this.getATStates() call above + for (int s = 0; s < this.atStates.size(); ++s) { + ATStateData ourAtState = this.ourAtStates.get(s); + ATStateData theirAtState = this.atStates.get(s); + + if (!ourAtState.getATAddress().equals(theirAtState.getATAddress())) + return ValidationResult.AT_STATES_MISMATCH; + + if (!ourAtState.getStateHash().equals(theirAtState.getStateHash())) + return ValidationResult.AT_STATES_MISMATCH; + + if (ourAtState.getFees().compareTo(theirAtState.getFees()) != 0) + return ValidationResult.AT_STATES_MISMATCH; + } + } } // Check transactions @@ -643,8 +768,47 @@ public class Block { return ValidationResult.OK; } + /** + * Execute CIYAM ATs for this block. + *

+ * This needs to be done locally for all blocks, regardless of origin.
+ * This method is called by isValid. + *

+ * After calling, AT-generated transactions are prepended to the block's transactions and AT state data is generated. + *

+ * This method is not needed if fetching an existing block from the repository. + *

+ * Updates this.ourAtStates and this.ourAtFees. + * + * @see #isValid() + * + * @throws DataException + * + */ + public void executeATs() throws DataException { + // We're expecting a lack of AT state data at this point. + if (this.ourAtStates != null) + throw new IllegalStateException("Attempted to execute ATs when block's local AT state data already exists"); + + // For old v1 CIYAM ATs we blindly accept them + if (this.blockData.getVersion() < 4) { + this.ourAtStates = this.atStates; + this.ourAtFees = this.blockData.getATFees(); + return; + } + + // Find all executable ATs, ordered by earliest creation date first + + // Run each AT, appends AT-Transactions and corresponding AT states, to our lists + + // Finally prepend our entire AT-Transactions/states to block's transactions/states, adjust fees, etc. + + // Note: store locally-calculated AT states separately to this.atStates so we can compare them in isValid() + } + public void process() throws DataException { // Process transactions (we'll link them to this block after saving the block itself) + // AT-generated transactions are already added to our transactions so no special handling is needed here. List transactions = this.getTransactions(); for (Transaction transaction : transactions) transaction.process(); @@ -654,6 +818,17 @@ public class Block { if (blockFee.compareTo(BigDecimal.ZERO) > 0) this.generator.setConfirmedBalance(Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).add(blockFee)); + // Process AT fees and save AT states into repository + ATRepository atRepository = this.repository.getATRepository(); + for (ATStateData atState : this.getATStates()) { + Account atAccount = new Account(this.repository, atState.getATAddress()); + + // Subtract AT-generated fees from AT accounts + atAccount.setConfirmedBalance(Asset.QORA, atAccount.getConfirmedBalance(Asset.QORA).subtract(atState.getFees())); + + atRepository.save(atState); + } + // Link block into blockchain by fetching signature of highest block and setting that as our reference int blockchainHeight = this.repository.getBlockRepository().getBlockchainHeight(); BlockData latestBlockData = this.repository.getBlockRepository().fromHeight(blockchainHeight); @@ -675,12 +850,8 @@ public class Block { } public void orphan() throws DataException { - // TODO - - // Orphan block's CIYAM ATs - orphanAutomatedTransactions(); - // Orphan transactions in reverse order, and unlink them from this block + // AT-generated transactions are already added to our transactions so no special handling is needed here. List transactions = this.getTransactions(); for (int sequence = transactions.size() - 1; sequence >= 0; --sequence) { Transaction transaction = transactions.get(sequence); @@ -696,25 +867,19 @@ public class Block { if (blockFee.compareTo(BigDecimal.ZERO) > 0) this.generator.setConfirmedBalance(Asset.QORA, this.generator.getConfirmedBalance(Asset.QORA).subtract(blockFee)); + // Return AT fees and delete AT states from repository + ATRepository atRepository = this.repository.getATRepository(); + for (ATStateData atState : this.getATStates()) { + Account atAccount = new Account(this.repository, atState.getATAddress()); + + // Return AT-generated fees to AT accounts + atAccount.setConfirmedBalance(Asset.QORA, atAccount.getConfirmedBalance(Asset.QORA).add(atState.getFees())); + } + // Delete ATStateData for this height + atRepository.deleteATStates(this.blockData.getHeight()); + // Delete block from blockchain this.repository.getBlockRepository().delete(this.blockData); } - public void orphanAutomatedTransactions() throws DataException { - // TODO - CIYAM AT support - /* - * LinkedHashMap< Tuple2 , AT_Transaction > atTxs = DBSet.getInstance().getATTransactionMap().getATTransactions(this.getHeight(db)); - * - * Iterator iter = atTxs.values().iterator(); - * - * while ( iter.hasNext() ) { AT_Transaction key = iter.next(); Long amount = key.getAmount(); if (key.getRecipientId() != null && - * !Arrays.equals(key.getRecipientId(), new byte[ AT_Constants.AT_ID_SIZE ]) && !key.getRecipient().equalsIgnoreCase("1") ) { Account recipient = new - * Account( key.getRecipient() ); recipient.setConfirmedBalance( recipient.getConfirmedBalance( db ).subtract( BigDecimal.valueOf( amount, 8 ) ) , db ); - * if ( Arrays.equals(recipient.getLastReference(db),new byte[64])) { recipient.removeReference(db); } } Account sender = new Account( key.getSender() - * ); sender.setConfirmedBalance( sender.getConfirmedBalance( db ).add( BigDecimal.valueOf( amount, 8 ) ) , db ); - * - * } - */ - } - } diff --git a/src/qora/block/GenesisBlock.java b/src/qora/block/GenesisBlock.java index 9daab7b6..0d309dd4 100644 --- a/src/qora/block/GenesisBlock.java +++ b/src/qora/block/GenesisBlock.java @@ -34,7 +34,7 @@ public class GenesisBlock extends Block { public GenesisBlock(Repository repository) throws DataException { super(repository, new BlockData(GENESIS_BLOCK_VERSION, GENESIS_REFERENCE, 0, BigDecimal.ZERO.setScale(8), GENESIS_TRANSACTIONS_SIGNATURE, 1, - Settings.getInstance().getGenesisTimestamp(), GENESIS_GENERATING_BALANCE, GENESIS_GENERATOR_PUBLIC_KEY, GENESIS_GENERATOR_SIGNATURE, null, null)); + Settings.getInstance().getGenesisTimestamp(), GENESIS_GENERATING_BALANCE, GENESIS_GENERATOR_PUBLIC_KEY, GENESIS_GENERATOR_SIGNATURE, 0, BigDecimal.ZERO.setScale(8))); this.transactions = new ArrayList(); diff --git a/src/qora/payment/Payment.java b/src/qora/payment/Payment.java index fbf0899f..66e2175d 100644 --- a/src/qora/payment/Payment.java +++ b/src/qora/payment/Payment.java @@ -46,31 +46,31 @@ public class Payment { amountsByAssetId.put(Asset.QORA, fee); // Check payments, and calculate amount total by assetId - if (payments != null) - for (PaymentData paymentData : payments) { - // Check amount is positive - if (paymentData.getAmount().compareTo(BigDecimal.ZERO) < 0) - return ValidationResult.NEGATIVE_AMOUNT; + for (PaymentData paymentData : payments) { + // Check amount is zero or positive + if (paymentData.getAmount().compareTo(BigDecimal.ZERO) < 0) + return ValidationResult.NEGATIVE_AMOUNT; - // Optional zero-amount check - if (!isZeroAmountValid && paymentData.getAmount().compareTo(BigDecimal.ZERO) <= 0) - return ValidationResult.NEGATIVE_AMOUNT; + // Optional zero-amount check + if (!isZeroAmountValid && paymentData.getAmount().compareTo(BigDecimal.ZERO) <= 0) + return ValidationResult.NEGATIVE_AMOUNT; - // Check recipient address is valid - if (!Crypto.isValidAddress(paymentData.getRecipient())) - return ValidationResult.INVALID_ADDRESS; + // Check recipient address is valid + if (!Crypto.isValidAddress(paymentData.getRecipient())) + return ValidationResult.INVALID_ADDRESS; - AssetData assetData = assetRepository.fromAssetId(paymentData.getAssetId()); - // Check asset even exists - if (assetData == null) - return ValidationResult.ASSET_DOES_NOT_EXIST; + AssetData assetData = assetRepository.fromAssetId(paymentData.getAssetId()); + // Check asset even exists + if (assetData == null) + return ValidationResult.ASSET_DOES_NOT_EXIST; - // Check asset amount is integer if asset is not divisible - if (!assetData.getIsDivisible() && paymentData.getAmount().stripTrailingZeros().scale() > 0) - return ValidationResult.INVALID_AMOUNT; + // Check asset amount is integer if asset is not divisible + if (!assetData.getIsDivisible() && paymentData.getAmount().stripTrailingZeros().scale() > 0) + return ValidationResult.INVALID_AMOUNT; - amountsByAssetId.compute(paymentData.getAssetId(), (assetId, amount) -> amount == null ? amount : amount.add(paymentData.getAmount())); - } + // Set or add amount into amounts-by-asset map + amountsByAssetId.compute(paymentData.getAssetId(), (assetId, amount) -> amount == null ? amount : amount.add(paymentData.getAmount())); + } // Check sender has enough of each asset Account sender = new PublicKeyAccount(this.repository, senderPublicKey); @@ -87,7 +87,7 @@ public class Payment { // Single payment forms public ValidationResult isValid(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee, boolean isZeroAmountValid) throws DataException { - return isValid(senderPublicKey, Collections.singletonList(paymentData), fee); + return isValid(senderPublicKey, Collections.singletonList(paymentData), fee, isZeroAmountValid); } public ValidationResult isValid(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee) throws DataException { @@ -105,22 +105,22 @@ public class Payment { sender.setLastReference(signature); // Process all payments - if (payments != null) - for (PaymentData paymentData : payments) { - Account recipient = new Account(this.repository, paymentData.getRecipient()); - long assetId = paymentData.getAssetId(); - BigDecimal amount = paymentData.getAmount(); + for (PaymentData paymentData : payments) { + Account recipient = new Account(this.repository, paymentData.getRecipient()); - // Update sender's balance due to amount - sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).subtract(amount)); + long assetId = paymentData.getAssetId(); + BigDecimal amount = paymentData.getAmount(); - // Update recipient's balance - recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).add(amount)); + // Update sender's balance due to amount + sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).subtract(amount)); - // For QORA amounts only: if recipient has no reference yet, then this is their starting reference - if ((alwaysInitializeRecipientReference || assetId == Asset.QORA) && recipient.getLastReference() == null) - recipient.setLastReference(signature); - } + // Update recipient's balance + recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).add(amount)); + + // For QORA amounts only: if recipient has no reference yet, then this is their starting reference + if ((alwaysInitializeRecipientReference || assetId == Asset.QORA) && recipient.getLastReference() == null) + recipient.setLastReference(signature); + } } public void process(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee, byte[] signature, boolean alwaysInitializeRecipientReference) @@ -139,25 +139,24 @@ public class Payment { sender.setLastReference(reference); // Orphan all payments - if (payments != null) - for (PaymentData paymentData : payments) { - Account recipient = new Account(this.repository, paymentData.getRecipient()); - long assetId = paymentData.getAssetId(); - BigDecimal amount = paymentData.getAmount(); + for (PaymentData paymentData : payments) { + Account recipient = new Account(this.repository, paymentData.getRecipient()); + long assetId = paymentData.getAssetId(); + BigDecimal amount = paymentData.getAmount(); - // Update sender's balance due to amount - sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).add(amount)); + // Update sender's balance due to amount + sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).add(amount)); - // Update recipient's balance - recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).subtract(amount)); + // Update recipient's balance + recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).subtract(amount)); - /* - * For QORA amounts only: If recipient's last reference is this transaction's signature, then they can't have made any transactions of their own - * (which would have changed their last reference) thus this is their first reference so remove it. - */ - if ((alwaysUninitializeRecipientReference || assetId == Asset.QORA) && Arrays.equals(recipient.getLastReference(), signature)) - recipient.setLastReference(null); - } + /* + * For QORA amounts only: If recipient's last reference is this transaction's signature, then they can't have made any transactions of their own + * (which would have changed their last reference) thus this is their first reference so remove it. + */ + if ((alwaysUninitializeRecipientReference || assetId == Asset.QORA) && Arrays.equals(recipient.getLastReference(), signature)) + recipient.setLastReference(null); + } } public void orphan(byte[] senderPublicKey, PaymentData paymentData, BigDecimal fee, byte[] signature, byte[] reference, diff --git a/src/qora/transaction/ATTransaction.java b/src/qora/transaction/ATTransaction.java index e3be08e9..790ba578 100644 --- a/src/qora/transaction/ATTransaction.java +++ b/src/qora/transaction/ATTransaction.java @@ -5,13 +5,12 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import data.PaymentData; +import data.assets.AssetData; import data.transaction.ATTransactionData; import data.transaction.TransactionData; import qora.account.Account; -import qora.account.PublicKeyAccount; import qora.assets.Asset; -import qora.payment.Payment; +import qora.crypto.Crypto; import repository.DataException; import repository.Repository; @@ -35,17 +34,17 @@ public class ATTransaction extends Transaction { @Override public List getRecipientAccounts() throws DataException { - return Collections.singletonList(new Account(this.repository, atTransactionData.getRecipient())); + return Collections.singletonList(new Account(this.repository, this.atTransactionData.getRecipient())); } @Override public boolean isInvolved(Account account) throws DataException { String address = account.getAddress(); - if (address.equals(this.getSender().getAddress())) + if (address.equals(this.atTransactionData.getATAddress())) return true; - if (address.equals(atTransactionData.getRecipient())) + if (address.equals(this.atTransactionData.getRecipient())) return true; return false; @@ -55,52 +54,73 @@ public class ATTransaction extends Transaction { public BigDecimal getAmount(Account account) throws DataException { String address = account.getAddress(); BigDecimal amount = BigDecimal.ZERO.setScale(8); - String senderAddress = this.getSender().getAddress(); + String atAddress = this.atTransactionData.getATAddress(); - if (address.equals(senderAddress)) { + if (address.equals(atAddress)) { amount = amount.subtract(this.atTransactionData.getFee()); - if (atTransactionData.getAmount() != null && atTransactionData.getAssetId() == Asset.QORA) - amount = amount.subtract(atTransactionData.getAmount()); + if (this.atTransactionData.getAmount() != null && this.atTransactionData.getAssetId() == Asset.QORA) + amount = amount.subtract(this.atTransactionData.getAmount()); } - if (address.equals(atTransactionData.getRecipient()) && atTransactionData.getAmount() != null) - amount = amount.add(atTransactionData.getAmount()); + if (address.equals(this.atTransactionData.getRecipient()) && this.atTransactionData.getAmount() != null) + amount = amount.add(this.atTransactionData.getAmount()); return amount; } // Navigation - public Account getSender() throws DataException { - return new PublicKeyAccount(this.repository, this.atTransactionData.getSenderPublicKey()); + public Account getATAccount() throws DataException { + return new Account(this.repository, this.atTransactionData.getATAddress()); + } + + public Account getRecipient() throws DataException { + return new Account(this.repository, this.atTransactionData.getRecipient()); } // Processing - private PaymentData getPaymentData() { - if (atTransactionData.getAmount() == null) - return null; - - return new PaymentData(atTransactionData.getRecipient(), atTransactionData.getAssetId(), atTransactionData.getAmount()); - } - @Override public ValidationResult isValid() throws DataException { // Check reference is correct - Account sender = getSender(); - if (!Arrays.equals(sender.getLastReference(), atTransactionData.getReference())) + Account atAccount = getATAccount(); + if (!Arrays.equals(atAccount.getLastReference(), atTransactionData.getReference())) return ValidationResult.INVALID_REFERENCE; if (this.atTransactionData.getMessage().length > MAX_DATA_SIZE) return ValidationResult.INVALID_DATA_LENGTH; + BigDecimal amount = this.atTransactionData.getAmount(); + // If we have no payment then we're done - if (this.atTransactionData.getAmount() == null) + if (amount == null) return ValidationResult.OK; - // Wrap and delegate final payment checks to Payment class - return new Payment(this.repository).isValid(atTransactionData.getSenderPublicKey(), getPaymentData(), atTransactionData.getFee()); + // Check amount is zero or positive + if (amount.compareTo(BigDecimal.ZERO) < 0) + return ValidationResult.NEGATIVE_AMOUNT; + + // Check recipient address is valid + if (!Crypto.isValidAddress(this.atTransactionData.getRecipient())) + return ValidationResult.INVALID_ADDRESS; + + long assetId = this.atTransactionData.getAssetId(); + AssetData assetData = this.repository.getAssetRepository().fromAssetId(assetId); + // Check asset even exists + if (assetData == null) + return ValidationResult.ASSET_DOES_NOT_EXIST; + + // Check asset amount is integer if asset is not divisible + if (!assetData.getIsDivisible() && amount.stripTrailingZeros().scale() > 0) + return ValidationResult.INVALID_AMOUNT; + + Account sender = getATAccount(); + // Check sender has enough of asset + if (sender.getConfirmedBalance(assetId).compareTo(amount) < 0) + return ValidationResult.NO_BALANCE; + + return ValidationResult.OK; } @Override @@ -108,10 +128,25 @@ public class ATTransaction extends Transaction { // Save this transaction itself this.repository.getTransactionRepository().save(this.transactionData); - if (this.atTransactionData.getAmount() != null) - // Wrap and delegate payment processing to Payment class. Only update recipient's last reference if transferring QORA. - new Payment(this.repository).process(atTransactionData.getSenderPublicKey(), getPaymentData(), atTransactionData.getFee(), - atTransactionData.getSignature(), false); + if (this.atTransactionData.getAmount() != null) { + Account sender = getATAccount(); + Account recipient = getRecipient(); + + long assetId = this.atTransactionData.getAssetId(); + BigDecimal amount = this.atTransactionData.getAmount(); + + // Update sender's balance due to amount + sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).subtract(amount)); + + // Update recipient's balance + recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).add(amount)); + + // For QORA amounts only: if recipient has no reference yet, then this is their starting reference + if (assetId == Asset.QORA && recipient.getLastReference() == null) + // In Qora1 last reference was set to 64-bytes of zero + // In Qora2 we use AT-Transction's signature, which makes more sense + recipient.setLastReference(this.atTransactionData.getSignature()); + } } @Override @@ -119,10 +154,26 @@ public class ATTransaction extends Transaction { // Delete this transaction this.repository.getTransactionRepository().delete(this.transactionData); - if (this.atTransactionData.getAmount() != null) - // Wrap and delegate payment processing to Payment class. Only revert recipient's last reference if transferring QORA. - new Payment(this.repository).orphan(atTransactionData.getSenderPublicKey(), getPaymentData(), atTransactionData.getFee(), - atTransactionData.getSignature(), atTransactionData.getReference(), false); + if (this.atTransactionData.getAmount() != null) { + Account sender = getATAccount(); + Account recipient = getRecipient(); + + long assetId = this.atTransactionData.getAssetId(); + BigDecimal amount = this.atTransactionData.getAmount(); + + // Update sender's balance due to amount + sender.setConfirmedBalance(assetId, sender.getConfirmedBalance(assetId).add(amount)); + + // Update recipient's balance + recipient.setConfirmedBalance(assetId, recipient.getConfirmedBalance(assetId).subtract(amount)); + + /* + * For QORA amounts only: If recipient's last reference is this transaction's signature, then they can't have made any transactions of their own + * (which would have changed their last reference) thus this is their first reference so remove it. + */ + if (assetId == Asset.QORA && Arrays.equals(recipient.getLastReference(), this.atTransactionData.getSignature())) + recipient.setLastReference(null); + } } } diff --git a/src/qora/transaction/ArbitraryTransaction.java b/src/qora/transaction/ArbitraryTransaction.java index 92d7f07b..ef82afaa 100644 --- a/src/qora/transaction/ArbitraryTransaction.java +++ b/src/qora/transaction/ArbitraryTransaction.java @@ -149,9 +149,9 @@ public class ArbitraryTransaction extends Transaction { // Make sure directory structure exists try { Files.createDirectories(dataPath.getParent()); - } catch (IOException e1) { + } catch (IOException e) { // TODO Auto-generated catch block - e1.printStackTrace(); + e.printStackTrace(); } // Output actual transaction data diff --git a/src/qora/transaction/DeployATTransaction.java b/src/qora/transaction/DeployATTransaction.java index 699b0654..e80412bf 100644 --- a/src/qora/transaction/DeployATTransaction.java +++ b/src/qora/transaction/DeployATTransaction.java @@ -205,7 +205,6 @@ public class DeployATTransaction extends Transaction { // Update AT's balance atAccount.setConfirmedBalance(Asset.QORA, deployATTransactionData.getAmount()); - } @Override diff --git a/src/qora/transaction/GenesisTransaction.java b/src/qora/transaction/GenesisTransaction.java index d3ae1bc1..3a1a83e9 100644 --- a/src/qora/transaction/GenesisTransaction.java +++ b/src/qora/transaction/GenesisTransaction.java @@ -133,12 +133,13 @@ public class GenesisTransaction extends Transaction { // Save this transaction itself this.repository.getTransactionRepository().save(this.transactionData); - // Update recipient's balance Account recipient = new Account(repository, genesisTransactionData.getRecipient()); - recipient.setConfirmedBalance(Asset.QORA, genesisTransactionData.getAmount()); - // Set recipient's starting reference + // Set recipient's starting reference (also creates account) recipient.setLastReference(genesisTransactionData.getSignature()); + + // Update recipient's balance + recipient.setConfirmedBalance(Asset.QORA, genesisTransactionData.getAmount()); } @Override @@ -146,12 +147,8 @@ public class GenesisTransaction extends Transaction { // Delete this transaction this.repository.getTransactionRepository().delete(this.transactionData); - // Delete recipient's balance - Account recipient = new Account(repository, genesisTransactionData.getRecipient()); - recipient.deleteBalance(Asset.QORA); - - // Delete recipient's last reference - recipient.setLastReference(null); + // Delete recipient's account (and balance) + this.repository.getAccountRepository().delete(genesisTransactionData.getRecipient()); } } diff --git a/src/qora/transaction/MessageTransaction.java b/src/qora/transaction/MessageTransaction.java index b01dcd3b..abc93d0a 100644 --- a/src/qora/transaction/MessageTransaction.java +++ b/src/qora/transaction/MessageTransaction.java @@ -85,7 +85,7 @@ public class MessageTransaction extends Transaction { // Processing private PaymentData getPaymentData() { - return new PaymentData(messageTransactionData.getRecipient(), Asset.QORA, messageTransactionData.getAmount()); + return new PaymentData(messageTransactionData.getRecipient(), messageTransactionData.getAssetId(), messageTransactionData.getAmount()); } @Override diff --git a/src/repository/ATRepository.java b/src/repository/ATRepository.java index fef0c76e..70e494ef 100644 --- a/src/repository/ATRepository.java +++ b/src/repository/ATRepository.java @@ -1,5 +1,7 @@ package repository; +import java.util.List; + import data.at.ATData; import data.at.ATStateData; @@ -17,8 +19,14 @@ public interface ATRepository { public ATStateData getATState(String atAddress, int height) throws DataException; + public List getBlockATStatesFromHeight(int height) throws DataException; + public void save(ATStateData atStateData) throws DataException; + /** Delete AT's state data at this height */ public void delete(String atAddress, int height) throws DataException; + /** Delete state data for all ATs at this height */ + public void deleteATStates(int height) throws DataException; + } diff --git a/src/repository/AccountRepository.java b/src/repository/AccountRepository.java index 5983ebfb..d4356a60 100644 --- a/src/repository/AccountRepository.java +++ b/src/repository/AccountRepository.java @@ -7,6 +7,8 @@ public interface AccountRepository { // General account + public void create(String address) throws DataException; + public AccountData getAccount(String address) throws DataException; public void save(AccountData accountData) throws DataException; diff --git a/src/repository/hsqldb/HSQLDBATRepository.java b/src/repository/hsqldb/HSQLDBATRepository.java index 4cb0cd23..a634fe98 100644 --- a/src/repository/hsqldb/HSQLDBATRepository.java +++ b/src/repository/hsqldb/HSQLDBATRepository.java @@ -3,6 +3,10 @@ package repository.hsqldb; import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; import data.at.ATData; import data.at.ATStateData; @@ -21,30 +25,32 @@ public class HSQLDBATRepository implements ATRepository { @Override public ATData fromATAddress(String atAddress) throws DataException { - try (ResultSet resultSet = this.repository - .checkedExecute("SELECT owner, asset_name, description, quantity, is_divisible, reference FROM Assets WHERE AT_address = ?", atAddress)) { + try (ResultSet resultSet = this.repository.checkedExecute( + "SELECT creator, creation, version, code_bytes, is_sleeping, sleep_until_height, is_finished, had_fatal_error, is_frozen, frozen_balance FROM ATs WHERE AT_address = ?", + atAddress)) { if (resultSet == null) return null; - int version = resultSet.getInt(1); - byte[] codeBytes = resultSet.getBytes(2); // Actually BLOB - boolean isSleeping = resultSet.getBoolean(3); + String creator = resultSet.getString(1); + long creation = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + int version = resultSet.getInt(3); + byte[] codeBytes = resultSet.getBytes(4); // Actually BLOB + boolean isSleeping = resultSet.getBoolean(5); - Integer sleepUntilHeight = resultSet.getInt(4); + Integer sleepUntilHeight = resultSet.getInt(6); if (resultSet.wasNull()) sleepUntilHeight = null; - boolean isFinished = resultSet.getBoolean(5); - boolean hadFatalError = resultSet.getBoolean(6); - boolean isFrozen = resultSet.getBoolean(7); + boolean isFinished = resultSet.getBoolean(7); + boolean hadFatalError = resultSet.getBoolean(8); + boolean isFrozen = resultSet.getBoolean(9); - BigDecimal frozenBalance = resultSet.getBigDecimal(8); + BigDecimal frozenBalance = resultSet.getBigDecimal(10); if (resultSet.wasNull()) frozenBalance = null; - byte[] deploySignature = resultSet.getBytes(9); - - return new ATData(atAddress, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance, deploySignature); + return new ATData(atAddress, creator, creation, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, + frozenBalance); } catch (SQLException e) { throw new DataException("Unable to fetch AT from repository", e); } @@ -54,10 +60,10 @@ public class HSQLDBATRepository implements ATRepository { public void save(ATData atData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("ATs"); - saveHelper.bind("AT_address", atData.getATAddress()).bind("version", atData.getVersion()).bind("code_bytes", atData.getCodeBytes()) - .bind("is_sleeping", atData.getIsSleeping()).bind("sleep_until_height", atData.getSleepUntilHeight()) - .bind("is_finished", atData.getIsFinished()).bind("had_fatal_error", atData.getHadFatalError()).bind("is_frozen", atData.getIsFrozen()) - .bind("frozen_balance", atData.getFrozenBalance()).bind("deploy_signature", atData.getDeploySignature()); + saveHelper.bind("AT_address", atData.getATAddress()).bind("creator", atData.getCreator()).bind("creation", new Timestamp(atData.getCreation())) + .bind("version", atData.getVersion()).bind("code_bytes", atData.getCodeBytes()).bind("is_sleeping", atData.getIsSleeping()) + .bind("sleep_until_height", atData.getSleepUntilHeight()).bind("is_finished", atData.getIsFinished()) + .bind("had_fatal_error", atData.getHadFatalError()).bind("is_frozen", atData.getIsFrozen()).bind("frozen_balance", atData.getFrozenBalance()); try { saveHelper.execute(this.repository); @@ -80,28 +86,63 @@ public class HSQLDBATRepository implements ATRepository { @Override public ATStateData getATState(String atAddress, int height) throws DataException { - try (ResultSet resultSet = this.repository.checkedExecute("SELECT state_data FROM ATStates WHERE AT_address = ? AND height = ?", atAddress, height)) { + try (ResultSet resultSet = this.repository + .checkedExecute("SELECT creation, state_data, state_hash, fees FROM ATStates WHERE AT_address = ? AND height = ?", atAddress, height)) { if (resultSet == null) return null; - byte[] stateData = resultSet.getBytes(1); // Actually BLOB + long creation = resultSet.getTimestamp(1, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + byte[] stateData = resultSet.getBytes(2); // Actually BLOB + byte[] stateHash = resultSet.getBytes(3); + BigDecimal fees = resultSet.getBigDecimal(4); - return new ATStateData(atAddress, height, stateData); + return new ATStateData(atAddress, height, creation, stateData, stateHash, fees); } catch (SQLException e) { - throw new DataException("Unable to fetch AT State from repository", e); + throw new DataException("Unable to fetch AT state from repository", e); } } + @Override + public List getBlockATStatesFromHeight(int height) throws DataException { + List atStates = new ArrayList(); + + try (ResultSet resultSet = this.repository.checkedExecute("SELECT AT_address, state_hash, fees FROM ATStates WHERE height = ? ORDER BY creation ASC", + height)) { + if (resultSet == null) + return atStates; // No atStates in this block + + // NB: do-while loop because .checkedExecute() implicitly calls ResultSet.next() for us + do { + String atAddress = resultSet.getString(1); + byte[] stateHash = resultSet.getBytes(2); + BigDecimal fees = resultSet.getBigDecimal(3); + + ATStateData atStateData = new ATStateData(atAddress, height, stateHash, fees); + atStates.add(atStateData); + } while (resultSet.next()); + } catch (SQLException e) { + throw new DataException("Unable to fetch AT states for this height from repository", e); + } + + return atStates; + } + @Override public void save(ATStateData atStateData) throws DataException { + // We shouldn't ever save partial ATStateData + if (atStateData.getCreation() == null || atStateData.getStateHash() == null || atStateData.getHeight() == null) + throw new IllegalArgumentException("Refusing to save partial AT state into repository!"); + HSQLDBSaver saveHelper = new HSQLDBSaver("ATStates"); - saveHelper.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight()).bind("state_data", atStateData.getStateData()); + saveHelper.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight()) + .bind("creation", new Timestamp(atStateData.getCreation())).bind("state_data", atStateData.getStateData()) + .bind("state_hash", atStateData.getStateHash()).bind("fees", atStateData.getFees()); try { saveHelper.execute(this.repository); } catch (SQLException e) { - throw new DataException("Unable to save AT State into repository", e); + throw new DataException("Unable to save AT state into repository", e); } } @@ -110,7 +151,16 @@ public class HSQLDBATRepository implements ATRepository { try { this.repository.delete("ATStates", "AT_address = ? AND height = ?", atAddress, height); } catch (SQLException e) { - throw new DataException("Unable to delete AT State from repository", e); + throw new DataException("Unable to delete AT state from repository", e); + } + } + + @Override + public void deleteATStates(int height) throws DataException { + try { + this.repository.delete("ATStates", "height = ?", height); + } catch (SQLException e) { + throw new DataException("Unable to delete AT states from repository", e); } } diff --git a/src/repository/hsqldb/HSQLDBAccountRepository.java b/src/repository/hsqldb/HSQLDBAccountRepository.java index d56604b3..5c716863 100644 --- a/src/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/repository/hsqldb/HSQLDBAccountRepository.java @@ -19,6 +19,19 @@ public class HSQLDBAccountRepository implements AccountRepository { // General account + @Override + public void create(String address) throws DataException { + HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts"); + + saveHelper.bind("account", address); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to create account in repository", e); + } + } + @Override public AccountData getAccount(String address) throws DataException { try (ResultSet resultSet = this.repository.checkedExecute("SELECT reference FROM Accounts WHERE account = ?", address)) { @@ -34,6 +47,7 @@ public class HSQLDBAccountRepository implements AccountRepository { @Override public void save(AccountData accountData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("Accounts"); + saveHelper.bind("account", accountData.getAddress()).bind("reference", accountData.getReference()); try { @@ -73,6 +87,7 @@ public class HSQLDBAccountRepository implements AccountRepository { @Override public void save(AccountBalanceData accountBalanceData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("AccountBalances"); + saveHelper.bind("account", accountBalanceData.getAddress()).bind("asset_id", accountBalanceData.getAssetId()).bind("balance", accountBalanceData.getBalance()); diff --git a/src/repository/hsqldb/HSQLDBBlockRepository.java b/src/repository/hsqldb/HSQLDBBlockRepository.java index 6b17aa30..96119aae 100644 --- a/src/repository/hsqldb/HSQLDBBlockRepository.java +++ b/src/repository/hsqldb/HSQLDBBlockRepository.java @@ -18,7 +18,7 @@ import repository.TransactionRepository; public class HSQLDBBlockRepository implements BlockRepository { private static final String BLOCK_DB_COLUMNS = "version, reference, transaction_count, total_fees, " - + "transactions_signature, height, generation, generating_balance, generator, generator_signature, AT_data, AT_fees"; + + "transactions_signature, height, generation, generating_balance, generator, generator_signature, AT_count, AT_fees"; protected HSQLDBRepository repository; @@ -41,11 +41,11 @@ public class HSQLDBBlockRepository implements BlockRepository { BigDecimal generatingBalance = resultSet.getBigDecimal(8); byte[] generatorPublicKey = resultSet.getBytes(9); byte[] generatorSignature = resultSet.getBytes(10); - byte[] atBytes = resultSet.getBytes(11); + int atCount = resultSet.getInt(11); BigDecimal atFees = resultSet.getBigDecimal(12); return new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance, - generatorPublicKey, generatorSignature, atBytes, atFees); + generatorPublicKey, generatorSignature, atCount, atFees); } catch (SQLException e) { throw new DataException("Error extracting data from result set", e); } @@ -62,7 +62,7 @@ public class HSQLDBBlockRepository implements BlockRepository { @Override public BlockData fromReference(byte[] reference) throws DataException { - try (ResultSet resultSet = this.repository.checkedExecute("SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE height = ?", reference)) { + try (ResultSet resultSet = this.repository.checkedExecute("SELECT " + BLOCK_DB_COLUMNS + " FROM Blocks WHERE reference = ?", reference)) { return getBlockFromResultSet(resultSet); } catch (SQLException e) { throw new DataException("Error loading data from DB", e); @@ -123,7 +123,7 @@ public class HSQLDBBlockRepository implements BlockRepository { transactions.add(transactionRepo.fromSignature(transactionSignature)); } while (resultSet.next()); } catch (SQLException e) { - throw new DataException(e); + throw new DataException("Unable to fetch block's transactions from repository", e); } return transactions; @@ -138,7 +138,7 @@ public class HSQLDBBlockRepository implements BlockRepository { .bind("transactions_signature", blockData.getTransactionsSignature()).bind("height", blockData.getHeight()) .bind("generation", new Timestamp(blockData.getTimestamp())).bind("generating_balance", blockData.getGeneratingBalance()) .bind("generator", blockData.getGeneratorPublicKey()).bind("generator_signature", blockData.getGeneratorSignature()) - .bind("AT_data", blockData.getAtBytes()).bind("AT_fees", blockData.getAtFees()); + .bind("AT_count", blockData.getATCount()).bind("AT_fees", blockData.getATFees()); try { saveHelper.execute(this.repository); @@ -159,6 +159,7 @@ public class HSQLDBBlockRepository implements BlockRepository { @Override public void save(BlockTransactionData blockTransactionData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("BlockTransactions"); + saveHelper.bind("block_signature", blockTransactionData.getBlockSignature()).bind("sequence", blockTransactionData.getSequence()) .bind("transaction_signature", blockTransactionData.getTransactionSignature()); diff --git a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java index e8efb45a..8d4742cf 100644 --- a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -85,7 +85,7 @@ public class HSQLDBDatabaseUpdates { stmt.execute("CREATE TYPE Signature AS VARBINARY(64)"); stmt.execute("CREATE TYPE QoraAddress AS VARCHAR(36)"); stmt.execute("CREATE TYPE QoraPublicKey AS VARBINARY(32)"); - stmt.execute("CREATE TYPE QoraAmount AS DECIMAL(19, 8)"); + stmt.execute("CREATE TYPE QoraAmount AS DECIMAL(27, 8)"); stmt.execute("CREATE TYPE RegisteredName AS VARCHAR(400) COLLATE SQL_TEXT_NO_PAD"); stmt.execute("CREATE TYPE NameData AS VARCHAR(4000)"); stmt.execute("CREATE TYPE PollName AS VARCHAR(400) COLLATE SQL_TEXT_NO_PAD"); @@ -108,7 +108,7 @@ public class HSQLDBDatabaseUpdates { stmt.execute("CREATE TABLE Blocks (signature BlockSignature PRIMARY KEY, version TINYINT NOT NULL, reference BlockSignature, " + "transaction_count INTEGER NOT NULL, total_fees QoraAmount NOT NULL, transactions_signature Signature NOT NULL, " + "height INTEGER NOT NULL, generation TIMESTAMP WITH TIME ZONE NOT NULL, generating_balance QoraAmount NOT NULL, " - + "generator QoraPublicKey NOT NULL, generator_signature Signature NOT NULL, AT_data VARBINARY(20000), AT_fees QoraAmount)"); + + "generator QoraPublicKey NOT NULL, generator_signature Signature NOT NULL, AT_count INTEGER NOT NULL, AT_fees QoraAmount NOT NULL)"); // For finding blocks by height. stmt.execute("CREATE INDEX BlockHeightIndex ON Blocks (height)"); // For finding blocks by the account that generated them. @@ -353,18 +353,21 @@ public class HSQLDBDatabaseUpdates { case 27: // CIYAM Automated Transactions - stmt.execute("CREATE TABLE ATs (AT_address QoraAddress, version INTEGER NOT NULL, code_bytes ATCode NOT NULL, " - + "is_sleeping BOOLEAN NOT NULL, sleep_until_height INTEGER, is_finished BOOLEAN NOT NULL, had_fatal_error BOOLEAN NOT NULL, " - + "is_frozen BOOLEAN NOT NULL, frozen_balance QoraAmount, deploy_signature Signature NOT NULL, PRIMARY key (AT_address))"); - // For finding executable ATs - stmt.execute("CREATE INDEX ATIndex on ATs (is_finished, AT_address)"); + stmt.execute("CREATE TABLE ATs (AT_address QoraAddress, creator QoraAddress, creation TIMESTAMP WITH TIME ZONE, version INTEGER NOT NULL, " + + "code_bytes ATCode NOT NULL, is_sleeping BOOLEAN NOT NULL, sleep_until_height INTEGER, " + + "is_finished BOOLEAN NOT NULL, had_fatal_error BOOLEAN NOT NULL, is_frozen BOOLEAN NOT NULL, frozen_balance QoraAmount, " + + "PRIMARY key (AT_address))"); + // For finding executable ATs, ordered by creation timestamp + stmt.execute("CREATE INDEX ATIndex on ATs (is_finished, creation, AT_address)"); // AT state on a per-block basis - stmt.execute( - "CREATE TABLE ATStates (AT_address QoraAddress, height INTEGER NOT NULL, state_data ATState, state_hash ATStateHash NOT NULL, fees QoraAmount NOT NULL, " - + "PRIMARY KEY (AT_address, height), FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)"); + stmt.execute("CREATE TABLE ATStates (AT_address QoraAddress, height INTEGER NOT NULL, creation TIMESTAMP WITH TIME ZONE, " + + "state_data ATState, state_hash ATStateHash NOT NULL, fees QoraAmount NOT NULL, " + + "PRIMARY KEY (AT_address, height), FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)"); + // For finding per-block AT states, ordered by creation timestamp + stmt.execute("CREATE INDEX BlockATStateIndex on ATStates (height, creation, AT_address)"); // Generated AT Transactions stmt.execute( - "CREATE TABLE ATTransactions (signature Signature, sender QoraPublicKey NOT NULL, recipient QoraAddress, amount QoraAmount, asset_id AssetID, message ATMessage, " + "CREATE TABLE ATTransactions (signature Signature, AT_address QoraAddress NOT NULL, recipient QoraAddress, amount QoraAmount, asset_id AssetID, message ATMessage, " + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); break; diff --git a/src/repository/hsqldb/transaction/HSQLDBATTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBATTransactionRepository.java index 279c540a..f1103985 100644 --- a/src/repository/hsqldb/transaction/HSQLDBATTransactionRepository.java +++ b/src/repository/hsqldb/transaction/HSQLDBATTransactionRepository.java @@ -17,12 +17,12 @@ public class HSQLDBATTransactionRepository extends HSQLDBTransactionRepository { } TransactionData fromBase(byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException { - try (ResultSet resultSet = this.repository.checkedExecute("SELECT sender, recipient, amount, asset_id, message FROM ATTransactions WHERE signature = ?", - signature)) { + try (ResultSet resultSet = this.repository + .checkedExecute("SELECT AT_address, recipient, amount, asset_id, message FROM ATTransactions WHERE signature = ?", signature)) { if (resultSet == null) return null; - byte[] senderPublicKey = resultSet.getBytes(1); + String atAddress = resultSet.getString(1); String recipient = resultSet.getString(2); BigDecimal amount = resultSet.getBigDecimal(3); @@ -37,7 +37,7 @@ public class HSQLDBATTransactionRepository extends HSQLDBTransactionRepository { if (resultSet.wasNull()) message = null; - return new ATTransactionData(senderPublicKey, recipient, amount, assetId, message, fee, timestamp, reference, signature); + return new ATTransactionData(atAddress, recipient, amount, assetId, message, fee, timestamp, reference, signature); } catch (SQLException e) { throw new DataException("Unable to fetch AT transaction from repository", e); } @@ -49,7 +49,7 @@ public class HSQLDBATTransactionRepository extends HSQLDBTransactionRepository { HSQLDBSaver saveHelper = new HSQLDBSaver("ATTransactions"); - saveHelper.bind("signature", atTransactionData.getSignature()).bind("sender", atTransactionData.getSenderPublicKey()) + saveHelper.bind("signature", atTransactionData.getSignature()).bind("AT_address", atTransactionData.getATAddress()) .bind("recipient", atTransactionData.getRecipient()).bind("amount", atTransactionData.getAmount()) .bind("asset_id", atTransactionData.getAssetId()).bind("message", atTransactionData.getMessage()); diff --git a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index 9a2ade4d..8360274a 100644 --- a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -164,13 +164,22 @@ public class HSQLDBTransactionRepository implements TransactionRepository { } } + /** + * Returns payments associated with a transaction's signature. + *

+ * Used by various transaction types, like Payment, MultiPayment, ArbitraryTransaction. + * + * @param signature + * @return list of payments, empty if none found + * @throws DataException + */ protected List getPaymentsFromSignature(byte[] signature) throws DataException { + List payments = new ArrayList(); + try (ResultSet resultSet = this.repository.checkedExecute("SELECT recipient, amount, asset_id FROM SharedTransactionPayments WHERE signature = ?", signature)) { if (resultSet == null) - return null; - - List payments = new ArrayList(); + return payments; // NOTE: do-while because checkedExecute() above has already called rs.next() for us do { diff --git a/src/test/ATTests.java b/src/test/ATTests.java index 076bb927..9d5a3a8a 100644 --- a/src/test/ATTests.java +++ b/src/test/ATTests.java @@ -9,6 +9,7 @@ import org.junit.Test; import com.google.common.hash.HashCode; +import data.at.ATStateData; import data.block.BlockData; import data.block.BlockTransactionData; import data.transaction.DeployATTransactionData; @@ -70,13 +71,22 @@ public class ATTests extends Common { long blockTimestamp = 1439997158336L; BigDecimal generatingBalance = BigDecimal.valueOf(1440368826L).setScale(8); byte[] generatorPublicKey = Base58.decode("X4s833bbtghh7gejmaBMbWqD44HrUobw93ANUuaNhFc"); - byte[] atBytes = HashCode.fromString("17950a6c62d17ff0caa545651c054a105f1c464daca443df846cc6a3d58f764b78c09cff50f0fd9ec2").asBytes(); + int atCount = 1; BigDecimal atFees = BigDecimal.valueOf(50.0).setScale(8); BlockData blockData = new BlockData(version, blockReference, transactionCount, totalFees, transactionsSignature, height, blockTimestamp, - generatingBalance, generatorPublicKey, generatorSignature, atBytes, atFees); + generatingBalance, generatorPublicKey, generatorSignature, atCount, atFees); repository.getBlockRepository().save(blockData); + + byte[] atBytes = HashCode.fromString("17950a6c62d17ff0caa545651c054a105f1c464daca443df846cc6a3d58f764b78c09cff50f0fd9ec2").asBytes(); + + String atAddress = Base58.encode(Arrays.copyOfRange(atBytes, 0, 25)); + byte[] stateHash = Arrays.copyOfRange(atBytes, 25, atBytes.length); + + ATStateData atStateData = new ATStateData(atAddress, height, timestamp, new byte[0], stateHash, atFees); + + repository.getATRepository().save(atStateData); } int sequence = 0; diff --git a/src/test/NavigationTests.java b/src/test/NavigationTests.java index 42cfdcb6..4ebdbc24 100644 --- a/src/test/NavigationTests.java +++ b/src/test/NavigationTests.java @@ -37,7 +37,7 @@ public class NavigationTests extends Common { System.out.println("Block " + blockData.getHeight() + ", signature: " + Base58.encode(blockData.getSignature())); - assertEquals(49778, blockData.getHeight()); + assertEquals((Integer) 49778, blockData.getHeight()); } } diff --git a/src/test/SignatureTests.java b/src/test/SignatureTests.java index bffc7144..2aa3f963 100644 --- a/src/test/SignatureTests.java +++ b/src/test/SignatureTests.java @@ -48,11 +48,7 @@ public class SignatureTests extends Common { PrivateKeyAccount generator = new PrivateKeyAccount(repository, new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 }); - byte[] atBytes = null; - - BigDecimal atFees = null; - - Block block = new Block(repository, version, reference, timestamp, generatingBalance, generator, atBytes, atFees); + Block block = new Block(repository, version, reference, timestamp, generatingBalance, generator); block.sign(); assertTrue(block.isSignatureValid()); diff --git a/src/test/TransactionTests.java b/src/test/TransactionTests.java index f976c59f..df8a17da 100644 --- a/src/test/TransactionTests.java +++ b/src/test/TransactionTests.java @@ -172,7 +172,7 @@ public class TransactionTests { assertEquals(ValidationResult.OK, paymentTransaction.isValid()); // Forge new block with transaction - Block block = new Block(repository, parentBlockData, generator, null, null); + Block block = new Block(repository, parentBlockData, generator); block.addTransaction(paymentTransactionData); block.sign(); @@ -233,7 +233,7 @@ public class TransactionTests { assertEquals(ValidationResult.OK, registerNameTransaction.isValid()); // Forge new block with transaction - Block block = new Block(repository, parentBlockData, generator, null, null); + Block block = new Block(repository, parentBlockData, generator); block.addTransaction(registerNameTransactionData); block.sign(); @@ -289,7 +289,7 @@ public class TransactionTests { assertEquals(ValidationResult.OK, updateNameTransaction.isValid()); // Forge new block with transaction - Block block = new Block(repository, parentBlockData, generator, null, null); + Block block = new Block(repository, parentBlockData, generator); block.addTransaction(updateNameTransactionData); block.sign(); @@ -334,7 +334,7 @@ public class TransactionTests { assertEquals(ValidationResult.OK, sellNameTransaction.isValid()); // Forge new block with transaction - Block block = new Block(repository, parentBlockData, generator, null, null); + Block block = new Block(repository, parentBlockData, generator); block.addTransaction(sellNameTransactionData); block.sign(); @@ -385,7 +385,7 @@ public class TransactionTests { assertEquals(ValidationResult.OK, cancelSellNameTransaction.isValid()); // Forge new block with transaction - Block block = new Block(repository, parentBlockData, generator, null, null); + Block block = new Block(repository, parentBlockData, generator); block.addTransaction(cancelSellNameTransactionData); block.sign(); @@ -432,7 +432,7 @@ public class TransactionTests { byte[] buyersReference = somePaymentTransaction.getTransactionData().getSignature(); // Forge new block with transaction - Block block = new Block(repository, parentBlockData, generator, null, null); + Block block = new Block(repository, parentBlockData, generator); block.addTransaction(somePaymentTransaction.getTransactionData()); block.sign(); @@ -451,7 +451,7 @@ public class TransactionTests { assertEquals(ValidationResult.OK, buyNameTransaction.isValid()); // Forge new block with transaction - block = new Block(repository, parentBlockData, generator, null, null); + block = new Block(repository, parentBlockData, generator); block.addTransaction(buyNameTransactionData); block.sign(); @@ -504,7 +504,7 @@ public class TransactionTests { assertEquals(ValidationResult.OK, createPollTransaction.isValid()); // Forge new block with transaction - Block block = new Block(repository, parentBlockData, generator, null, null); + Block block = new Block(repository, parentBlockData, generator); block.addTransaction(createPollTransactionData); block.sign(); @@ -563,7 +563,7 @@ public class TransactionTests { assertEquals(ValidationResult.OK, voteOnPollTransaction.isValid()); // Forge new block with transaction - Block block = new Block(repository, parentBlockData, generator, null, null); + Block block = new Block(repository, parentBlockData, generator); block.addTransaction(voteOnPollTransactionData); block.sign(); @@ -630,7 +630,7 @@ public class TransactionTests { assertEquals(ValidationResult.OK, issueAssetTransaction.isValid()); // Forge new block with transaction - Block block = new Block(repository, parentBlockData, generator, null, null); + Block block = new Block(repository, parentBlockData, generator); block.addTransaction(issueAssetTransactionData); block.sign(); @@ -720,7 +720,7 @@ public class TransactionTests { assertEquals(ValidationResult.OK, transferAssetTransaction.isValid()); // Forge new block with transaction - Block block = new Block(repository, parentBlockData, generator, null, null); + Block block = new Block(repository, parentBlockData, generator); block.addTransaction(transferAssetTransactionData); block.sign(); @@ -800,7 +800,7 @@ public class TransactionTests { byte[] buyersReference = somePaymentTransaction.getTransactionData().getSignature(); // Forge new block with transaction - Block block = new Block(repository, parentBlockData, generator, null, null); + Block block = new Block(repository, parentBlockData, generator); block.addTransaction(somePaymentTransaction.getTransactionData()); block.sign(); @@ -824,7 +824,7 @@ public class TransactionTests { assertEquals(ValidationResult.OK, createOrderTransaction.isValid()); // Forge new block with transaction - block = new Block(repository, parentBlockData, generator, null, null); + block = new Block(repository, parentBlockData, generator); block.addTransaction(createOrderTransactionData); block.sign(); @@ -905,7 +905,7 @@ public class TransactionTests { assertEquals(ValidationResult.OK, cancelOrderTransaction.isValid()); // Forge new block with transaction - Block block = new Block(repository, parentBlockData, generator, null, null); + Block block = new Block(repository, parentBlockData, generator); block.addTransaction(cancelOrderTransactionData); block.sign(); @@ -980,7 +980,7 @@ public class TransactionTests { assertEquals(ValidationResult.OK, createOrderTransaction.isValid()); // Forge new block with transaction - Block block = new Block(repository, parentBlockData, generator, null, null); + Block block = new Block(repository, parentBlockData, generator); block.addTransaction(createOrderTransactionData); block.sign(); @@ -1089,7 +1089,7 @@ public class TransactionTests { assertEquals(ValidationResult.OK, multiPaymentTransaction.isValid()); // Forge new block with payment transaction - Block block = new Block(repository, parentBlockData, generator, null, null); + Block block = new Block(repository, parentBlockData, generator); block.addTransaction(multiPaymentTransactionData); block.sign(); @@ -1159,7 +1159,7 @@ public class TransactionTests { assertEquals(ValidationResult.OK, messageTransaction.isValid()); // Forge new block with message transaction - Block block = new Block(repository, parentBlockData, generator, null, null); + Block block = new Block(repository, parentBlockData, generator); block.addTransaction(messageTransactionData); block.sign(); diff --git a/src/transform/Transformer.java b/src/transform/Transformer.java index cd6b4328..ad9959f9 100644 --- a/src/transform/Transformer.java +++ b/src/transform/Transformer.java @@ -14,4 +14,7 @@ public abstract class Transformer { public static final int SIGNATURE_LENGTH = 64; public static final int TIMESTAMP_LENGTH = LONG_LENGTH; + public static final int MD5_LENGTH = 16; + public static final int SHA256_LENGTH = 32; + } diff --git a/src/transform/block/BlockTransformer.java b/src/transform/block/BlockTransformer.java index 7d7336ae..5bc5796e 100644 --- a/src/transform/block/BlockTransformer.java +++ b/src/transform/block/BlockTransformer.java @@ -17,6 +17,7 @@ import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; import data.assets.TradeData; +import data.at.ATStateData; import data.block.BlockData; import data.transaction.TransactionData; import qora.account.PublicKeyAccount; @@ -24,12 +25,13 @@ import qora.assets.Order; import qora.block.Block; import qora.transaction.CreateOrderTransaction; import qora.transaction.Transaction; +import qora.transaction.Transaction.TransactionType; import repository.DataException; import transform.TransformationException; import transform.Transformer; import transform.transaction.TransactionTransformer; import utils.Base58; -import utils.Pair; +import utils.Triple; import utils.Serialization; public class BlockTransformer extends Transformer { @@ -52,6 +54,9 @@ public class BlockTransformer extends Transformer { protected static final int AT_FEES_LENGTH = LONG_LENGTH; protected static final int AT_LENGTH = AT_FEES_LENGTH + AT_BYTES_LENGTH; + protected static final int V2_AT_ENTRY_LENGTH = ADDRESS_LENGTH + MD5_LENGTH; + protected static final int V4_AT_ENTRY_LENGTH = ADDRESS_LENGTH + SHA256_LENGTH + BIG_DECIMAL_LENGTH; + /** * Extract block data and transaction data from serialized bytes. * @@ -59,7 +64,7 @@ public class BlockTransformer extends Transformer { * @return BlockData and a List of transactions. * @throws TransformationException */ - public static Pair> fromBytes(byte[] bytes) throws TransformationException { + public static Triple, List> fromBytes(byte[] bytes) throws TransformationException { if (bytes == null) return null; @@ -88,25 +93,74 @@ public class BlockTransformer extends Transformer { byte[] generatorSignature = new byte[GENERATOR_SIGNATURE_LENGTH]; byteBuffer.get(generatorSignature); - byte[] atBytes = null; - BigDecimal atFees = null; + BigDecimal totalFees = BigDecimal.ZERO.setScale(8); + + int atCount = 0; + BigDecimal atFees = BigDecimal.ZERO.setScale(8); + List atStates = new ArrayList(); + if (version >= 2) { int atBytesLength = byteBuffer.getInt(); if (atBytesLength > Block.MAX_BLOCK_BYTES) throw new TransformationException("Byte data too long for Block's AT info"); - atBytes = new byte[atBytesLength]; - byteBuffer.get(atBytes); + ByteBuffer atByteBuffer = byteBuffer.slice(); + atByteBuffer.limit(atBytesLength); - atFees = BigDecimal.valueOf(byteBuffer.getLong()).setScale(8); + if (version < 4) { + // For versions < 4, read AT-address & MD5 pairs + if (atBytesLength % V2_AT_ENTRY_LENGTH != 0) + throw new TransformationException("AT byte data not a multiple of version 2+ entries"); + + while (atByteBuffer.hasRemaining()) { + byte[] atAddressBytes = new byte[ADDRESS_LENGTH]; + atByteBuffer.get(atAddressBytes); + String atAddress = Base58.encode(atAddressBytes); + + byte[] stateHash = new byte[MD5_LENGTH]; + atByteBuffer.get(stateHash); + + atStates.add(new ATStateData(atAddress, stateHash)); + } + + // Bump byteBuffer over AT states just read in slice + byteBuffer.position(byteBuffer.position() + atBytesLength); + + // AT fees follow in versions < 4 + atFees = Serialization.deserializeBigDecimal(byteBuffer); + } else { + // For block versions >= 4, read AT-address, SHA256 hash and fees + if (atBytesLength % V4_AT_ENTRY_LENGTH != 0) + throw new TransformationException("AT byte data not a multiple of version 4+ entries"); + + while (atByteBuffer.hasRemaining()) { + byte[] atAddressBytes = new byte[ADDRESS_LENGTH]; + atByteBuffer.get(atAddressBytes); + String atAddress = Base58.encode(atAddressBytes); + + byte[] stateHash = new byte[SHA256_LENGTH]; + atByteBuffer.get(stateHash); + + BigDecimal fees = Serialization.deserializeBigDecimal(atByteBuffer); + // Add this AT's fees to our total + atFees = atFees.add(fees); + + atStates.add(new ATStateData(atAddress, stateHash, fees)); + } + } + + // AT count to reflect the number of states we have + atCount = atStates.size(); + + // Add AT fees to totalFees + totalFees = totalFees.add(atFees); } int transactionCount = byteBuffer.getInt(); // Parse transactions now, compared to deferred parsing in Gen1, so we can throw ParseException if need be. List transactions = new ArrayList(); - BigDecimal totalFees = BigDecimal.ZERO.setScale(8); for (int t = 0; t < transactionCount; ++t) { if (byteBuffer.remaining() < TRANSACTION_SIZE_LENGTH) @@ -126,26 +180,28 @@ public class BlockTransformer extends Transformer { TransactionData transactionData = TransactionTransformer.fromBytes(transactionBytes); transactions.add(transactionData); - totalFees.add(transactionData.getFee()); + totalFees = totalFees.add(transactionData.getFee()); } if (byteBuffer.hasRemaining()) throw new TransformationException("Excess byte data found after parsing Block"); - // XXX we don't know height! - int height = 0; + // We don't have a height! + Integer height = null; BlockData blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance, - generatorPublicKey, generatorSignature, atBytes, atFees); + generatorPublicKey, generatorSignature, atCount, atFees); - return new Pair>(blockData, transactions); + return new Triple, List>(blockData, transactions, atStates); } public static int getDataLength(Block block) throws TransformationException { BlockData blockData = block.getBlockData(); int blockLength = BASE_LENGTH; - if (blockData.getVersion() >= 2 && blockData.getAtBytes() != null) - blockLength += AT_FEES_LENGTH + AT_BYTES_LENGTH + blockData.getAtBytes().length; + if (blockData.getVersion() >= 4) + blockLength += AT_BYTES_LENGTH + blockData.getATCount() * V4_AT_ENTRY_LENGTH; + else if (blockData.getVersion() >= 2) + blockLength += AT_FEES_LENGTH + AT_BYTES_LENGTH + blockData.getATCount() * V2_AT_ENTRY_LENGTH; try { // Short cut for no transactions @@ -177,18 +233,29 @@ public class BlockTransformer extends Transformer { bytes.write(blockData.getTransactionsSignature()); bytes.write(blockData.getGeneratorSignature()); - if (blockData.getVersion() >= 2) { - byte[] atBytes = blockData.getAtBytes(); + if (blockData.getVersion() >= 4) { + int atBytesLength = blockData.getATCount() * V4_AT_ENTRY_LENGTH; + bytes.write(Ints.toByteArray(atBytesLength)); - if (atBytes != null) { - bytes.write(Ints.toByteArray(atBytes.length)); - bytes.write(atBytes); - // NOTE: atFees serialized as long value, not as BigDecimal, for historic compatibility - bytes.write(Longs.toByteArray(blockData.getAtFees().longValue())); - } else { - bytes.write(Ints.toByteArray(0)); - bytes.write(Longs.toByteArray(0L)); + for (ATStateData atStateData : block.getATStates()) { + bytes.write(Base58.decode(atStateData.getATAddress())); + bytes.write(atStateData.getStateHash()); + Serialization.serializeBigDecimal(bytes, atStateData.getFees()); } + } else if (blockData.getVersion() >= 2) { + int atBytesLength = blockData.getATCount() * V2_AT_ENTRY_LENGTH; + bytes.write(Ints.toByteArray(atBytesLength)); + + for (ATStateData atStateData : block.getATStates()) { + bytes.write(Base58.decode(atStateData.getATAddress())); + bytes.write(atStateData.getStateHash()); + } + + if (blockData.getATFees() != null) + // NOTE: atFees serialized as long value, not as BigDecimal, for historic compatibility + bytes.write(Longs.toByteArray(blockData.getATFees().longValue())); + else + bytes.write(Longs.toByteArray(0)); } // Transactions @@ -263,30 +330,39 @@ public class BlockTransformer extends Transformer { json.put("assetTrades", tradesHappened); // Add CIYAM AT info (if any) - if (blockData.getAtBytes() != null) { - json.put("blockATs", HashCode.fromBytes(blockData.getAtBytes()).toString()); - json.put("atFees", blockData.getAtFees()); + if (blockData.getATCount() > 0) { + JSONArray atsJson = new JSONArray(); + + try { + for (ATStateData atStateData : block.getATStates()) { + JSONObject atJson = new JSONObject(); + + atJson.put("AT", atStateData.getATAddress()); + atJson.put("stateHash", HashCode.fromBytes(atStateData.getStateHash()).toString()); + + if (blockData.getVersion() >= 4) + atJson.put("fees", atStateData.getFees().toPlainString()); + + atsJson.add(atJson); + } + } catch (DataException e) { + throw new TransformationException("Unable to transform block into JSON", e); + } + + json.put("ATs", atsJson); + + if (blockData.getVersion() >= 2) + json.put("atFees", blockData.getATFees()); } return json; } public static byte[] getBytesForGeneratorSignature(BlockData blockData) throws TransformationException { - try { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH); + byte[] generatorSignature = Arrays.copyOf(blockData.getReference(), GENERATOR_SIGNATURE_LENGTH); + PublicKeyAccount generator = new PublicKeyAccount(null, blockData.getGeneratorPublicKey()); - // Only copy the generator signature from reference, which is the first 64 bytes. - bytes.write(Arrays.copyOf(blockData.getReference(), GENERATOR_SIGNATURE_LENGTH)); - - bytes.write(Longs.toByteArray(blockData.getGeneratingBalance().longValue())); - - // We're padding here just in case the generator is the genesis account whose public key is only 8 bytes long. - bytes.write(Bytes.ensureCapacity(blockData.getGeneratorPublicKey(), GENERATOR_LENGTH, 0)); - - return bytes.toByteArray(); - } catch (IOException e) { - throw new TransformationException(e); - } + return getBytesForGeneratorSignature(generatorSignature, blockData.getGeneratingBalance(), generator); } public static byte[] getBytesForGeneratorSignature(byte[] generatorSignature, BigDecimal generatingBalance, PublicKeyAccount generator) @@ -308,13 +384,18 @@ public class BlockTransformer extends Transformer { } public static byte[] getBytesForTransactionsSignature(Block block) throws TransformationException { - ByteArrayOutputStream bytes = new ByteArrayOutputStream( - GENERATOR_SIGNATURE_LENGTH + block.getBlockData().getTransactionCount() * TransactionTransformer.SIGNATURE_LENGTH); - try { + List transactions = block.getTransactions(); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + transactions.size() * TransactionTransformer.SIGNATURE_LENGTH); + bytes.write(block.getBlockData().getGeneratorSignature()); - for (Transaction transaction : block.getTransactions()) { + for (Transaction transaction : transactions) { + // For legacy blocks, we don't include AT-Transactions + if (block.getBlockData().getVersion() < 4 && transaction.getTransactionData().getType() == TransactionType.AT) + continue; + if (!transaction.isSignatureValid()) throw new TransformationException("Transaction signature invalid when building block's transactions signature"); diff --git a/src/transform/transaction/ATTransactionTransformer.java b/src/transform/transaction/ATTransactionTransformer.java new file mode 100644 index 00000000..ff83272b --- /dev/null +++ b/src/transform/transaction/ATTransactionTransformer.java @@ -0,0 +1,92 @@ +package transform.transaction; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.json.simple.JSONObject; + +import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; + +import data.transaction.TransactionData; +import data.transaction.ATTransactionData; +import transform.TransformationException; +import utils.Serialization; + +public class ATTransactionTransformer extends TransactionTransformer { + + // Property lengths + private static final int SENDER_LENGTH = ADDRESS_LENGTH; + private static final int RECIPIENT_LENGTH = ADDRESS_LENGTH; + private static final int AMOUNT_LENGTH = BIG_DECIMAL_LENGTH; + private static final int ASSET_ID_LENGTH = LONG_LENGTH; + private static final int DATA_SIZE_LENGTH = INT_LENGTH; + + private static final int TYPELESS_DATALESS_LENGTH = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH + ASSET_ID_LENGTH + DATA_SIZE_LENGTH; + + static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { + throw new TransformationException("Serialized AT Transactions should not exist!"); + } + + public static int getDataLength(TransactionData transactionData) throws TransformationException { + ATTransactionData atTransactionData = (ATTransactionData) transactionData; + + return TYPE_LENGTH + TYPELESS_DATALESS_LENGTH + atTransactionData.getMessage().length; + } + + public static byte[] toBytes(TransactionData transactionData) throws TransformationException { + try { + ATTransactionData atTransactionData = (ATTransactionData) transactionData; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(atTransactionData.getType().value)); + bytes.write(Longs.toByteArray(atTransactionData.getTimestamp())); + bytes.write(atTransactionData.getReference()); + + Serialization.serializeAddress(bytes, atTransactionData.getATAddress()); + + Serialization.serializeAddress(bytes, atTransactionData.getRecipient()); + + if (atTransactionData.getAssetId() != null) { + Serialization.serializeBigDecimal(bytes, atTransactionData.getAmount()); + bytes.write(Longs.toByteArray(atTransactionData.getAssetId())); + } + + byte[] message = atTransactionData.getMessage(); + if (message.length > 0) { + bytes.write(Ints.toByteArray(message.length)); + bytes.write(message); + } else { + bytes.write(Ints.toByteArray(0)); + } + + Serialization.serializeBigDecimal(bytes, atTransactionData.getFee()); + + if (atTransactionData.getSignature() != null) + bytes.write(atTransactionData.getSignature()); + + return bytes.toByteArray(); + } catch (IOException | ClassCastException e) { + throw new TransformationException(e); + } + } + + @SuppressWarnings("unchecked") + public static JSONObject toJSON(TransactionData transactionData) throws TransformationException { + JSONObject json = TransactionTransformer.getBaseJSON(transactionData); + + try { + ATTransactionData atTransactionData = (ATTransactionData) transactionData; + + json.put("sender", atTransactionData.getATAddress()); + + } catch (ClassCastException e) { + throw new TransformationException(e); + } + + return json; + } + +} diff --git a/src/utils/Pair.java b/src/utils/Pair.java index 8c353395..8468bbee 100644 --- a/src/utils/Pair.java +++ b/src/utils/Pair.java @@ -29,4 +29,22 @@ public class Pair { return b; } + @Override + public boolean equals(Object o) { + if (o == this) + return true; + + if (!(o instanceof Pair)) + return false; + + Pair other = (Pair) o; + + return this.a.equals(other.getA()) && this.b.equals(other.getB()); + } + + @Override + public int hashCode() { + return this.a.hashCode() ^ this.b.hashCode(); + } + } diff --git a/src/utils/Triple.java b/src/utils/Triple.java new file mode 100644 index 00000000..c19ee068 --- /dev/null +++ b/src/utils/Triple.java @@ -0,0 +1,42 @@ +package utils; + +public class Triple { + + private T a; + private U b; + private V c; + + public Triple() { + } + + public Triple(T a, U b, V c) { + this.a = a; + this.b = b; + this.c = c; + } + + public void setA(T a) { + this.a = a; + } + + public T getA() { + return a; + } + + public void setB(U b) { + this.b = b; + } + + public U getB() { + return b; + } + + public void setC(V c) { + this.c = c; + } + + public V getC() { + return c; + } + +} diff --git a/src/v1feeder.java b/src/v1feeder.java index a2c13a30..ab032645 100644 --- a/src/v1feeder.java +++ b/src/v1feeder.java @@ -1,7 +1,9 @@ +import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.IOException; import java.io.OutputStream; +import java.math.BigDecimal; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketException; @@ -9,17 +11,32 @@ import java.net.SocketTimeoutException; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; +import org.json.simple.parser.ParseException; +import com.google.common.hash.HashCode; +import com.google.common.primitives.Bytes; import com.google.common.primitives.Ints; +import data.at.ATData; +import data.at.ATStateData; import data.block.BlockData; +import data.transaction.ATTransactionData; import data.transaction.TransactionData; +import qora.assets.Asset; import qora.block.Block; import qora.block.Block.ValidationResult; import qora.block.BlockChain; @@ -29,7 +46,10 @@ import repository.Repository; import repository.RepositoryManager; import transform.TransformationException; import transform.block.BlockTransformer; +import transform.transaction.ATTransactionTransformer; +import utils.Base58; import utils.Pair; +import utils.Triple; public class v1feeder extends Thread { @@ -77,6 +97,9 @@ public class v1feeder extends Thread { private long lastPingTimestamp = System.currentTimeMillis(); private List signatures = new ArrayList(); + private static Map, BigDecimal> legacyATFees; + private static Map> legacyATTransactions; + private v1feeder(String address, int port) throws InterruptedException { try { for (int i = 0; i < 10; ++i) @@ -177,6 +200,9 @@ public class v1feeder extends Thread { // shove into list int numSignatures = byteBuffer.getInt(); + if (numSignatures == 0) + throw new RuntimeException("No signatures from peer - are we up to date?"); + while (numSignatures-- > 0) { byte[] signature = new byte[SIGNATURE_LENGTH]; byteBuffer.get(signature); @@ -201,7 +227,7 @@ public class v1feeder extends Thread { byte[] blockBytes = new byte[byteBuffer.remaining()]; byteBuffer.get(blockBytes); - Pair> blockInfo = null; + Triple, List> blockInfo = null; try { blockInfo = BlockTransformer.fromBytes(blockBytes); @@ -211,7 +237,26 @@ public class v1feeder extends Thread { } try (final Repository repository = RepositoryManager.getRepository()) { - Block block = new Block(repository, blockInfo.getA(), blockInfo.getB()); + BlockData blockData = blockInfo.getA(); + + // Adjust AT state data to include fees + List atStates = new ArrayList(); + for (ATStateData atState : blockInfo.getC()) { + BigDecimal fees = legacyATFees.get(new Pair(atState.getATAddress(), claimedHeight)); + ATData atData = repository.getATRepository().fromATAddress(atState.getATAddress()); + + atStates.add(new ATStateData(atState.getATAddress(), claimedHeight, atData.getCreation(), null, atState.getStateHash(), fees)); + } + + // AT-Transaction injection goes here! + List transactions = blockInfo.getB(); + List atTransactions = legacyATTransactions.get(claimedHeight); + if (atTransactions != null) { + transactions.addAll(0, atTransactions); + blockData.setTransactionCount(blockData.getTransactionCount() + atTransactions.size()); + } + + Block block = new Block(repository, blockData, transactions, atStates); if (!block.isSignatureValid()) { LOGGER.error("Invalid block signature"); @@ -398,12 +443,88 @@ public class v1feeder extends Thread { } } + private static void readLegacyATs(String legacyATPathname) { + legacyATFees = new HashMap, BigDecimal>(); + legacyATTransactions = new HashMap>(); + + Path path = Paths.get(legacyATPathname); + + JSONArray json = null; + + try (BufferedReader in = Files.newBufferedReader(path)) { + json = (JSONArray) JSONValue.parseWithException(in); + } catch (IOException | ParseException e) { + throw new RuntimeException("Couldn't read legacy AT JSON file"); + } + + for (Object o : json) { + JSONObject entry = (JSONObject) o; + + int height = Integer.parseInt((String) entry.get("height")); + long timestamp = (Long) entry.get("timestamp"); + + JSONArray transactionEntries = (JSONArray) entry.get("transactions"); + + List transactions = new ArrayList(); + + for (Object t : transactionEntries) { + JSONObject transactionEntry = (JSONObject) t; + + String recipient = (String) transactionEntry.get("recipient"); + String sender = (String) transactionEntry.get("sender"); + BigDecimal amount = new BigDecimal((String) transactionEntry.get("amount")).setScale(8); + + if (recipient.equals("1111111111111111111111111")) { + // fee + legacyATFees.put(new Pair(sender, height), amount); + } else { + // Actual AT Transaction + String messageString = (String) transactionEntry.get("message"); + byte[] message = messageString.isEmpty() ? new byte[0] : HashCode.fromString(messageString).asBytes(); + int sequence = ((Long) transactionEntry.get("seq")).intValue(); + byte[] reference = Base58.decode((String) transactionEntry.get("reference")); + + // reference is AT's deploy tx signature + // sender's public key is genesis account + // zero fee + // timestamp is block's timestamp + // signature = duplicated hash of transaction data + + BigDecimal fee = BigDecimal.ZERO.setScale(8); + + TransactionData transactionData = new ATTransactionData(sender, recipient, amount, Asset.QORA, message, fee, timestamp, reference); + byte[] digest; + try { + digest = Crypto.digest(ATTransactionTransformer.toBytes(transactionData)); + byte[] signature = Bytes.concat(digest, digest); + + transactionData = new ATTransactionData(sender, recipient, amount, Asset.QORA, message, fee, timestamp, reference, signature); + } catch (TransformationException e) { + throw new RuntimeException("Couldn't transform AT Transaction into bytes", e); + } + + if (sequence > transactions.size()) + transactions.add(transactionData); + else + transactions.add(sequence, transactionData); + } + } + + if (!transactions.isEmpty()) + legacyATTransactions.put(height, transactions); + } + } + public static void main(String[] args) { - if (args.length == 0) { - System.err.println("usage: v1feeder v1-node-address [port]"); + if (args.length < 2 || args.length > 3) { + System.err.println("usage: v1feeder legacy-AT-json v1-node-address [port]"); + System.err.println("example: v1feeder legacy-ATs.json 10.0.0.100 9084"); System.exit(1); } + String legacyATPathname = args[0]; + readLegacyATs(legacyATPathname); + try { test.Common.setRepository(); } catch (DataException e) { @@ -419,8 +540,8 @@ public class v1feeder extends Thread { } // connect to v1 node - String address = args[0]; - int port = args.length > 1 ? Integer.valueOf(args[1]) : DEFAULT_PORT; + String address = args[1]; + int port = args.length > 2 ? Integer.valueOf(args[2]) : DEFAULT_PORT; try { new v1feeder(address, port).join();