From 2c51a0362b98fc81da70ba2918ef6798e9c89441 Mon Sep 17 00:00:00 2001 From: catbref Date: Fri, 26 Oct 2018 17:47:47 +0100 Subject: [PATCH] Finally syncs with qora1 chain! ATData no longer needs deploySignature as a link back to DeployATTransaction, but does need creator and creation [timestamp]. "creation" is critical for ordering ATs when creating/validating blocks. Similar changes to ATStateData, adding creation, stateHash (for quicker comparison with blocks received over the network), and fees incurred by running AT on that block. Also added more explicit constructors for different scenarios. BlockData upgraded from simplistic "atBytes" to use ATStateData (above) which has details on ATs run for that block, fees incurred, and a hash of the AT's state. atCount added to keep track of how many ATs ran. ATTransactions essentially reuse the GenesisAccount's publickey as creator/sender as they're brought into existence by the Qora code rather than an end user. ATTransactionData updated to reflect this and the AT's address used as a "sender" field. Account tidied up with respect to CIYAM ATs and setConfirmedBalance ensures there is a corresponding record in Accounts (DB table). Account, and subclasses, don't need "throws DataException" on constructor any more. Fixed bug in Asset Order matching where the matching engine would give up after first potential match instead of trying others. Lots more work on CIYAM AT, albeit mainly blind importing of old v1 ATs from static JSON file as they're all dead and new v2 implementation is not backwards compatible. More work on Blocks, mostly AT stuff, but also fork-based corruption prevention using fix from Qora v1. Payment-related transactions (multipayment, etc.) always expect/use non-null (albeit maybe empty) list of PaymentData when validating, processing or orphaning. Mainly a change in HSQLDBTransactionRepository.getPayments() Payment.isValid(byte[], PaymentData, BigDecimal, boolean isZeroAmountValid) didn't pass on isZeroAmountValid to called method - whoops! Lots of work on ATTransactions themselves. MessageTransactions incorrectly assumed the optional payment was always in Qora. Now fixed to use the transaction's provided assetId. Mass of fixes/additions to HSQLDBATRepository, especially fixing incorrect reference to Assets DB table! In HSQLDBDatabaseUpdates, bump QoraAmount type from DECIMAL(19,8) to DECIMAL(27,8) to allow for huge asset quantities. You WILL have to rebuild your database! --- src/data/at/ATData.java | 28 +- src/data/at/ATStateData.java | 47 ++- src/data/block/BlockData.java | 68 +++-- src/data/block/BlockTransactionData.java | 6 +- src/data/transaction/ATTransactionData.java | 17 +- src/qora/account/Account.java | 25 +- src/qora/account/GenesisAccount.java | 3 +- src/qora/account/PublicKeyAccount.java | 3 +- src/qora/assets/Order.java | 8 +- src/qora/at/AT.java | 23 +- src/qora/block/Block.java | 267 ++++++++++++++---- src/qora/block/GenesisBlock.java | 2 +- src/qora/payment/Payment.java | 99 ++++--- src/qora/transaction/ATTransaction.java | 119 +++++--- .../transaction/ArbitraryTransaction.java | 4 +- src/qora/transaction/DeployATTransaction.java | 1 - src/qora/transaction/GenesisTransaction.java | 15 +- src/qora/transaction/MessageTransaction.java | 2 +- src/repository/ATRepository.java | 8 + src/repository/AccountRepository.java | 2 + src/repository/hsqldb/HSQLDBATRepository.java | 98 +++++-- .../hsqldb/HSQLDBAccountRepository.java | 15 + .../hsqldb/HSQLDBBlockRepository.java | 13 +- .../hsqldb/HSQLDBDatabaseUpdates.java | 25 +- .../HSQLDBATTransactionRepository.java | 10 +- .../HSQLDBTransactionRepository.java | 15 +- src/test/ATTests.java | 14 +- src/test/NavigationTests.java | 2 +- src/test/SignatureTests.java | 6 +- src/test/TransactionTests.java | 34 +-- src/transform/Transformer.java | 3 + src/transform/block/BlockTransformer.java | 173 +++++++++--- .../transaction/ATTransactionTransformer.java | 92 ++++++ src/utils/Pair.java | 18 ++ src/utils/Triple.java | 42 +++ src/v1feeder.java | 133 ++++++++- 36 files changed, 1075 insertions(+), 365 deletions(-) create mode 100644 src/transform/transaction/ATTransactionTransformer.java create mode 100644 src/utils/Triple.java 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();