diff --git a/lib/org/ciyam/at/1.0/at-1.0.jar b/lib/org/ciyam/at/1.0/at-1.0.jar index 669e9887..57e52842 100644 Binary files a/lib/org/ciyam/at/1.0/at-1.0.jar and b/lib/org/ciyam/at/1.0/at-1.0.jar differ diff --git a/lib/org/ciyam/at/1.0/at-1.0.jar.md5 b/lib/org/ciyam/at/1.0/at-1.0.jar.md5 deleted file mode 100644 index be960be8..00000000 --- a/lib/org/ciyam/at/1.0/at-1.0.jar.md5 +++ /dev/null @@ -1 +0,0 @@ -ab1560171ae5c6c15b0dfa8e6cccc7f8 \ No newline at end of file diff --git a/lib/org/ciyam/at/1.0/at-1.0.jar.sha1 b/lib/org/ciyam/at/1.0/at-1.0.jar.sha1 deleted file mode 100644 index 29767395..00000000 --- a/lib/org/ciyam/at/1.0/at-1.0.jar.sha1 +++ /dev/null @@ -1 +0,0 @@ -c293c9656f43b432a08053f19ec5aa0de1cd10ea \ No newline at end of file diff --git a/lib/org/ciyam/at/1.0/at-1.0.pom b/lib/org/ciyam/at/1.0/at-1.0.pom index 05532457..973d4371 100644 --- a/lib/org/ciyam/at/1.0/at-1.0.pom +++ b/lib/org/ciyam/at/1.0/at-1.0.pom @@ -5,4 +5,5 @@ org.ciyam at 1.0 + POM was created from install:install-file diff --git a/lib/org/ciyam/at/1.0/at-1.0.pom.md5 b/lib/org/ciyam/at/1.0/at-1.0.pom.md5 deleted file mode 100644 index 811dfee8..00000000 --- a/lib/org/ciyam/at/1.0/at-1.0.pom.md5 +++ /dev/null @@ -1 +0,0 @@ -42f6e3eb3c6e510f65c963ce97583f05 \ No newline at end of file diff --git a/lib/org/ciyam/at/1.0/at-1.0.pom.sha1 b/lib/org/ciyam/at/1.0/at-1.0.pom.sha1 deleted file mode 100644 index 6b6618c9..00000000 --- a/lib/org/ciyam/at/1.0/at-1.0.pom.sha1 +++ /dev/null @@ -1 +0,0 @@ -490287647d3c69c05bd50ab565ffff86192ff423 \ No newline at end of file diff --git a/lib/org/ciyam/at/maven-metadata-local.xml b/lib/org/ciyam/at/maven-metadata-local.xml index 513f5312..da76b2ba 100644 --- a/lib/org/ciyam/at/maven-metadata-local.xml +++ b/lib/org/ciyam/at/maven-metadata-local.xml @@ -7,6 +7,6 @@ 1.0 - 20181015085522 + 20181101172102 diff --git a/lib/org/ciyam/at/maven-metadata.xml b/lib/org/ciyam/at/maven-metadata.xml deleted file mode 100644 index dd714561..00000000 --- a/lib/org/ciyam/at/maven-metadata.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - org.ciyam - at - - 1.0 - - 1.0 - - 20181015081124 - - diff --git a/lib/org/ciyam/at/maven-metadata.xml.md5 b/lib/org/ciyam/at/maven-metadata.xml.md5 deleted file mode 100644 index e8f9b7d2..00000000 --- a/lib/org/ciyam/at/maven-metadata.xml.md5 +++ /dev/null @@ -1 +0,0 @@ -2369bf36c52580a89d5ea71a0f037a82 \ No newline at end of file diff --git a/lib/org/ciyam/at/maven-metadata.xml.sha1 b/lib/org/ciyam/at/maven-metadata.xml.sha1 deleted file mode 100644 index 88816e70..00000000 --- a/lib/org/ciyam/at/maven-metadata.xml.sha1 +++ /dev/null @@ -1 +0,0 @@ -6bc38899b93ffce2286ae26f7af0b2d8b69db3cf \ No newline at end of file diff --git a/src/blockgenerator.java b/src/blockgenerator.java new file mode 100644 index 00000000..700482bd --- /dev/null +++ b/src/blockgenerator.java @@ -0,0 +1,73 @@ +import java.security.SecureRandom; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import qora.account.PrivateKeyAccount; +import qora.block.BlockChain; +import qora.block.BlockGenerator; +import repository.DataException; +import repository.Repository; +import repository.RepositoryManager; +import utils.Base58; + +public class blockgenerator { + + private static final Logger LOGGER = LogManager.getLogger(blockgenerator.class); + + public static void main(String[] args) { + if (args.length != 1) { + System.err.println("usage: blockgenerator private-key-base58 | 'RANDOM'"); + System.err.println("example: blockgenerator 7Vg53HrETZZuVySMPWJnVwQESS3dV8jCXPL5GDHMCeKS"); + System.exit(1); + } + + byte[] privateKey; + + if (args[0].equalsIgnoreCase("RANDOM")) { + privateKey = new byte[32]; + new SecureRandom().nextBytes(privateKey); + } else { + privateKey = Base58.decode(args[0]); + } + + try { + test.Common.setRepository(); + } catch (DataException e) { + LOGGER.error("Couldn't connect to repository", e); + System.exit(2); + } + + try { + BlockChain.validate(); + } catch (DataException e) { + LOGGER.error("Couldn't validate repository", e); + System.exit(2); + } + + BlockGenerator blockGenerator = new BlockGenerator(privateKey); + blockGenerator.start(); + + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + blockGenerator.shutdown(); + + try { + blockGenerator.join(); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + try { + test.Common.closeRepository(); + } catch (DataException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + }); + } + +} diff --git a/src/data/at/ATData.java b/src/data/at/ATData.java index 41278c51..388113b3 100644 --- a/src/data/at/ATData.java +++ b/src/data/at/ATData.java @@ -6,7 +6,7 @@ public class ATData { // Properties private String ATAddress; - private String creator; + private byte[] creatorPublicKey; private long creation; private int version; private byte[] codeBytes; @@ -19,10 +19,10 @@ public class ATData { // Constructors - public ATData(String ATAddress, String creator, long creation, int version, byte[] codeBytes, boolean isSleeping, Integer sleepUntilHeight, + public ATData(String ATAddress, byte[] creatorPublicKey, 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.creatorPublicKey = creatorPublicKey; this.creation = creation; this.version = version; this.codeBytes = codeBytes; @@ -34,9 +34,9 @@ public class ATData { this.frozenBalance = frozenBalance; } - public ATData(String ATAddress, String creator, long creation, int version, byte[] codeBytes, boolean isSleeping, Integer sleepUntilHeight, + public ATData(String ATAddress, byte[] creatorPublicKey, 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); + this(ATAddress, creatorPublicKey, creation, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, (BigDecimal) null); // Convert Long frozenBalance to BigDecimal if (frozenBalance != null) @@ -49,8 +49,8 @@ public class ATData { return this.ATAddress; } - public String getCreator() { - return this.creator; + public byte[] getCreatorPublicKey() { + return this.creatorPublicKey; } public long getCreation() { diff --git a/src/data/transaction/TransactionData.java b/src/data/transaction/TransactionData.java index a4447b2e..b15b1138 100644 --- a/src/data/transaction/TransactionData.java +++ b/src/data/transaction/TransactionData.java @@ -1,7 +1,10 @@ package data.transaction; import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Arrays; +import data.account.AccountData; import qora.transaction.Transaction.TransactionType; public abstract class TransactionData { @@ -59,4 +62,35 @@ public abstract class TransactionData { this.signature = signature; } + // Comparison + + @Override + public int hashCode() { + byte[] bytes = this.signature; + + // No signature? Use reference instead + if (bytes == null) + bytes = this.reference; + + return new BigInteger(bytes).intValue(); + } + + @Override + public boolean equals(Object other) { + // If we don't have a signature then fail + if (this.signature == null) + return false; + + if (!(other instanceof TransactionData)) + return false; + + TransactionData otherTransactionData = (TransactionData) other; + + // If other transactionData has no signature then fail + if (otherTransactionData.signature == null) + return false; + + return Arrays.equals(this.signature, otherTransactionData.signature); + } + } diff --git a/src/migrate.java b/src/migrate.java deleted file mode 100644 index 76aa541f..00000000 --- a/src/migrate.java +++ /dev/null @@ -1,634 +0,0 @@ -import static org.junit.Assert.*; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.math.BigDecimal; -import java.net.URL; -import java.nio.charset.Charset; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.sql.Timestamp; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -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.io.CharStreams; - -import qora.transaction.Transaction; -import repository.BlockRepository; -import repository.DataException; -import repository.Repository; -import repository.RepositoryManager; -import utils.Base58; - -public class migrate { - - private static final String connectionUrl = "jdbc:hsqldb:file:db/test;create=true;close_result=true;sql.strict_exec=true;sql.enforce_names=true;sql.syntax_mys=true"; - - private static final String GENESIS_ADDRESS = "QfGMeDQQUQePMpAmfLBJzgqyrM35RWxHGD"; - private static final byte[] GENESIS_PUBLICKEY = new byte[] { 1, 1, 1, 1, 1, 1, 1, 1 }; - - private static Map publicKeyByAddress = new HashMap(); - - public static Object fetchBlockJSON(int height) throws IOException { - try (InputStream is = new URL("http://localhost:9085/blocks/byheight/" + height).openStream(); - InputStreamReader isr = new InputStreamReader(is, Charset.forName("UTF-8")); - BufferedReader reader = new BufferedReader(isr)) { - return JSONValue.parseWithException(reader); - } catch (IOException e) { - return null; - } catch (ParseException e) { - return null; - } - } - - public static byte[] addressToPublicKey(String address) throws IOException { - byte[] cachedPublicKey = publicKeyByAddress.get(address); - if (cachedPublicKey != null) - return cachedPublicKey; - - InputStream is = new URL("http://localhost:9085/addresses/publickey/" + address).openStream(); - - try (InputStreamReader isr = new InputStreamReader(is, Charset.forName("UTF-8"))) { - String publicKey58 = CharStreams.toString(isr); - - byte[] publicKey = Base58.decode(publicKey58); - publicKeyByAddress.put(address, publicKey); - return publicKey; - } finally { - is.close(); - } - } - - public static void savePublicKeys(Connection connection) throws SQLException { - try (PreparedStatement pStmt = connection.prepareStatement("INSERT IGNORE INTO Test_public_keys VALUES (?, ?)")) { - for (Entry entry : publicKeyByAddress.entrySet()) { - pStmt.setString(1, entry.getKey()); - pStmt.setBytes(2, entry.getValue()); - pStmt.execute(); - } - } - } - - public static String formatWithPlaceholders(String... columns) { - String[] placeholders = new String[columns.length]; - Arrays.setAll(placeholders, (int i) -> "?"); - - StringBuilder output = new StringBuilder(); - output.append("("); - output.append(String.join(", ", columns)); - output.append(") VALUES ("); - output.append(String.join(", ", placeholders)); - output.append(")"); - return output.toString(); - } - - @SuppressWarnings("resource") - public static void main(String args[]) throws SQLException, DataException, IOException { - // Genesis public key - publicKeyByAddress.put(GENESIS_ADDRESS, GENESIS_PUBLICKEY); - // Some other public keys for addresses that have never created a transaction - publicKeyByAddress.put("QcDLhirHkSbR4TLYeShLzHw61B8UGTFusk", Base58.decode("HP58uWRBae654ze6ysmdyGv3qaDrr9BEk6cHv4WuiF7d")); - - // TODO convert to repository - Connection c = DriverManager.getConnection(connectionUrl); - - c.createStatement() - .execute("CREATE TABLE IF NOT EXISTS Test_public_keys ( address varchar(64), public_key varbinary(32) not null, primary key(address) )"); - c.createStatement().execute("CREATE INDEX IF NOT EXISTS Test_public_key_index ON Test_public_keys (public_key)"); - - test.Common.setRepository(); - - PreparedStatement blocksPStmt = c - .prepareStatement("INSERT INTO Blocks " + formatWithPlaceholders("signature", "version", "reference", "transaction_count", "total_fees", - "transactions_signature", "height", "generation", "generating_balance", "generator", "generator_signature", "AT_data", "AT_fees")); - - PreparedStatement txPStmt = c.prepareStatement( - "INSERT INTO Transactions " + formatWithPlaceholders("signature", "reference", "type", "creator", "creation", "fee", "milestone_block")); - - PreparedStatement recipientPStmt = c.prepareStatement("INSERT INTO TransactionRecipients " + formatWithPlaceholders("signature", "recipient")); - - PreparedStatement genesisPStmt = c.prepareStatement("INSERT INTO GenesisTransactions " + formatWithPlaceholders("signature", "recipient", "amount")); - PreparedStatement paymentPStmt = c - .prepareStatement("INSERT INTO PaymentTransactions " + formatWithPlaceholders("signature", "sender", "recipient", "amount")); - PreparedStatement registerNamePStmt = c - .prepareStatement("INSERT INTO RegisterNameTransactions " + formatWithPlaceholders("signature", "registrant", "name", "owner", "data")); - PreparedStatement updateNamePStmt = c.prepareStatement( - "INSERT INTO UpdateNameTransactions " + formatWithPlaceholders("signature", "owner", "name", "new_owner", "new_data", "name_reference")); - PreparedStatement sellNamePStmt = c - .prepareStatement("INSERT INTO SellNameTransactions " + formatWithPlaceholders("signature", "owner", "name", "amount")); - PreparedStatement cancelSellNamePStmt = c - .prepareStatement("INSERT INTO CancelSellNameTransactions " + formatWithPlaceholders("signature", "owner", "name")); - PreparedStatement buyNamePStmt = c.prepareStatement( - "INSERT INTO BuyNameTransactions " + formatWithPlaceholders("signature", "buyer", "name", "seller", "amount", "name_reference")); - PreparedStatement createPollPStmt = c - .prepareStatement("INSERT INTO CreatePollTransactions " + formatWithPlaceholders("signature", "creator", "owner", "poll_name", "description")); - PreparedStatement createPollOptionPStmt = c - .prepareStatement("INSERT INTO CreatePollTransactionOptions " + formatWithPlaceholders("signature", "option_name")); - PreparedStatement voteOnPollPStmt = c - .prepareStatement("INSERT INTO VoteOnPollTransactions " + formatWithPlaceholders("signature", "voter", "poll_name", "option_index")); - PreparedStatement arbitraryPStmt = c - .prepareStatement("INSERT INTO ArbitraryTransactions " + formatWithPlaceholders("signature", "creator", "service", "data_hash")); - PreparedStatement issueAssetPStmt = c.prepareStatement("INSERT INTO IssueAssetTransactions " - + formatWithPlaceholders("signature", "issuer", "owner", "asset_name", "description", "quantity", "is_divisible")); - PreparedStatement transferAssetPStmt = c - .prepareStatement("INSERT INTO TransferAssetTransactions " + formatWithPlaceholders("signature", "sender", "recipient", "asset_id", "amount")); - PreparedStatement createAssetOrderPStmt = c.prepareStatement("INSERT INTO CreateAssetOrderTransactions " - + formatWithPlaceholders("signature", "creator", "have_asset_id", "amount", "want_asset_id", "price")); - PreparedStatement cancelAssetOrderPStmt = c - .prepareStatement("INSERT INTO CancelAssetOrderTransactions " + formatWithPlaceholders("signature", "creator", "asset_order_id")); - PreparedStatement multiPaymentPStmt = c.prepareStatement("INSERT INTO MultiPaymentTransactions " + formatWithPlaceholders("signature", "sender")); - PreparedStatement deployATPStmt = c.prepareStatement("INSERT INTO DeployATTransactions " - + formatWithPlaceholders("signature", "creator", "AT_name", "description", "AT_type", "AT_tags", "creation_bytes", "amount")); - PreparedStatement messagePStmt = c.prepareStatement("INSERT INTO MessageTransactions " - + formatWithPlaceholders("signature", "version", "sender", "recipient", "is_text", "is_encrypted", "amount", "asset_id", "data")); - - PreparedStatement sharedPaymentPStmt = c - .prepareStatement("INSERT INTO SharedTransactionPayments " + formatWithPlaceholders("signature", "recipient", "amount", "asset_id")); - - PreparedStatement blockTxPStmt = c - .prepareStatement("INSERT INTO BlockTransactions " + formatWithPlaceholders("block_signature", "sequence", "transaction_signature")); - - int height; - try (final Repository repository = RepositoryManager.getRepository()) { - BlockRepository blockRepository = repository.getBlockRepository(); - height = blockRepository.getBlockchainHeight() + 1; - } - - byte[] milestone_block = null; - System.out.println("Starting migration from block height " + height); - - while (true) { - JSONObject json = (JSONObject) fetchBlockJSON(height); - if (json == null) - break; - - if (height % 1000 == 0) - System.out.println("Height: " + height + ", public key map size: " + publicKeyByAddress.size()); - - JSONArray transactions = (JSONArray) json.get("transactions"); - - // Blocks: - // signature, version, reference, transaction_count, total_fees, transactions_signature, height, generation, generating_balance, generator, - // generator_signature - // varchar, tinyint, varchar, int, decimal, varchar, int, timestamp, decimal, varchar, varchar - byte[] blockSignature = Base58.decode((String) json.get("signature")); - byte[] blockReference = Base58.decode((String) json.get("reference")); - byte[] blockTransactionsSignature = Base58.decode((String) json.get("transactionsSignature")); - byte[] blockGeneratorSignature = Base58.decode((String) json.get("generatorSignature")); - - byte[] generatorPublicKey = addressToPublicKey((String) json.get("generator")); - - blocksPStmt.setBytes(1, blockSignature); - blocksPStmt.setInt(2, ((Long) json.get("version")).intValue()); - blocksPStmt.setBytes(3, blockReference); - blocksPStmt.setInt(4, transactions.size()); - blocksPStmt.setBigDecimal(5, BigDecimal.valueOf(Double.valueOf((String) json.get("fee")).doubleValue())); - blocksPStmt.setBytes(6, blockTransactionsSignature); - blocksPStmt.setInt(7, height); - blocksPStmt.setTimestamp(8, new Timestamp((Long) json.get("timestamp"))); - blocksPStmt.setBigDecimal(9, BigDecimal.valueOf((Long) json.get("generatingBalance"))); - blocksPStmt.setBytes(10, generatorPublicKey); - blocksPStmt.setBytes(11, blockGeneratorSignature); - - String blockATs = (String) json.get("blockATs"); - if (blockATs != null && blockATs.length() > 0) { - HashCode atBytes = HashCode.fromString(blockATs); - - blocksPStmt.setBytes(12, atBytes.asBytes()); - blocksPStmt.setBigDecimal(13, BigDecimal.valueOf(((Long) json.get("atFees")).longValue(), 8)); - } else { - blocksPStmt.setNull(12, java.sql.Types.VARBINARY); - blocksPStmt.setNull(13, java.sql.Types.DECIMAL); - } - - blocksPStmt.execute(); - blocksPStmt.clearParameters(); - - // Transactions: - // signature, reference, type, creator, creation, fee, milestone_block - // varchar, varchar, int, varchar, timestamp, decimal, varchar - for (int txIndex = 0; txIndex < transactions.size(); ++txIndex) { - JSONObject transaction = (JSONObject) transactions.get(txIndex); - - byte[] txSignature = Base58.decode((String) transaction.get("signature")); - txPStmt.setBytes(1, txSignature); - - String txReference58 = (String) transaction.get("reference"); - byte[] txReference = txReference58.isEmpty() ? null : Base58.decode(txReference58); - int type = ((Long) transaction.get("type")).intValue(); - - if (txReference != null) - txPStmt.setBytes(2, txReference); - else if (height == 1 && type == 1) - txPStmt.setNull(2, java.sql.Types.VARBINARY); // genesis transactions only - else - fail(); - - txPStmt.setInt(3, type); - - // Determine transaction "creator" from specific transaction info - switch (type) { - case 1: // genesis - txPStmt.setBytes(4, GENESIS_PUBLICKEY); // genesis transactions only - break; - - case 2: // payment - case 12: // transfer asset - case 15: // multi-payment - txPStmt.setBytes(4, addressToPublicKey((String) transaction.get("sender"))); - break; - - case 3: // register name - txPStmt.setBytes(4, addressToPublicKey((String) transaction.get("registrant"))); - break; - - case 4: // update name - case 5: // sell name - case 6: // cancel sell name - txPStmt.setBytes(4, addressToPublicKey((String) transaction.get("owner"))); - break; - - case 7: // buy name - txPStmt.setBytes(4, addressToPublicKey((String) transaction.get("buyer"))); - break; - - case 8: // create poll - case 9: // vote on poll - case 10: // arbitrary transaction - case 11: // issue asset - case 13: // create asset order - case 14: // cancel asset order - case 16: // deploy CIYAM AT - case 17: // message - txPStmt.setBytes(4, addressToPublicKey((String) transaction.get("creator"))); - break; - - default: - fail(); - } - - long transactionTimestamp = ((Long) transaction.get("timestamp")).longValue(); - txPStmt.setTimestamp(5, new Timestamp(transactionTimestamp)); - txPStmt.setBigDecimal(6, BigDecimal.valueOf(Double.valueOf((String) transaction.get("fee")).doubleValue())); - - if (milestone_block != null) - txPStmt.setBytes(7, milestone_block); - else if (height == 1 && type == 1) - txPStmt.setNull(7, java.sql.Types.VARBINARY); // genesis transactions only - else - fail(); - - txPStmt.execute(); - txPStmt.clearParameters(); - - JSONArray multiPayments = null; - if (type == 15) - multiPayments = (JSONArray) transaction.get("payments"); - - List recipients = new ArrayList(); - switch (type) { - case 1: // genesis - case 2: // payment - case 12: // transfer asset - case 17: // message - recipients.add((String) transaction.get("recipient")); - break; - - case 3: // register name - case 4: // update name - // parse Name data for "owner" - break; - - case 5: // sell name - case 6: // cancel sell name - case 8: // create poll - case 9: // vote on poll - case 10: // arbitrary transaction - case 13: // create asset order - case 14: // cancel asset order - case 16: // deploy CIYAM AT - // no recipients - break; - - case 7: // buy name - recipients.add((String) transaction.get("seller")); - break; - - case 11: // issue asset - recipients.add((String) transaction.get("creator")); - break; - - case 15: // multi-payment - assertNotNull(multiPayments); - for (Object payment : multiPayments) { - String recipient = (String) ((JSONObject) payment).get("recipient"); - recipients.add(recipient); - } - break; - - default: - fail(); - } - - for (String recipient : recipients) { - recipientPStmt.setBytes(1, txSignature); - recipientPStmt.setString(2, recipient); - - recipientPStmt.execute(); - recipientPStmt.clearParameters(); - } - - // Transaction-type-specific processing - switch (type) { - case 1: // genesis - genesisPStmt.setBytes(1, txSignature); - genesisPStmt.setString(2, recipients.get(0)); - genesisPStmt.setBigDecimal(3, BigDecimal.valueOf(Double.valueOf((String) transaction.get("amount")).doubleValue())); - - genesisPStmt.execute(); - genesisPStmt.clearParameters(); - break; - - case 2: // payment - paymentPStmt.setBytes(1, txSignature); - paymentPStmt.setBytes(2, addressToPublicKey((String) transaction.get("sender"))); - paymentPStmt.setString(3, recipients.get(0)); - paymentPStmt.setBigDecimal(4, BigDecimal.valueOf(Double.valueOf((String) transaction.get("amount")).doubleValue())); - - paymentPStmt.execute(); - paymentPStmt.clearParameters(); - break; - - case 3: // register name - registerNamePStmt.setBytes(1, txSignature); - registerNamePStmt.setBytes(2, addressToPublicKey((String) transaction.get("registrant"))); - registerNamePStmt.setString(3, (String) transaction.get("name")); - registerNamePStmt.setString(4, (String) transaction.get("owner")); - registerNamePStmt.setString(5, (String) transaction.get("value")); - - registerNamePStmt.execute(); - registerNamePStmt.clearParameters(); - break; - - case 4: // update name - updateNamePStmt.setBytes(1, txSignature); - updateNamePStmt.setBytes(2, addressToPublicKey((String) transaction.get("owner"))); - updateNamePStmt.setString(3, (String) transaction.get("name")); - updateNamePStmt.setString(4, (String) transaction.get("newOwner")); - updateNamePStmt.setString(5, (String) transaction.get("newValue")); - updateNamePStmt.setBytes(6, txSignature); // dummy value for name_reference - - updateNamePStmt.execute(); - updateNamePStmt.clearParameters(); - break; - - case 5: // sell name - sellNamePStmt.setBytes(1, txSignature); - sellNamePStmt.setBytes(2, addressToPublicKey((String) transaction.get("owner"))); - sellNamePStmt.setString(3, (String) transaction.get("name")); - sellNamePStmt.setBigDecimal(4, BigDecimal.valueOf(Double.valueOf((String) transaction.get("amount")).doubleValue())); - - sellNamePStmt.execute(); - sellNamePStmt.clearParameters(); - break; - - case 6: // cancel sell name - cancelSellNamePStmt.setBytes(1, txSignature); - cancelSellNamePStmt.setBytes(2, addressToPublicKey((String) transaction.get("owner"))); - cancelSellNamePStmt.setString(3, (String) transaction.get("name")); - - cancelSellNamePStmt.execute(); - cancelSellNamePStmt.clearParameters(); - break; - - case 7: // buy name - buyNamePStmt.setBytes(1, txSignature); - buyNamePStmt.setBytes(2, addressToPublicKey((String) transaction.get("buyer"))); - buyNamePStmt.setString(3, (String) transaction.get("name")); - buyNamePStmt.setString(4, (String) transaction.get("seller")); - buyNamePStmt.setBigDecimal(5, BigDecimal.valueOf(Double.valueOf((String) transaction.get("amount")).doubleValue())); - buyNamePStmt.setBytes(6, txSignature); // dummy value for name_reference - - buyNamePStmt.execute(); - buyNamePStmt.clearParameters(); - break; - - case 8: // create poll - createPollPStmt.setBytes(1, txSignature); - createPollPStmt.setBytes(2, addressToPublicKey((String) transaction.get("creator"))); - // In gen1, there are no polls where the owner is not the creator - createPollPStmt.setString(3, (String) transaction.get("creator")); // owner - createPollPStmt.setString(4, (String) transaction.get("name")); - createPollPStmt.setString(5, (String) transaction.get("description")); - - createPollPStmt.execute(); - createPollPStmt.clearParameters(); - - // options - JSONArray options = (JSONArray) transaction.get("options"); - for (Object option : options) { - createPollOptionPStmt.setBytes(1, txSignature); - createPollOptionPStmt.setString(2, (String) option); - - createPollOptionPStmt.execute(); - createPollOptionPStmt.clearParameters(); - } - break; - - case 9: // vote on poll - voteOnPollPStmt.setBytes(1, txSignature); - voteOnPollPStmt.setBytes(2, addressToPublicKey((String) transaction.get("creator"))); - voteOnPollPStmt.setString(3, (String) transaction.get("poll")); - voteOnPollPStmt.setInt(4, ((Long) transaction.get("option")).intValue()); - - voteOnPollPStmt.execute(); - voteOnPollPStmt.clearParameters(); - break; - - case 10: // arbitrary transactions - arbitraryPStmt.setBytes(1, txSignature); - arbitraryPStmt.setBytes(2, addressToPublicKey((String) transaction.get("creator"))); - arbitraryPStmt.setInt(3, ((Long) transaction.get("service")).intValue()); - arbitraryPStmt.setString(4, "TODO"); - - arbitraryPStmt.execute(); - arbitraryPStmt.clearParameters(); - - if (multiPayments != null) - for (Object paymentObj : multiPayments) { - JSONObject payment = (JSONObject) paymentObj; - - sharedPaymentPStmt.setBytes(1, txSignature); - sharedPaymentPStmt.setString(2, (String) payment.get("recipient")); - sharedPaymentPStmt.setBigDecimal(3, BigDecimal.valueOf(Double.valueOf((String) payment.get("amount")).doubleValue())); - sharedPaymentPStmt.setLong(4, ((Long) payment.get("asset")).longValue()); - - sharedPaymentPStmt.execute(); - sharedPaymentPStmt.clearParameters(); - } - break; - - case 11: // issue asset - issueAssetPStmt.setBytes(1, txSignature); - issueAssetPStmt.setBytes(2, addressToPublicKey((String) transaction.get("creator"))); - // In gen1, there are no polls where the owner is not the creator - issueAssetPStmt.setString(3, (String) transaction.get("creator")); // owner - issueAssetPStmt.setString(4, (String) transaction.get("name")); - issueAssetPStmt.setString(5, (String) transaction.get("description")); - issueAssetPStmt.setBigDecimal(6, BigDecimal.valueOf(((Long) transaction.get("quantity")).longValue())); - issueAssetPStmt.setBoolean(7, (Boolean) transaction.get("divisible")); - - issueAssetPStmt.execute(); - issueAssetPStmt.clearParameters(); - break; - - case 12: // transfer asset - transferAssetPStmt.setBytes(1, txSignature); - transferAssetPStmt.setBytes(2, addressToPublicKey((String) transaction.get("sender"))); - transferAssetPStmt.setString(3, (String) transaction.get("recipient")); - transferAssetPStmt.setLong(4, ((Long) transaction.get("asset")).longValue()); - transferAssetPStmt.setBigDecimal(5, BigDecimal.valueOf(Double.valueOf((String) transaction.get("amount")).doubleValue())); - - transferAssetPStmt.execute(); - transferAssetPStmt.clearParameters(); - break; - - case 13: // create asset order - createAssetOrderPStmt.setBytes(1, txSignature); - createAssetOrderPStmt.setBytes(2, addressToPublicKey((String) transaction.get("creator"))); - - JSONObject assetOrder = (JSONObject) transaction.get("order"); - createAssetOrderPStmt.setLong(3, ((Long) assetOrder.get("have")).longValue()); - createAssetOrderPStmt.setBigDecimal(4, BigDecimal.valueOf(Double.valueOf((String) assetOrder.get("amount")).doubleValue())); - createAssetOrderPStmt.setLong(5, ((Long) assetOrder.get("want")).longValue()); - createAssetOrderPStmt.setBigDecimal(6, BigDecimal.valueOf(Double.valueOf((String) assetOrder.get("price")).doubleValue())); - - createAssetOrderPStmt.execute(); - createAssetOrderPStmt.clearParameters(); - break; - - case 14: // cancel asset order - cancelAssetOrderPStmt.setBytes(1, txSignature); - cancelAssetOrderPStmt.setBytes(2, addressToPublicKey((String) transaction.get("creator"))); - cancelAssetOrderPStmt.setBytes(3, Base58.decode((String) transaction.get("order"))); - - cancelAssetOrderPStmt.execute(); - cancelAssetOrderPStmt.clearParameters(); - break; - - case 15: // multi-payment - multiPaymentPStmt.setBytes(1, txSignature); - multiPaymentPStmt.setBytes(2, addressToPublicKey((String) transaction.get("sender"))); - - multiPaymentPStmt.execute(); - multiPaymentPStmt.clearParameters(); - - for (Object paymentObj : multiPayments) { - JSONObject payment = (JSONObject) paymentObj; - - sharedPaymentPStmt.setBytes(1, txSignature); - sharedPaymentPStmt.setString(2, (String) payment.get("recipient")); - sharedPaymentPStmt.setBigDecimal(3, BigDecimal.valueOf(Double.valueOf((String) payment.get("amount")).doubleValue())); - sharedPaymentPStmt.setLong(4, ((Long) payment.get("asset")).longValue()); - - sharedPaymentPStmt.execute(); - sharedPaymentPStmt.clearParameters(); - } - break; - - case 16: // deploy AT - HashCode creationBytes = HashCode.fromString((String) transaction.get("creationBytes")); - - deployATPStmt.setBytes(1, txSignature); - deployATPStmt.setBytes(2, addressToPublicKey((String) transaction.get("creator"))); - deployATPStmt.setString(3, (String) transaction.get("name")); - deployATPStmt.setString(4, (String) transaction.get("description")); - deployATPStmt.setString(5, (String) transaction.get("atType")); - deployATPStmt.setString(6, (String) transaction.get("tags")); - deployATPStmt.setBytes(7, creationBytes.asBytes()); - deployATPStmt.setBigDecimal(8, BigDecimal.valueOf(Double.valueOf((String) transaction.get("amount")).doubleValue())); - - deployATPStmt.execute(); - deployATPStmt.clearParameters(); - break; - - case 17: // message - boolean isText = (Boolean) transaction.get("isText"); - boolean isEncrypted = (Boolean) transaction.get("encrypted"); - String messageData = (String) transaction.get("data"); - - byte[] messageDataBytes; - if (isText && !isEncrypted) { - messageDataBytes = messageData.getBytes("UTF-8"); - } else { - HashCode messageBytes = HashCode.fromString(messageData); - messageDataBytes = messageBytes.asBytes(); - } - - messagePStmt.setBytes(1, txSignature); - messagePStmt.setInt(2, Transaction.getVersionByTimestamp(transactionTimestamp)); - messagePStmt.setBytes(3, addressToPublicKey((String) transaction.get("creator"))); - messagePStmt.setString(4, (String) transaction.get("recipient")); - messagePStmt.setBoolean(5, isText); - messagePStmt.setBoolean(6, isEncrypted); - messagePStmt.setBigDecimal(7, BigDecimal.valueOf(Double.valueOf((String) transaction.get("amount")).doubleValue())); - - if (transaction.containsKey("asset")) - messagePStmt.setLong(8, ((Long) transaction.get("asset")).longValue()); - else - messagePStmt.setLong(8, 0L); // QORA simulated asset - - messagePStmt.setBytes(9, messageDataBytes); - - messagePStmt.execute(); - messagePStmt.clearParameters(); - break; - - default: - // fail(); - } - - blockTxPStmt.setBytes(1, blockSignature); - blockTxPStmt.setInt(2, txIndex); - blockTxPStmt.setBytes(3, txSignature); - - blockTxPStmt.execute(); - blockTxPStmt.clearParameters(); - - // repository.saveChanges(); - } - - // new milestone block every 500 blocks? - if (milestone_block == null || (height % 500) == 0) - milestone_block = blockSignature; - - ++height; - } - - savePublicKeys(c); - - c.close(); - - try (final Repository repository = RepositoryManager.getRepository()) { - BlockRepository blockRepository = repository.getBlockRepository(); - System.out.println("Migration finished with new blockchain height " + blockRepository.getBlockchainHeight()); - } - - RepositoryManager.closeRepositoryFactory(); - } - -} diff --git a/src/qora/at/AT.java b/src/qora/at/AT.java index 1eb8e762..10d98a65 100644 --- a/src/qora/at/AT.java +++ b/src/qora/at/AT.java @@ -2,14 +2,16 @@ package qora.at; import java.math.BigDecimal; import java.nio.ByteBuffer; +import java.util.List; 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 qora.transaction.ATTransaction; +import repository.ATRepository; import repository.DataException; import repository.Repository; @@ -28,13 +30,17 @@ public class AT { this.atStateData = atStateData; } - /** Deploying AT */ + public AT(Repository repository, ATData atData) { + this(repository, atData, null); + } + + /** Constructs AT-handling object when deploying AT */ public AT(Repository repository, DeployATTransactionData deployATTransactionData) throws DataException { this.repository = repository; String atAddress = deployATTransactionData.getATAddress(); int height = this.repository.getBlockRepository().getBlockchainHeight() + 1; - String creator = new PublicKeyAccount(repository, deployATTransactionData.getCreatorPublicKey()).getAddress(); + byte[] creatorPublicKey = deployATTransactionData.getCreatorPublicKey(); long creation = deployATTransactionData.getTimestamp(); byte[] creationBytes = deployATTransactionData.getCreationBytes(); @@ -43,7 +49,7 @@ public class AT { if (version >= 2) { MachineState machineState = new MachineState(deployATTransactionData.getCreationBytes()); - this.atData = new ATData(atAddress, creator, creation, machineState.version, machineState.getCodeBytes(), machineState.getIsSleeping(), + this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, machineState.getCodeBytes(), machineState.getIsSleeping(), machineState.getSleepUntilHeight(), machineState.getIsFinished(), machineState.getHadFatalError(), machineState.getIsFrozen(), machineState.getFrozenBalance()); @@ -52,15 +58,23 @@ public class AT { this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, BigDecimal.ZERO.setScale(8)); } else { - // Legacy v1 AT in 'dead' state + // Legacy v1 AT + // We deploy these in 'dead' state as they will never be run on Qora2 + // Extract code bytes length ByteBuffer byteBuffer = ByteBuffer.wrap(deployATTransactionData.getCreationBytes()); + // v1 AT header is: version, reserved, code-pages, data-pages, call-stack-pages, user-stack-pages (all shorts) + + // Number of code pages short numCodePages = byteBuffer.get(2 + 2); + // Skip header and also "minimum activation amount" (long) byteBuffer.position(6 * 2 + 8); + int codeLen = 0; + // Extract actual code length, stored in minimal-size form (byte, short or int) if (numCodePages * 256 < 257) { codeLen = (int) (byteBuffer.get() & 0xff); } else if (numCodePages * 256 < Short.MAX_VALUE + 1) { @@ -73,20 +87,71 @@ public class AT { byte[] codeBytes = new byte[codeLen]; byteBuffer.get(codeBytes); - this.atData = new ATData(atAddress, creator, creation, 1, codeBytes, false, null, true, false, false, (Long) null); + // Create AT but in dead state + boolean isSleeping = false; + Integer sleepUntilHeight = null; + boolean isFinished = true; + boolean hadFatalError = false; + boolean isFrozen = false; + Long frozenBalance = null; + + this.atData = new ATData(atAddress, creatorPublicKey, creation, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, + frozenBalance); this.atStateData = new ATStateData(atAddress, height, creation, null, null, BigDecimal.ZERO.setScale(8)); } } + // Getters / setters + + public ATStateData getATStateData() { + return this.atStateData; + } + // Processing public void deploy() throws DataException { - this.repository.getATRepository().save(this.atData); + ATRepository atRepository = this.repository.getATRepository(); + atRepository.save(this.atData); + + // For version 2+ we also store initial AT state data + if (this.atData.getVersion() >= 2) + atRepository.save(this.atStateData); } public void undeploy() throws DataException { // AT states deleted implicitly by repository this.repository.getATRepository().delete(this.atData.getATAddress()); } + + public List run(long blockTimestamp) throws DataException { + String atAddress = this.atData.getATAddress(); + + QoraATAPI api = new QoraATAPI(repository, this.atData, blockTimestamp); + QoraATLogger logger = new QoraATLogger(); + + byte[] codeBytes = this.atData.getCodeBytes(); + + // Fetch latest ATStateData for this AT (if any) + ATStateData atStateData = this.repository.getATRepository().getLatestATState(atAddress); + + // There should be at least initial AT state data + if (atStateData == null) + throw new IllegalStateException("No initial AT state data found"); + + // [Re]create AT machine state using AT state data or from scratch as applicable + MachineState state = MachineState.fromBytes(api, logger, atStateData.getStateData(), codeBytes); + state.execute(); + + int height = this.repository.getBlockRepository().getBlockchainHeight() + 1; + long creation = this.atData.getCreation(); + byte[] stateData = state.toBytes(); + byte[] stateHash = Crypto.digest(stateData); + BigDecimal atFees = api.calcFinalFees(state); + + this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, atFees); + + return api.getTransactions(); + } + } diff --git a/src/qora/at/QoraATAPI.java b/src/qora/at/QoraATAPI.java new file mode 100644 index 00000000..9fbe7335 --- /dev/null +++ b/src/qora/at/QoraATAPI.java @@ -0,0 +1,490 @@ +package qora.at; + +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; + +import org.ciyam.at.API; +import org.ciyam.at.ExecutionException; +import org.ciyam.at.FunctionData; +import org.ciyam.at.IllegalFunctionCodeException; +import org.ciyam.at.MachineState; +import org.ciyam.at.OpCode; +import org.ciyam.at.Timestamp; + +import com.google.common.primitives.Bytes; + +import data.at.ATData; +import data.block.BlockData; +import data.transaction.ATTransactionData; +import data.transaction.MessageTransactionData; +import data.transaction.PaymentTransactionData; +import data.transaction.TransactionData; +import qora.account.Account; +import qora.account.PublicKeyAccount; +import qora.assets.Asset; +import qora.block.Block; +import qora.crypto.Crypto; +import qora.transaction.ATTransaction; +import qora.transaction.Transaction; +import repository.BlockRepository; +import repository.DataException; +import repository.Repository; + +public class QoraATAPI extends API { + + // Useful constants + private static final BigDecimal FEE_PER_STEP = BigDecimal.valueOf(1.0).setScale(8); // 1 Qora per "step" + private static final int MAX_STEPS_PER_ROUND = 500; + private static final int STEPS_PER_FUNCTION_CALL = 10; + private static final int MINUTES_PER_BLOCK = 10; + + // Properties + Repository repository; + ATData atData; + long blockTimestamp; + + /** List of generated AT transactions */ + List transactions; + + // Constructors + + public QoraATAPI(Repository repository, ATData atData, long blockTimestamp) { + this.repository = repository; + this.atData = atData; + this.transactions = new ArrayList(); + this.blockTimestamp = blockTimestamp; + } + + // Methods specific to Qora AT processing, not inherited + + public List getTransactions() { + return this.transactions; + } + + public BigDecimal calcFinalFees(MachineState state) { + return FEE_PER_STEP.multiply(BigDecimal.valueOf(state.getSteps())); + } + + // Inherited methods from CIYAM AT API + + @Override + public int getMaxStepsPerRound() { + return MAX_STEPS_PER_ROUND; + } + + @Override + public int getOpCodeSteps(OpCode opcode) { + if (opcode.value >= OpCode.EXT_FUN.value && opcode.value <= OpCode.EXT_FUN_RET_DAT_2.value) + return STEPS_PER_FUNCTION_CALL; + + return 1; + } + + @Override + public long getFeePerStep() { + return FEE_PER_STEP.unscaledValue().longValue(); + } + + @Override + public int getCurrentBlockHeight() { + try { + return this.repository.getBlockRepository().getBlockchainHeight(); + } catch (DataException e) { + throw new RuntimeException("AT API unable to fetch current blockchain height?", e); + } + } + + @Override + public int getATCreationBlockHeight(MachineState state) { + try { + return this.repository.getATRepository().getATCreationBlockHeight(this.atData.getATAddress()); + } catch (DataException e) { + throw new RuntimeException("AT API unable to fetch AT's creation block height?", e); + } + } + + @Override + public void putPreviousBlockHashInA(MachineState state) { + try { + BlockData blockData = this.repository.getBlockRepository().fromHeight(this.getPreviousBlockHeight()); + + // Block's signature is 128 bytes so we need to reduce this to 4 longs (32 bytes) + byte[] blockHash = Crypto.digest(blockData.getSignature()); + + this.setA1(state, fromBytes(blockHash, 0)); + this.setA2(state, fromBytes(blockHash, 8)); + this.setA3(state, fromBytes(blockHash, 16)); + this.setA4(state, fromBytes(blockHash, 24)); + } catch (DataException e) { + throw new RuntimeException("AT API unable to fetch previous block?", e); + } + } + + @Override + public void putTransactionAfterTimestampInA(Timestamp timestamp, MachineState state) { + // "Timestamp" is block height and transaction sequence + int height = timestamp.blockHeight; + int sequence = timestamp.transactionSequence + 1; + + Account atAccount = new Account(this.repository, this.atData.getATAddress()); + BlockRepository blockRepository = this.repository.getBlockRepository(); + + try { + while (height <= blockRepository.getBlockchainHeight()) { + BlockData blockData = blockRepository.fromHeight(height); + + if (blockData == null) + throw new DataException("Unable to fetch block " + height + " from repository?"); + + Block block = new Block(this.repository, blockData); + + List transactions = block.getTransactions(); + + // No more transactions in this block? Try next block + if (sequence >= transactions.size()) { + ++height; + sequence = 0; + continue; + } + + Transaction transaction = transactions.get(sequence); + + // Transaction needs to be sent to this AT + if (transaction.getRecipientAccounts().contains(atAccount)) { + // Found a transaction + + this.setA1(state, new Timestamp(height, sequence).longValue()); + + // Hash transaction's signature into other three A fields for future verification that it's the same transaction + byte[] hash = sha192(transaction.getTransactionData().getSignature()); + + this.setA2(state, fromBytes(hash, 0)); + this.setA3(state, fromBytes(hash, 8)); + this.setA4(state, fromBytes(hash, 16)); + return; + } + + // Transaction wasn't for us - keep going + ++sequence; + } + + // No more transactions - zero A and exit + this.zeroA(state); + } catch (DataException e) { + throw new RuntimeException("AT API unable to fetch next transaction?", e); + } + } + + @Override + public long getTypeFromTransactionInA(MachineState state) { + TransactionData transactionData = this.fetchTransaction(state); + + switch (transactionData.getType()) { + case PAYMENT: + return ATTransactionType.PAYMENT.value; + + case MESSAGE: + return ATTransactionType.MESSAGE.value; + + case AT: + if (((ATTransactionData) transactionData).getAmount() != null) + return ATTransactionType.PAYMENT.value; + else + return ATTransactionType.MESSAGE.value; + + default: + return 0xffffffffffffffffL; + } + } + + @Override + public long getAmountFromTransactionInA(MachineState state) { + TransactionData transactionData = this.fetchTransaction(state); + + switch (transactionData.getType()) { + case PAYMENT: + return ((PaymentTransactionData) transactionData).getAmount().unscaledValue().longValue(); + + case AT: + BigDecimal amount = ((ATTransactionData) transactionData).getAmount(); + + if (amount != null) + return amount.unscaledValue().longValue(); + else + return 0xffffffffffffffffL; + + default: + return 0xffffffffffffffffL; + } + } + + @Override + public long getTimestampFromTransactionInA(MachineState state) { + // Transaction's "timestamp" already stored in A1 + Timestamp timestamp = new Timestamp(state.getA1()); + return timestamp.longValue(); + } + + @Override + public long generateRandomUsingTransactionInA(MachineState state) { + // The plan here is to sleep for a block then use next block's signature and this transaction's signature to generate pseudo-random, but deterministic, + // value. + + if (!isFirstOpCodeAfterSleeping(state)) { + // First call + + // Sleep for a block + this.setIsSleeping(state, true); + + return 0L; // not used + } else { + // Second call + + // HASH(A and new block hash) + TransactionData transactionData = this.fetchTransaction(state); + + try { + BlockData blockData = this.repository.getBlockRepository().getLastBlock(); + + if (blockData == null) + throw new RuntimeException("AT API unable to fetch latest block?"); + + byte[] input = Bytes.concat(transactionData.getSignature(), blockData.getSignature()); + + byte[] hash = Crypto.digest(input); + + return fromBytes(hash, 0); + } catch (DataException e) { + throw new RuntimeException("AT API unable to fetch latest block from repository?", e); + } + } + } + + @Override + public void putMessageFromTransactionInAIntoB(MachineState state) { + // Zero B in case of issues or shorter-than-B message + this.zeroB(state); + + TransactionData transactionData = this.fetchTransaction(state); + + byte[] messageData = null; + + switch (transactionData.getType()) { + case MESSAGE: + messageData = ((MessageTransactionData) transactionData).getData(); + break; + + case AT: + messageData = ((ATTransactionData) transactionData).getMessage(); + break; + + default: + return; + } + + // Check data length is appropriate, i.e. not larger than B + if (messageData.length > 4 * 8) + return; + + // Pad messageData to fit B + byte[] paddedMessageData = Bytes.ensureCapacity(messageData, 4 * 8, 0); + + // Endian must be correct here so that (for example) a SHA256 message can be compared to one generated locally + ByteBuffer digestByteBuffer = ByteBuffer.wrap(paddedMessageData); + digestByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + + this.setB1(state, digestByteBuffer.getLong()); + this.setB2(state, digestByteBuffer.getLong()); + this.setB3(state, digestByteBuffer.getLong()); + this.setB4(state, digestByteBuffer.getLong()); + } + + @Override + public void putAddressFromTransactionInAIntoB(MachineState state) { + TransactionData transactionData = this.fetchTransaction(state); + + // We actually use public key as it has more potential utility (e.g. message verification) than an address + byte[] bytes = transactionData.getCreatorPublicKey(); + + // Enforce endian + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + + this.setB1(state, byteBuffer.getLong()); + this.setB2(state, byteBuffer.getLong()); + this.setB3(state, byteBuffer.getLong()); + this.setB4(state, byteBuffer.getLong()); + } + + @Override + public void putCreatorAddressIntoB(MachineState state) { + // We actually use public key as it has more potential utility (e.g. message verification) than an address + byte[] bytes = atData.getCreatorPublicKey(); + + // Enforce endian + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + + this.setB1(state, byteBuffer.getLong()); + this.setB2(state, byteBuffer.getLong()); + this.setB3(state, byteBuffer.getLong()); + this.setB4(state, byteBuffer.getLong()); + } + + @Override + public long getCurrentBalance(MachineState state) { + Account atAccount = new Account(this.repository, this.atData.getATAddress()); + + try { + return atAccount.getConfirmedBalance(Asset.QORA).unscaledValue().longValue(); + } catch (DataException e) { + throw new RuntimeException("AT API unable to fetch AT's current balance?", e); + } + } + + @Override + public void payAmountToB(long amount, MachineState state) { + // TODO Auto-generated method stub + + } + + @Override + public void messageAToB(MachineState state) { + // TODO Auto-generated method stub + + } + + @Override + public long addMinutesToTimestamp(Timestamp timestamp, long minutes, MachineState state) { + int blockHeight = timestamp.blockHeight; + + // At least one block in the future + blockHeight += (minutes / MINUTES_PER_BLOCK) + 1; + + return new Timestamp(blockHeight, 0).longValue(); + } + + @Override + public void onFinished(long finalBalance, MachineState state) { + // Refund remaining balance (if any) to AT's creator + Account creator = this.getCreator(); + long timestamp = this.getNextTransactionTimestamp(); + byte[] reference = this.getLastReference(); + BigDecimal amount = BigDecimal.valueOf(finalBalance, 8); + + ATTransactionData atTransactionData = new ATTransactionData(this.atData.getATAddress(), creator.getAddress(), amount, Asset.QORA, null, + BigDecimal.ZERO.setScale(8), timestamp, reference); + ATTransaction atTransaction = new ATTransaction(this.repository, atTransactionData); + + // Add to our transactions + this.transactions.add(atTransaction); + } + + @Override + public void onFatalError(MachineState state, ExecutionException e) { + state.getLogger().error("AT " + this.atData.getATAddress() + " suffered fatal error: " + e.getMessage()); + } + + @Override + public void platformSpecificPreExecuteCheck(short functionCodeValue, int paramCount, boolean returnValueExpected) throws IllegalFunctionCodeException { + // Currently not in use + throw new IllegalFunctionCodeException("AT API platform-specific functions currently not in use"); + } + + @Override + public void platformSpecificPostCheckExecute(short functionCodeValue, FunctionData functionData, MachineState state) throws ExecutionException { + // Currently not in use + throw new ExecutionException("AT API platform-specific functions currently not in use"); + } + + // Utility methods + + /** Convert part of little-endian byte[] to long */ + private static long fromBytes(byte[] bytes, int start) { + return (bytes[start] & 0xffL) | (bytes[start + 1] & 0xffL) << 8 | (bytes[start + 2] & 0xffL) << 16 | (bytes[start + 3] & 0xffL) << 24 + | (bytes[start + 4] & 0xffL) << 32 | (bytes[start + 5] & 0xffL) << 40 | (bytes[start + 6] & 0xffL) << 48 | (bytes[start + 7] & 0xffL) << 56; + } + + /** Returns SHA2-192 digest of input - used to verify transaction signatures */ + private static byte[] sha192(byte[] input) { + try { + // SHA2-192 + MessageDigest sha192 = MessageDigest.getInstance("SHA-192"); + return sha192.digest(input); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("SHA-192 not available"); + } + } + + /** Verify transaction's SHA2-192 hashed signature matches A2 thru A4 */ + private static void verifyTransaction(TransactionData transactionData, MachineState state) { + // Compare SHA2-192 of transaction's signature against A2 thru A4 + byte[] hash = sha192(transactionData.getSignature()); + + if (state.getA2() != fromBytes(hash, 0) || state.getA3() != fromBytes(hash, 8) || state.getA4() != fromBytes(hash, 16)) + throw new IllegalStateException("Transaction signature in A no longer matches signature from repository"); + } + + /** Returns transaction data from repository using block height & sequence from A1, checking the transaction signatures match too */ + private TransactionData fetchTransaction(MachineState state) { + Timestamp timestamp = new Timestamp(state.getA1()); + + try { + TransactionData transactionData = this.repository.getTransactionRepository().fromHeightAndSequence(timestamp.blockHeight, + timestamp.transactionSequence); + + if (transactionData == null) + throw new RuntimeException("AT API unable to fetch transaction?"); + + // Check transaction referenced still matches the one from the repository + verifyTransaction(transactionData, state); + + return transactionData; + } catch (DataException e) { + throw new RuntimeException("AT API unable to fetch transaction type?", e); + } + } + + /** Returns AT's creator's account */ + private PublicKeyAccount getCreator() { + return new PublicKeyAccount(this.repository, this.atData.getCreatorPublicKey()); + } + + /** Returns the timestamp to use for next AT Transaction */ + private long getNextTransactionTimestamp() { + /* + * Timestamp is block's timestamp + position in AT-Transactions list. + * + * We need increasing timestamps to preserve transaction order and hence a correct signature-reference chain when the block is processed. + * + * As Qora blocks must share the same milliseconds component in their timestamps, this allows us to generate up to 1,000 AT-Transactions per AT without + * issue. + * + * As long as ATs are not allowed to generate that many per block, e.g. by limiting maximum steps per execution round, then we should be fine. + */ + + return this.blockTimestamp + this.transactions.size(); + } + + /** Returns AT account's lastReference, taking newly generated ATTransactions into account */ + private byte[] getLastReference() { + // Use signature from last AT Transaction we generated + if (!this.transactions.isEmpty()) + return this.transactions.get(this.transactions.size() - 1).getTransactionData().getSignature(); + + // No transactions yet, so look up AT's account's last reference from repository + Account atAccount = new Account(this.repository, this.atData.getATAddress()); + + try { + return atAccount.getLastReference(); + } catch (DataException e) { + throw new RuntimeException("AT API unable to fetch AT's last reference from repository?", e); + } + } + +} diff --git a/src/qora/at/QoraATLogger.java b/src/qora/at/QoraATLogger.java new file mode 100644 index 00000000..002450fa --- /dev/null +++ b/src/qora/at/QoraATLogger.java @@ -0,0 +1,28 @@ +package qora.at; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import qora.at.AT; + +public class QoraATLogger implements org.ciyam.at.LoggerInterface { + + // NOTE: We're logging on behalf of qora.at.AT, not ourselves! + private static final Logger LOGGER = LogManager.getLogger(AT.class); + + @Override + public void error(String message) { + LOGGER.error(message); + } + + @Override + public void debug(String message) { + LOGGER.debug(message); + } + + @Override + public void echo(String message) { + LOGGER.info(message); + } + +} diff --git a/src/qora/block/Block.java b/src/qora/block/Block.java index 1f000e1b..3ca9e702 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.ATData; import data.at.ATStateData; import data.block.BlockData; import data.block.BlockTransactionData; @@ -23,7 +24,9 @@ import qora.account.Account; import qora.account.PrivateKeyAccount; import qora.account.PublicKeyAccount; import qora.assets.Asset; +import qora.at.AT; import qora.crypto.Crypto; +import qora.transaction.ATTransaction; import qora.transaction.GenesisTransaction; import qora.transaction.Transaction; import repository.ATRepository; @@ -100,15 +103,23 @@ public class Block { // Other properties private static final Logger LOGGER = LogManager.getLogger(Block.class); + + /** Sorted list of transactions attached to this block */ protected List transactions; + /** Remote/imported/loaded AT states */ protected List atStates; - protected List ourAtStates; // Generated locally + /** Locally-generated AT states */ + protected List ourAtStates; + /** Locally-generated AT fees */ protected BigDecimal ourAtFees; // Generated locally + /** Cached copy of next block's generating balance */ protected BigDecimal cachedNextGeneratingBalance; // Other useful constants + + /** Maximum size of block in bytes */ public static final int MAX_BLOCK_BYTES = 1048576; // Constructors @@ -174,30 +185,6 @@ public class Block { * @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; - - 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) throws DataException { this.repository = repository; this.generator = generator; @@ -218,17 +205,20 @@ 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.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 = new ArrayList(); + this.atStates = this.ourAtStates; } // Getters/setters @@ -258,7 +248,7 @@ public class Block { /** * Return the next block's version. * - * @return 1, 2 or 3 + * @return 1, 2, 3 or 4 */ public int getNextBlockVersion() { if (this.blockData.getHeight() == null) @@ -367,6 +357,7 @@ public class Block { return target; } + /** Returns pseudo-random, but deterministic, integer for this block (and block's generator for v3+ blocks) */ private BigInteger calcBlockHash() { byte[] hashData; @@ -382,6 +373,7 @@ public class Block { return new BigInteger(1, hash); } + /** Returns pseudo-random, but deterministic, integer for next block (and next block's generator for v3+ blocks) */ private BigInteger calcNextBlockHash(int nextBlockVersion, byte[] preVersion3GeneratorSignature, PublicKeyAccount nextBlockGenerator) { byte[] hashData; @@ -397,6 +389,7 @@ public class Block { return new BigInteger(1, hash); } + /** Calculate next block's timestamp, given next block's version, generator signature and generator's private key */ private long calcNextBlockTimestamp(int nextBlockVersion, byte[] nextBlockGeneratorSignature, PrivateKeyAccount nextBlockGenerator) throws DataException { BigInteger hashValue = calcNextBlockHash(nextBlockVersion, nextBlockGeneratorSignature, nextBlockGenerator); BigInteger target = calcGeneratorsTarget(nextBlockGenerator); @@ -465,7 +458,7 @@ public class Block { 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()); + List atStateData = this.repository.getATRepository().getBlockATStatesAtHeight(this.blockData.getHeight()); // The number of AT states fetched from repository should correspond with Block's atCount if (atStateData.size() != this.blockData.getATCount()) @@ -531,6 +524,10 @@ public class Block { if (this.blockData.getGeneratorSignature() == null) throw new IllegalStateException("Cannot calculate transactions signature as block has no generator signature"); + // Already added? + if (this.transactions.contains(transactionData)) + return true; + // Check there is space in block try { if (BlockTransformer.getDataLength(this) + TransactionTransformer.getDataLength(transactionData) > MAX_BLOCK_BYTES) @@ -542,12 +539,16 @@ public class Block { // Add to block this.transactions.add(Transaction.fromData(this.repository, transactionData)); + // Re-sort + this.transactions.sort(Transaction.getComparator()); + // Update transaction count this.blockData.setTransactionCount(this.blockData.getTransactionCount() + 1); // Update totalFees this.blockData.setTotalFees(this.blockData.getTotalFees().add(transactionData.getFee())); + // We've added a transaction, so recalculate transactions signature calcTransactionsSignature(); return true; @@ -601,6 +602,14 @@ public class Block { } } + /** + * Recalculate block's generator and transactions signatures, thus giving block full signature. + *

+ * Note: Block instance must have been constructed with a PrivateKeyAccount generator or this call will throw an IllegalStateException. + * + * @throws IllegalStateException + * if block's {@code generator} is not a {@code PrivateKeyAccount}. + */ public void sign() { this.calcGeneratorSignature(); this.calcTransactionsSignature(); @@ -608,6 +617,11 @@ public class Block { this.blockData.setSignature(this.getSignature()); } + /** + * Returns whether this block's signatures are valid. + * + * @return true if both generator and transaction signatures are valid, false otherwise + */ public boolean isSignatureValid() { try { // Check generator's signature first @@ -658,7 +672,7 @@ public class Block { if (this.blockData.getTimestamp() - BlockChain.BLOCK_TIMESTAMP_MARGIN > NTP.getTime()) return ValidationResult.TIMESTAMP_IN_FUTURE; - // Legacy gen1 test: check timestamp ms is the same as parent timestamp ms? + // Legacy gen1 test: check timestamp milliseconds is the same as parent timestamp milliseconds? if (this.blockData.getTimestamp() % 1000 != parentBlockData.getTimestamp() % 1000) return ValidationResult.TIMESTAMP_MS_INCORRECT; @@ -685,8 +699,8 @@ public class Block { if (hashValue.compareTo(target) >= 0) return ValidationResult.GENERATOR_NOT_ACCEPTED; - // XXX Odd gen1 test: "CHECK IF FIRST BLOCK OF USER" - // Is the comment wrong? Does each second elapsed allows generator to test a new "target" window against hashValue? + // Odd gen1 comment: "CHECK IF FIRST BLOCK OF USER" + // Each second elapsed allows generator to test a new "target" window against hashValue if (hashValue.compareTo(lowerTarget) < 0) return ValidationResult.GENERATOR_NOT_ACCEPTED; @@ -694,8 +708,16 @@ public class Block { 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(); + // For old v1 CIYAM ATs we blindly accept them + if (this.blockData.getVersion() < 4) { + this.ourAtStates = this.atStates; + this.ourAtFees = this.blockData.getATFees(); + } else { + // Generate local AT states for comparison + this.executeATs(); + } + + // Check locally generated AT states against ones received from elsewhere if (this.ourAtStates.size() != this.blockData.getATCount()) return ValidationResult.AT_STATES_MISMATCH; @@ -772,40 +794,66 @@ public class Block { * Execute CIYAM ATs for this block. *

* This needs to be done locally for all blocks, regardless of origin.
- * This method is called by isValid. + * Typically called by isValid() or new block constructor. *

* 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 (local version) and this.ourAtFees (remote/imported/loaded version). *

- * Updates this.ourAtStates and this.ourAtFees. + * Note: this method does not store new AT state data into repository - that is handled by process(). + *

+ * This method is not needed if fetching an existing block from the repository as AT state data will be loaded from repository as well. * * @see #isValid() * * @throws DataException * */ - public void executeATs() throws DataException { + private 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; - } + // AT-Transactions generated by running ATs, to be prepended to block's transactions + List allATTransactions = new ArrayList(); + + this.ourAtStates = new ArrayList(); + this.ourAtFees = BigDecimal.ZERO.setScale(8); // Find all executable ATs, ordered by earliest creation date first + List executableATs = this.repository.getATRepository().getAllExecutableATs(); // Run each AT, appends AT-Transactions and corresponding AT states, to our lists + for (ATData atData : executableATs) { + AT at = new AT(this.repository, atData); + List atTransactions = at.run(this.blockData.getTimestamp()); - // Finally prepend our entire AT-Transactions/states to block's transactions/states, adjust fees, etc. + allATTransactions.addAll(atTransactions); - // Note: store locally-calculated AT states separately to this.atStates so we can compare them in isValid() + ATStateData atStateData = at.getATStateData(); + this.ourAtStates.add(atStateData); + + this.ourAtFees = this.ourAtFees.add(atStateData.getFees()); + } + + // Prepend our entire AT-Transactions/states to block's transactions + this.transactions.addAll(0, allATTransactions); + + // Re-sort + this.transactions.sort(Transaction.getComparator()); + + // Update transaction count + this.blockData.setTransactionCount(this.blockData.getTransactionCount() + 1); + + // We've added transactions, so recalculate transactions signature + calcTransactionsSignature(); } + /** + * Process block, and its transactions, adding them to the blockchain. + * + * @throws DataException + */ 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. @@ -846,9 +894,19 @@ public class Block { BlockTransactionData blockTransactionData = new BlockTransactionData(this.getSignature(), sequence, transaction.getTransactionData().getSignature()); this.repository.getBlockRepository().save(blockTransactionData); + + // No longer unconfirmed + this.repository.getTransactionRepository().confirmTransaction(transaction.getTransactionData().getSignature()); } } + /** + * Removes block from blockchain undoing transactions. + *

+ * Note: it is up to the caller to re-add any of the block's transactions back to the unconfirmed transactions pile. + * + * @throws DataException + */ public void orphan() throws DataException { // 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. diff --git a/src/qora/block/BlockGenerator.java b/src/qora/block/BlockGenerator.java new file mode 100644 index 00000000..cdd54d2a --- /dev/null +++ b/src/qora/block/BlockGenerator.java @@ -0,0 +1,121 @@ +package qora.block; + +import java.util.Arrays; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import data.block.BlockData; +import data.transaction.TransactionData; +import qora.account.PrivateKeyAccount; +import qora.block.Block.ValidationResult; +import repository.BlockRepository; +import repository.DataException; +import repository.Repository; +import repository.RepositoryManager; + +// Forging new blocks + +// How is the private key going to be supplied? + +public class BlockGenerator extends Thread { + + // Properties + private byte[] generatorPrivateKey; + private PrivateKeyAccount generator; + private Block previousBlock; + private Block newBlock; + private boolean running; + + // Other properties + private static final Logger LOGGER = LogManager.getLogger(BlockGenerator.class); + + // Constructors + + public BlockGenerator(byte[] generatorPrivateKey) { + this.generatorPrivateKey = generatorPrivateKey; + this.previousBlock = null; + this.newBlock = null; + this.running = true; + } + + // Main thread loop + @Override + public void run() { + try (final Repository repository = RepositoryManager.getRepository()) { + generator = new PrivateKeyAccount(repository, generatorPrivateKey); + + // Going to need this a lot... + BlockRepository blockRepository = repository.getBlockRepository(); + + while (running) { + // Check blockchain hasn't changed + BlockData lastBlockData = blockRepository.getLastBlock(); + if (previousBlock == null || !Arrays.equals(previousBlock.getSignature(), lastBlockData.getSignature())) { + previousBlock = new Block(repository, lastBlockData); + newBlock = null; + } + + // Do we need to build a potential new block? + if (newBlock == null) + newBlock = new Block(repository, previousBlock.getBlockData(), generator); + + // Is new block valid yet? (Before adding unconfirmed transactions) + if (newBlock.isValid() == ValidationResult.OK) { + // Add unconfirmed transactions + addUnconfirmedTransactions(repository, newBlock); + + // Sign to create block's signature + newBlock.sign(); + + // If newBlock is still valid then we can use it + ValidationResult validationResult = newBlock.isValid(); + if (validationResult == ValidationResult.OK) { + // Add to blockchain - something else will notice and broadcast new block to network + try { + newBlock.process(); + LOGGER.info("Generated new block: " + newBlock.getBlockData().getHeight()); + repository.saveChanges(); + } catch (DataException e) { + // Unable to process block - report and discard + LOGGER.error("Unable to process newly generated block?", e); + newBlock = null; + } + } else { + // No longer valid? Report and discard + LOGGER.error("Valid, generated block now invalid '" + validationResult.name() + "' after adding unconfirmed transactions?"); + newBlock = null; + } + } + + // Sleep for a while + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + // We've been interrupted - time to exit + return; + } + } + } catch (DataException e) { + LOGGER.warn("Repository issue while running block generator", e); + } + } + + private void addUnconfirmedTransactions(Repository repository, Block newBlock) throws DataException { + // Grab all unconfirmed transactions (already sorted) + List unconfirmedTransactions = repository.getTransactionRepository().getAllUnconfirmedTransactions(); + + // Attempt to add transactions until block is full, or we run out + for (TransactionData transactionData : unconfirmedTransactions) + if (!newBlock.addTransaction(transactionData)) + break; + } + + public void shutdown() { + this.running = false; + // Interrupt too, absorbed by HSQLDB but could be caught by Thread.sleep() + Thread.currentThread().interrupt(); + } + +} diff --git a/src/qora/transaction/ATTransaction.java b/src/qora/transaction/ATTransaction.java index 790ba578..2a12e2fd 100644 --- a/src/qora/transaction/ATTransaction.java +++ b/src/qora/transaction/ATTransaction.java @@ -5,6 +5,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import com.google.common.primitives.Bytes; + import data.assets.AssetData; import data.transaction.ATTransactionData; import data.transaction.TransactionData; @@ -13,6 +15,8 @@ import qora.assets.Asset; import qora.crypto.Crypto; import repository.DataException; import repository.Repository; +import transform.TransformationException; +import transform.transaction.ATTransactionTransformer; public class ATTransaction extends Transaction { @@ -28,6 +32,18 @@ public class ATTransaction extends Transaction { super(repository, transactionData); this.atTransactionData = (ATTransactionData) this.transactionData; + + // Check whether we need to generate the ATTransaction's pseudo-signature + if (this.atTransactionData.getSignature() == null) { + // Signature is SHA2-256 of serialized transaction data, duplicated to make standard signature size of 64 bytes. + try { + byte[] digest = Crypto.digest(ATTransactionTransformer.toBytes(transactionData)); + byte[] signature = Bytes.concat(digest, digest); + this.atTransactionData.setSignature(signature); + } catch (TransformationException e) { + throw new RuntimeException("Couldn't transform AT Transaction into bytes", e); + } + } } // More information @@ -92,6 +108,14 @@ public class ATTransaction extends Transaction { return ValidationResult.INVALID_DATA_LENGTH; BigDecimal amount = this.atTransactionData.getAmount(); + byte[] message = this.atTransactionData.getMessage(); + + // We can only have either message or amount + boolean amountIsZero = amount.compareTo(BigDecimal.ZERO.setScale(8)) == 0; + boolean messageIsEmpty = message.length == 0; + + if ((messageIsEmpty && amountIsZero) || (!messageIsEmpty && !amountIsZero)) + return ValidationResult.INVALID_AT_TRANSACTION; // If we have no payment then we're done if (amount == null) diff --git a/src/qora/transaction/ArbitraryTransaction.java b/src/qora/transaction/ArbitraryTransaction.java index ef82afaa..62e8a633 100644 --- a/src/qora/transaction/ArbitraryTransaction.java +++ b/src/qora/transaction/ArbitraryTransaction.java @@ -4,13 +4,18 @@ import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.math.BigDecimal; +import java.nio.file.DirectoryNotEmptyException; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + import data.PaymentData; import data.transaction.ArbitraryTransactionData; import data.transaction.TransactionData; @@ -31,6 +36,9 @@ public class ArbitraryTransaction extends Transaction { // Properties private ArbitraryTransactionData arbitraryTransactionData; + // Other properties + private static final Logger LOGGER = LogManager.getLogger(ArbitraryTransaction.class); + // Other useful constants public static final int MAX_DATA_SIZE = 4000; @@ -141,8 +149,11 @@ public class ArbitraryTransaction extends Transaction { // Now store actual data somewhere, e.g. /arbitrary///-.raw Account sender = this.getSender(); int blockHeight = this.repository.getBlockRepository().getBlockchainHeight(); - String dataPathname = Settings.getInstance().getUserpath() + "arbitrary" + File.separator + sender.getAddress() + File.separator + blockHeight - + File.separator + Base58.encode(arbitraryTransactionData.getSignature()) + "-" + arbitraryTransactionData.getService() + ".raw"; + + String senderPathname = Settings.getInstance().getUserpath() + "arbitrary" + File.separator + sender.getAddress(); + String blockPathname = senderPathname + File.separator + blockHeight; + String dataPathname = blockPathname + File.separator + Base58.encode(arbitraryTransactionData.getSignature()) + "-" + + arbitraryTransactionData.getService() + ".raw"; Path dataPath = Paths.get(dataPathname); @@ -150,16 +161,16 @@ public class ArbitraryTransaction extends Transaction { try { Files.createDirectories(dataPath.getParent()); } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + LOGGER.error("Unable to create arbitrary transaction directory", e); + throw new DataException("Unable to create arbitrary transaction directory", e); } // Output actual transaction data try (OutputStream dataOut = Files.newOutputStream(dataPath)) { dataOut.write(rawData); } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + LOGGER.error("Unable to store arbitrary transaction data", e); + throw new DataException("Unable to store arbitrary transaction data", e); } } @@ -176,15 +187,27 @@ public class ArbitraryTransaction extends Transaction { // Delete corresponding data file (if any - storing raw data is optional) Account sender = this.getSender(); int blockHeight = this.repository.getBlockRepository().getBlockchainHeight(); - String dataPathname = Settings.getInstance().getUserpath() + "arbitrary" + File.separator + sender.getAddress() + File.separator + blockHeight - + File.separator + Base58.encode(arbitraryTransactionData.getSignature()) + "-" + arbitraryTransactionData.getService() + ".raw"; - Path dataPath = Paths.get(dataPathname); + String senderPathname = Settings.getInstance().getUserpath() + "arbitrary" + File.separator + sender.getAddress(); + String blockPathname = senderPathname + File.separator + blockHeight; + String dataPathname = blockPathname + File.separator + Base58.encode(arbitraryTransactionData.getSignature()) + "-" + + arbitraryTransactionData.getService() + ".raw"; + try { - Files.deleteIfExists(dataPath); + // Delete the actual arbitrary data + Files.delete(Paths.get(dataPathname)); + + // If block-directory now empty, delete that too + Files.delete(Paths.get(blockPathname)); + + // If sender-directory now empty, delete that too + Files.delete(Paths.get(senderPathname)); + } catch (NoSuchFileException e) { + LOGGER.warn("Unable to remove old arbitrary transaction data at " + dataPathname); + } catch (DirectoryNotEmptyException e) { + // This happens when block-directory or sender-directory is not empty but is OK } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + LOGGER.warn("IOException when trying to remove old arbitrary transaction data", e); } // Delete this transaction itself diff --git a/src/qora/transaction/CreatePollTransaction.java b/src/qora/transaction/CreatePollTransaction.java index 6658c3ba..48ae3de1 100644 --- a/src/qora/transaction/CreatePollTransaction.java +++ b/src/qora/transaction/CreatePollTransaction.java @@ -80,7 +80,7 @@ public class CreatePollTransaction extends Transaction { @Override public ValidationResult isValid() throws DataException { // Are CreatePollTransactions even allowed at this point? - // XXX In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used? + // In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used if (this.createPollTransactionData.getTimestamp() < BlockChain.getVotingReleaseTimestamp()) return ValidationResult.NOT_YET_RELEASED; @@ -106,7 +106,7 @@ public class CreatePollTransaction extends Transaction { if (this.repository.getVotingRepository().pollExists(createPollTransactionData.getPollName())) return ValidationResult.POLL_ALREADY_EXISTS; - // XXX In gen1 we tested for votes but how can there be any if poll doesn't exist? + // In gen1 we tested for presence of existing votes but how could there be any if poll doesn't exist? // Check number of options List pollOptions = createPollTransactionData.getPollOptions(); diff --git a/src/qora/transaction/DeployATTransaction.java b/src/qora/transaction/DeployATTransaction.java index e80412bf..042f3c24 100644 --- a/src/qora/transaction/DeployATTransaction.java +++ b/src/qora/transaction/DeployATTransaction.java @@ -8,6 +8,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.ciyam.at.MachineState; + import com.google.common.base.Utf8; import data.transaction.DeployATTransactionData; @@ -173,6 +175,12 @@ public class DeployATTransaction extends Transaction { // Check creation bytes are valid (for v2+) if (this.getVersion() >= 2) { // Do actual validation + try { + new MachineState(deployATTransactionData.getCreationBytes()); + } catch (IllegalArgumentException e) { + // Not valid + return ValidationResult.INVALID_CREATION_BYTES; + } } else { // Skip validation for old, dead ATs } diff --git a/src/qora/transaction/IssueAssetTransaction.java b/src/qora/transaction/IssueAssetTransaction.java index d3954df6..e1b38e7d 100644 --- a/src/qora/transaction/IssueAssetTransaction.java +++ b/src/qora/transaction/IssueAssetTransaction.java @@ -82,7 +82,7 @@ public class IssueAssetTransaction extends Transaction { @Override public ValidationResult isValid() throws DataException { // Are IssueAssetTransactions even allowed at this point? - // XXX In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used? + // In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used if (this.issueAssetTransactionData.getTimestamp() < BlockChain.getAssetsReleaseTimestamp()) return ValidationResult.NOT_YET_RELEASED; @@ -119,7 +119,7 @@ public class IssueAssetTransaction extends Transaction { if (issuer.getConfirmedBalance(Asset.QORA).compareTo(issueAssetTransactionData.getFee()) < 0) return ValidationResult.NO_BALANCE; - // XXX: Surely we want to check the asset name isn't already taken? This check is not present in gen1. + // Check the asset name isn't already taken. This check is not present in gen1. if (issueAssetTransactionData.getTimestamp() >= BlockChain.getIssueAssetV2Timestamp()) if (this.repository.getAssetRepository().assetExists(issueAssetTransactionData.getAssetName())) return ValidationResult.ASSET_ALREADY_EXISTS; diff --git a/src/qora/transaction/Transaction.java b/src/qora/transaction/Transaction.java index f532e3bf..d93ad5c4 100644 --- a/src/qora/transaction/Transaction.java +++ b/src/qora/transaction/Transaction.java @@ -1,7 +1,9 @@ package qora.transaction; import java.math.BigDecimal; +import java.math.BigInteger; import java.math.MathContext; +import java.util.Comparator; import java.util.List; import java.util.Map; @@ -95,8 +97,10 @@ public abstract class Transaction { INVALID_ORDER_CREATOR(33), INVALID_PAYMENTS_COUNT(34), NEGATIVE_PRICE(35), + INVALID_CREATION_BYTES(36), INVALID_TAGS_LENGTH(37), INVALID_AT_TYPE_LENGTH(38), + INVALID_AT_TRANSACTION(39), ASSET_ALREADY_EXISTS(43), NOT_YET_RELEASED(1000); @@ -347,6 +351,7 @@ public abstract class Transaction { * @return BlockData, or null if transaction is not in a Block * @throws DataException */ + @Deprecated public BlockData getBlock() throws DataException { return this.repository.getTransactionRepository().getBlockDataFromSignature(this.transactionData.getSignature()); } @@ -430,4 +435,51 @@ public abstract class Transaction { */ public abstract void orphan() throws DataException; + // Comparison + + /** Returns comparator that sorts ATTransactions first, then by timestamp, then by signature */ + public static Comparator getComparator() { + class TransactionComparator implements Comparator { + + // Compare by type, timestamp, then signature + @Override + public int compare(Transaction t1, Transaction t2) { + TransactionData td1 = t1.getTransactionData(); + TransactionData td2 = t2.getTransactionData(); + + // AT transactions come before non-AT transactions + if (td1.getType() == TransactionType.AT && td2.getType() != TransactionType.AT) + return -1; + // Non-AT transactions come after AT transactions + if (td1.getType() != TransactionType.AT && td2.getType() == TransactionType.AT) + return 1; + + // Both transactions are either AT or non-AT so compare timestamps + int result = Long.compare(td1.getTimestamp(), td2.getTimestamp()); + + if (result == 0) + // Same timestamp so compare signatures + result = new BigInteger(td1.getSignature()).compareTo(new BigInteger(td2.getSignature())); + + return result; + } + + } + + return new TransactionComparator(); + } + + @Override + public int hashCode() { + return this.transactionData.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof TransactionData)) + return false; + + return this.transactionData.equals(other); + } + } diff --git a/src/qora/transaction/TransferAssetTransaction.java b/src/qora/transaction/TransferAssetTransaction.java index 5325c9f0..4bf0703f 100644 --- a/src/qora/transaction/TransferAssetTransaction.java +++ b/src/qora/transaction/TransferAssetTransaction.java @@ -85,7 +85,7 @@ public class TransferAssetTransaction extends Transaction { @Override public ValidationResult isValid() throws DataException { // Are TransferAssetTransactions even allowed at this point? - // XXX In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used? + // In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used if (this.transferAssetTransactionData.getTimestamp() < BlockChain.getAssetsReleaseTimestamp()) return ValidationResult.NOT_YET_RELEASED; diff --git a/src/qora/transaction/VoteOnPollTransaction.java b/src/qora/transaction/VoteOnPollTransaction.java index 83600fab..6d72ed35 100644 --- a/src/qora/transaction/VoteOnPollTransaction.java +++ b/src/qora/transaction/VoteOnPollTransaction.java @@ -72,7 +72,7 @@ public class VoteOnPollTransaction extends Transaction { @Override public ValidationResult isValid() throws DataException { // Are VoteOnPollTransactions even allowed at this point? - // XXX In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used? + // In gen1 this used NTP.getTime() but surely the transaction's timestamp should be used if (this.voteOnPollTransactionData.getTimestamp() < BlockChain.getVotingReleaseTimestamp()) return ValidationResult.NOT_YET_RELEASED; diff --git a/src/repository/ATRepository.java b/src/repository/ATRepository.java index 70e494ef..355baffb 100644 --- a/src/repository/ATRepository.java +++ b/src/repository/ATRepository.java @@ -9,18 +9,71 @@ public interface ATRepository { // CIYAM AutomatedTransactions + /** Returns ATData using AT's address or null if none found */ public ATData fromATAddress(String atAddress) throws DataException; + /** Returns list of executable ATs, empty if none found */ + public List getAllExecutableATs() throws DataException; + + /** Returns creation block height given AT's address or null if not found */ + public Integer getATCreationBlockHeight(String atAddress) throws DataException; + + /** Saves ATData into repository */ public void save(ATData atData) throws DataException; + /** Removes an AT from repository, including associated ATStateData */ public void delete(String atAddress) throws DataException; // AT States - public ATStateData getATState(String atAddress, int height) throws DataException; + /** + * Returns ATStateData for an AT at given height. + * + * @param atAddress + * - AT's address + * @param height + * - block height + * @return ATStateData for AT at given height or null if none found + */ + public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException; - public List getBlockATStatesFromHeight(int height) throws DataException; + /** + * Returns latest ATStateData for an AT. + *

+ * As ATs don't necessarily run every block, this will return the ATStateData with the greatest height. + * + * @param atAddress + * - AT's address + * @return ATStateData for AT with greatest height or null if none found + */ + public ATStateData getLatestATState(String atAddress) throws DataException; + /** + * Returns all ATStateData for a given block height. + *

+ * Unlike getATState, only returns ATStateData saved at the given height. + * + * @param height + * - block height + * @return list of ATStateData for given height, empty list if none found + * @throws DataException + */ + public List getBlockATStatesAtHeight(int height) throws DataException; + + /** + * Save ATStateData into repository. + *

+ * Note: Requires at least these ATStateData properties to be filled, or an IllegalArgumentException will be thrown: + *

+ *

    + *
  • creation
  • + *
  • stateHash
  • + *
  • height
  • + *
+ * + * @param atStateData + * @throws IllegalArgumentException + */ public void save(ATStateData atStateData) throws DataException; /** Delete AT's state data at this height */ diff --git a/src/repository/BlockRepository.java b/src/repository/BlockRepository.java index 6077cc3a..829fcf30 100644 --- a/src/repository/BlockRepository.java +++ b/src/repository/BlockRepository.java @@ -8,10 +8,31 @@ import data.transaction.TransactionData; public interface BlockRepository { + /** + * Returns BlockData from repository using block signature. + * + * @param signature + * @return block data, or null if not found in blockchain. + * @throws DataException + */ public BlockData fromSignature(byte[] signature) throws DataException; + /** + * Returns BlockData from repository using block reference. + * + * @param reference + * @return block data, or null if not found in blockchain. + * @throws DataException + */ public BlockData fromReference(byte[] reference) throws DataException; + /** + * Returns BlockData from repository using block height. + * + * @param height + * @return block data, or null if not found in blockchain. + * @throws DataException + */ public BlockData fromHeight(int height) throws DataException; /** @@ -38,14 +59,58 @@ public interface BlockRepository { */ public BlockData getLastBlock() throws DataException; + /** + * Returns block's transactions given block's signature. + *

+ * This is typically used by Block.getTransactions() which uses lazy-loading of transactions. + * + * @param signature + * @return list of transactions, or null if block not found in blockchain. + * @throws DataException + */ public List getTransactionsFromSignature(byte[] signature) throws DataException; + /** + * Saves block into repository. + * + * @param blockData + * @throws DataException + */ public void save(BlockData blockData) throws DataException; + /** + * Deletes block from repository. + * + * @param blockData + * @throws DataException + */ public void delete(BlockData blockData) throws DataException; + /** + * Saves a block-transaction mapping into the repository. + *

+ * This essentially links a transaction to a specific block.
+ * Transactions cannot be mapped to more than one block, so attempts will result in a DataException. + *

+ * Note: it is the responsibility of the caller to maintain contiguous "sequence" values + * for all transactions mapped to a block. + * + * @param blockTransactionData + * @throws DataException + */ public void save(BlockTransactionData blockTransactionData) throws DataException; + /** + * Deletes a block-transaction mapping from the repository. + *

+ * This essentially unlinks a transaction from a specific block. + *

+ * Note: it is the responsibility of the caller to maintain contiguous "sequence" values + * for all transactions mapped to a block. + * + * @param blockTransactionData + * @throws DataException + */ public void delete(BlockTransactionData blockTransactionData) throws DataException; } diff --git a/src/repository/TransactionRepository.java b/src/repository/TransactionRepository.java index b15a36b3..7afce46c 100644 --- a/src/repository/TransactionRepository.java +++ b/src/repository/TransactionRepository.java @@ -1,6 +1,9 @@ package repository; import data.transaction.TransactionData; + +import java.util.List; + import data.block.BlockData; public interface TransactionRepository { @@ -9,10 +12,29 @@ public interface TransactionRepository { public TransactionData fromReference(byte[] reference) throws DataException; + public TransactionData fromHeightAndSequence(int height, int sequence) throws DataException; + public int getHeightFromSignature(byte[] signature) throws DataException; + @Deprecated public BlockData getBlockDataFromSignature(byte[] signature) throws DataException; + /** + * Returns list of unconfirmed transactions in timestamp-else-signature order. + * + * @return list of transactions, or empty if none. + * @throws DataException + */ + public List getAllUnconfirmedTransactions() throws DataException; + + /** + * Remove transaction from unconfirmed transactions pile. + * + * @param signature + * @throws DataException + */ + public void confirmTransaction(byte[] signature) throws DataException; + public void save(TransactionData transactionData) throws DataException; public void delete(TransactionData transactionData) throws DataException; diff --git a/src/repository/hsqldb/HSQLDBATRepository.java b/src/repository/hsqldb/HSQLDBATRepository.java index a634fe98..fb7d7a50 100644 --- a/src/repository/hsqldb/HSQLDBATRepository.java +++ b/src/repository/hsqldb/HSQLDBATRepository.java @@ -31,7 +31,7 @@ public class HSQLDBATRepository implements ATRepository { if (resultSet == null) return null; - String creator = resultSet.getString(1); + byte[] creatorPublicKey = resultSet.getBytes(1); long creation = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); int version = resultSet.getInt(3); byte[] codeBytes = resultSet.getBytes(4); // Actually BLOB @@ -49,18 +49,74 @@ public class HSQLDBATRepository implements ATRepository { if (resultSet.wasNull()) frozenBalance = null; - return new ATData(atAddress, creator, creation, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, + return new ATData(atAddress, creatorPublicKey, creation, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance); } catch (SQLException e) { throw new DataException("Unable to fetch AT from repository", e); } } + @Override + public List getAllExecutableATs() throws DataException { + List executableATs = new ArrayList(); + + try (ResultSet resultSet = this.repository.checkedExecute( + "SELECT AT_address, creator, creation, version, code_bytes, is_sleeping, sleep_until_height, had_fatal_error, is_frozen, frozen_balance FROM ATs WHERE is_finished = false ORDER BY creation ASC")) { + if (resultSet == null) + return executableATs; + + boolean isFinished = false; + + do { + String atAddress = resultSet.getString(1); + byte[] creatorPublicKey = resultSet.getBytes(2); + long creation = resultSet.getTimestamp(3, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + int version = resultSet.getInt(4); + byte[] codeBytes = resultSet.getBytes(5); // Actually BLOB + boolean isSleeping = resultSet.getBoolean(6); + + Integer sleepUntilHeight = resultSet.getInt(7); + if (resultSet.wasNull()) + sleepUntilHeight = null; + + boolean hadFatalError = resultSet.getBoolean(8); + boolean isFrozen = resultSet.getBoolean(9); + + BigDecimal frozenBalance = resultSet.getBigDecimal(10); + if (resultSet.wasNull()) + frozenBalance = null; + + ATData atData = new ATData(atAddress, creatorPublicKey, creation, version, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, + frozenBalance); + + executableATs.add(atData); + } while (resultSet.next()); + + return executableATs; + } catch (SQLException e) { + throw new DataException("Unable to fetch executable ATs from repository", e); + } + } + + @Override + public Integer getATCreationBlockHeight(String atAddress) throws DataException { + try (ResultSet resultSet = this.repository.checkedExecute( + "SELECT height from DeployATTransactions JOIN BlockTransactions ON transaction_signature = signature JOIN Blocks ON Blocks.signature = block_signature WHERE AT_address = ?", + atAddress)) { + if (resultSet == null) + return null; + + return resultSet.getInt(1); + } catch (SQLException e) { + throw new DataException("Unable to fetch AT's creation block height from repository", e); + } + } + @Override public void save(ATData atData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("ATs"); - saveHelper.bind("AT_address", atData.getATAddress()).bind("creator", atData.getCreator()).bind("creation", new Timestamp(atData.getCreation())) + saveHelper.bind("AT_address", atData.getATAddress()).bind("creator", atData.getCreatorPublicKey()).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()); @@ -85,7 +141,7 @@ public class HSQLDBATRepository implements ATRepository { // AT State @Override - public ATStateData getATState(String atAddress, int height) throws DataException { + public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException { 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) @@ -103,7 +159,26 @@ public class HSQLDBATRepository implements ATRepository { } @Override - public List getBlockATStatesFromHeight(int height) throws DataException { + public ATStateData getLatestATState(String atAddress) throws DataException { + try (ResultSet resultSet = this.repository + .checkedExecute("SELECT height, creation, state_data, state_hash, fees FROM ATStates WHERE AT_address = ? ORDER BY height DESC", atAddress)) { + if (resultSet == null) + return null; + + int height = resultSet.getInt(1); + long creation = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + byte[] stateData = resultSet.getBytes(3); // Actually BLOB + byte[] stateHash = resultSet.getBytes(4); + BigDecimal fees = resultSet.getBigDecimal(5); + + return new ATStateData(atAddress, height, creation, stateData, stateHash, fees); + } catch (SQLException e) { + throw new DataException("Unable to fetch latest AT state from repository", e); + } + } + + @Override + public List getBlockATStatesAtHeight(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", diff --git a/src/repository/hsqldb/HSQLDBAccountRepository.java b/src/repository/hsqldb/HSQLDBAccountRepository.java index 5c716863..6ecc8147 100644 --- a/src/repository/hsqldb/HSQLDBAccountRepository.java +++ b/src/repository/hsqldb/HSQLDBAccountRepository.java @@ -72,7 +72,7 @@ public class HSQLDBAccountRepository implements AccountRepository { @Override public AccountBalanceData getBalance(String address, long assetId) throws DataException { - try (ResultSet resultSet = this.repository.checkedExecute("SELECT balance FROM AccountBalances WHERE account = ? and asset_id = ?", address, assetId)) { + try (ResultSet resultSet = this.repository.checkedExecute("SELECT balance FROM AccountBalances WHERE account = ? AND asset_id = ?", address, assetId)) { if (resultSet == null) return null; diff --git a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java index 8d4742cf..40afdce9 100644 --- a/src/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -105,10 +105,11 @@ public class HSQLDBDatabaseUpdates { case 1: // Blocks - stmt.execute("CREATE TABLE Blocks (signature BlockSignature PRIMARY KEY, version TINYINT NOT NULL, reference BlockSignature, " + stmt.execute("CREATE TABLE Blocks (signature BlockSignature, 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_count INTEGER NOT NULL, AT_fees QoraAmount NOT NULL)"); + + "generator QoraPublicKey NOT NULL, generator_signature Signature NOT NULL, AT_count INTEGER NOT NULL, AT_fees QoraAmount NOT NULL, " + + "PRIMARY KEY (signature))"); // For finding blocks by height. stmt.execute("CREATE INDEX BlockHeightIndex ON Blocks (height)"); // For finding blocks by the account that generated them. @@ -121,30 +122,32 @@ public class HSQLDBDatabaseUpdates { case 2: // Generic transactions (null reference, creator and milestone_block for genesis transactions) - stmt.execute("CREATE TABLE Transactions (signature Signature PRIMARY KEY, reference Signature, type TINYINT NOT NULL, " - + "creator QoraPublicKey, creation TIMESTAMP WITH TIME ZONE NOT NULL, fee QoraAmount NOT NULL, milestone_block BlockSignature)"); + stmt.execute("CREATE TABLE Transactions (signature Signature, reference Signature, type TINYINT NOT NULL, " + + "creator QoraPublicKey NOT NULL, creation TIMESTAMP WITH TIME ZONE NOT NULL, fee QoraAmount NOT NULL, milestone_block BlockSignature, " + + "PRIMARY KEY (signature))"); // For finding transactions by transaction type. stmt.execute("CREATE INDEX TransactionTypeIndex ON Transactions (type)"); - // For finding transactions using timestamp. + // For finding transactions using creation timestamp. stmt.execute("CREATE INDEX TransactionCreationIndex ON Transactions (creation)"); - // For when a user wants to lookup ALL transactions they have created, regardless of type. - stmt.execute("CREATE INDEX TransactionCreatorIndex ON Transactions (creator)"); + // For when a user wants to lookup ALL transactions they have created, with optional type. + stmt.execute("CREATE INDEX TransactionCreatorIndex ON Transactions (creator, type)"); // For finding transactions by reference, e.g. child transactions. stmt.execute("CREATE INDEX TransactionReferenceIndex ON Transactions (reference)"); // Use a separate table space as this table will be very large. stmt.execute("SET TABLE Transactions NEW SPACE"); - // Transaction-Block mapping ("signature" is unique as a transaction cannot be included in more than one block) - stmt.execute("CREATE TABLE BlockTransactions (block_signature BlockSignature, sequence INTEGER, transaction_signature Signature, " + // Transaction-Block mapping ("transaction_signature" is unique as a transaction cannot be included in more than one block) + stmt.execute("CREATE TABLE BlockTransactions (block_signature BlockSignature, sequence INTEGER, transaction_signature Signature UNIQUE, " + "PRIMARY KEY (block_signature, sequence), FOREIGN KEY (transaction_signature) REFERENCES Transactions (signature) ON DELETE CASCADE, " + "FOREIGN KEY (block_signature) REFERENCES Blocks (signature) ON DELETE CASCADE)"); // Use a separate table space as this table will be very large. stmt.execute("SET TABLE BlockTransactions NEW SPACE"); // Unconfirmed transactions - // XXX Do we need this? If a transaction doesn't have a corresponding BlockTransactions record then it's unconfirmed? - stmt.execute("CREATE TABLE UnconfirmedTransactions (signature Signature PRIMARY KEY, expiry TIMESTAMP WITH TIME ZONE NOT NULL)"); - stmt.execute("CREATE INDEX UnconfirmedTransactionExpiryIndex ON UnconfirmedTransactions (expiry)"); + // We use this as searching for transactions with no corresponding mapping in BlockTransactions is much slower. + stmt.execute("CREATE TABLE UnconfirmedTransactions (signature Signature PRIMARY KEY, creation TIMESTAMP WITH TIME ZONE NOT NULL)"); + // Index to allow quick sorting by creation-else-signature + stmt.execute("CREATE INDEX UnconfirmedTransactionsIndex ON UnconfirmedTransactions (creation, signature)"); // Transaction recipients stmt.execute("CREATE TABLE TransactionRecipients (signature Signature, recipient QoraAddress NOT NULL, " @@ -276,6 +279,8 @@ public class HSQLDBDatabaseUpdates { + "description VARCHAR(2000) NOT NULL, AT_type ATType NOT NULL, AT_tags VARCHAR(200) NOT NULL, " + "creation_bytes VARBINARY(100000) NOT NULL, amount QoraAmount NOT NULL, AT_address QoraAddress, " + "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)"); + // For looking up the Deploy AT Transaction based on deployed AT address + stmt.execute("CREATE INDEX DeployATAddressIndex on DeployATTransactions (AT_address)"); break; case 20: @@ -353,22 +358,29 @@ public class HSQLDBDatabaseUpdates { case 27: // CIYAM Automated Transactions - 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))"); + stmt.execute( + "CREATE TABLE ATs (AT_address QoraAddress, creator QoraPublicKey, 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)"); + stmt.execute("CREATE INDEX ATIndex on ATs (is_finished, creation)"); + // For finding ATs by creator + stmt.execute("CREATE INDEX ATCreatorIndex on ATs (creator)"); + // AT state on a per-block basis 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)"); + stmt.execute("CREATE INDEX BlockATStateIndex on ATStates (height, creation)"); + // Generated AT Transactions stmt.execute( "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)"); + // For finding AT Transactions generated by a specific AT + stmt.execute("CREATE INDEX ATTransactionsIndex on ATTransactions (AT_address)"); break; default: diff --git a/src/repository/hsqldb/HSQLDBVotingRepository.java b/src/repository/hsqldb/HSQLDBVotingRepository.java index cedb1c62..232a63aa 100644 --- a/src/repository/hsqldb/HSQLDBVotingRepository.java +++ b/src/repository/hsqldb/HSQLDBVotingRepository.java @@ -35,7 +35,7 @@ public class HSQLDBVotingRepository implements VotingRepository { long published = resultSet.getTimestamp(4, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); try (ResultSet optionsResultSet = this.repository - .checkedExecute("SELECT option_name FROM PollOptions where poll_name = ? ORDER BY option_index ASC", pollName)) { + .checkedExecute("SELECT option_name FROM PollOptions WHERE poll_name = ? ORDER BY option_index ASC", pollName)) { if (optionsResultSet == null) return null; diff --git a/src/repository/hsqldb/transaction/HSQLDBCreatePollTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBCreatePollTransactionRepository.java index 08d8dc4e..ad6733f6 100644 --- a/src/repository/hsqldb/transaction/HSQLDBCreatePollTransactionRepository.java +++ b/src/repository/hsqldb/transaction/HSQLDBCreatePollTransactionRepository.java @@ -30,7 +30,7 @@ public class HSQLDBCreatePollTransactionRepository extends HSQLDBTransactionRepo String description = resultSet.getString(3); try (ResultSet optionsResultSet = this.repository - .checkedExecute("SELECT option_name FROM CreatePollTransactionOptions where signature = ? ORDER BY option_index ASC", signature)) { + .checkedExecute("SELECT option_name FROM CreatePollTransactionOptions WHERE signature = ? ORDER BY option_index ASC", signature)) { if (optionsResultSet == null) return null; diff --git a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index 8360274a..69e3f112 100644 --- a/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -102,6 +102,22 @@ public class HSQLDBTransactionRepository implements TransactionRepository { } } + @Override + public TransactionData fromHeightAndSequence(int height, int sequence) throws DataException { + try (ResultSet resultSet = this.repository.checkedExecute( + "SELECT transaction_signature FROM BlockTransactions JOIN Blocks ON signature = block_signature WHERE height = ? AND sequence = ?", height, + sequence)) { + if (resultSet == null) + return null; + + byte[] signature = resultSet.getBytes(1); + + return this.fromSignature(signature); + } catch (SQLException e) { + throw new DataException("Unable to fetch transaction from repository", e); + } + } + private TransactionData fromBase(TransactionType type, byte[] signature, byte[] reference, byte[] creatorPublicKey, long timestamp, BigDecimal fee) throws DataException { switch (type) { @@ -236,7 +252,7 @@ public class HSQLDBTransactionRepository implements TransactionRepository { return null; // Fetch block signature (if any) - try (ResultSet resultSet = this.repository.checkedExecute("SELECT block_signature from BlockTransactions WHERE transaction_signature = ? LIMIT 1", + try (ResultSet resultSet = this.repository.checkedExecute("SELECT block_signature FROM BlockTransactions WHERE transaction_signature = ? LIMIT 1", signature)) { if (resultSet == null) return null; @@ -249,6 +265,42 @@ public class HSQLDBTransactionRepository implements TransactionRepository { } } + @Override + public List getAllUnconfirmedTransactions() throws DataException { + List transactions = new ArrayList(); + + // Find transactions with no corresponding row in BlockTransactions + try (ResultSet resultSet = this.repository.checkedExecute("SELECT signature FROM UnconfirmedTransactions ORDER BY creation ASC, signature ASC")) { + if (resultSet == null) + return transactions; + + do { + byte[] signature = resultSet.getBytes(1); + + TransactionData transactionData = this.fromSignature(signature); + + if (transactionData == null) + // Something inconsistent with the repository + throw new DataException("Unable to fetch unconfirmed transaction from repository?"); + + transactions.add(transactionData); + } while (resultSet.next()); + + return transactions; + } catch (SQLException | DataException e) { + throw new DataException("Unable to fetch unconfirmed transactions from repository", e); + } + } + + @Override + public void confirmTransaction(byte[] signature) throws DataException { + try { + this.repository.delete("UnconfirmedTransactions", "signature = ?", signature); + } catch (SQLException e) { + throw new DataException("Unable to remove transaction from unconfirmed transactions repository", e); + } + } + @Override public void save(TransactionData transactionData) throws DataException { HSQLDBSaver saver = new HSQLDBSaver("Transactions"); diff --git a/src/test/SignatureTests.java b/src/test/SignatureTests.java index bc15ee14..83a05fc6 100644 --- a/src/test/SignatureTests.java +++ b/src/test/SignatureTests.java @@ -34,20 +34,30 @@ public class SignatureTests extends Common { @Test public void testBlockSignature() throws DataException { - int version = 3; - - byte[] reference = Base58.decode( - "BSfgEr6r1rXGGJCv8criR5NcBWfpHdJnm9x5unPwxvojEKCESv1wH1zJm7yvCeC48wshymYtARbHdUojbqWCCWW7h2UTc8g5oEx59C9M41dM7H48My8gVkcEZdxR1of3VgpE5UcowFp3kFC12hVcD9hUttJ2i2nZWMwprbFtUGyVv1U"); - - long timestamp = NTP.getTime() - 5000; - - BigDecimal generatingBalance = BigDecimal.valueOf(10_000_000L).setScale(8); - try (final Repository repository = RepositoryManager.getRepository()) { 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 }); - Block block = new Block(repository, version, reference, timestamp, generatingBalance, generator); + int version = 3; + + byte[] reference = Base58.decode( + "BSfgEr6r1rXGGJCv8criR5NcBWfpHdJnm9x5unPwxvojEKCESv1wH1zJm7yvCeC48wshymYtARbHdUojbqWCCWW7h2UTc8g5oEx59C9M41dM7H48My8gVkcEZdxR1of3VgpE5UcowFp3kFC12hVcD9hUttJ2i2nZWMwprbFtUGyVv1U"); + + int transactionCount = 0; + BigDecimal totalFees = BigDecimal.ZERO.setScale(8); + byte[] transactionsSignature = null; + int height = 0; + long timestamp = NTP.getTime() - 5000; + BigDecimal generatingBalance = BigDecimal.valueOf(10_000_000L).setScale(8); + byte[] generatorPublicKey = generator.getPublicKey(); + byte[] generatorSignature = null; + int atCount = 0; + BigDecimal atFees = BigDecimal.valueOf(10_000_000L).setScale(8); + + BlockData blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance, + generatorPublicKey, generatorSignature, atCount, atFees); + + Block block = new Block(repository, blockData, generator); block.sign(); assertTrue(block.isSignatureValid()); diff --git a/src/transform/block/BlockTransformer.java b/src/transform/block/BlockTransformer.java index 5bc5796e..f73471ac 100644 --- a/src/transform/block/BlockTransformer.java +++ b/src/transform/block/BlockTransformer.java @@ -310,7 +310,7 @@ public class BlockTransformer extends Transformer { Order order = orderTransaction.getOrder(); List trades = order.getTrades(); - // Filter out trades with initiatingOrderId that doesn't match this order + // Filter out trades with initiatingOrderId that don't match this order trades.removeIf((TradeData tradeData) -> !Arrays.equals(tradeData.getInitiator(), order.getOrderData().getOrderId())); // Any trades left? diff --git a/src/transform/transaction/ArbitraryTransactionTransformer.java b/src/transform/transaction/ArbitraryTransactionTransformer.java index 5d408a34..fac8f34e 100644 --- a/src/transform/transaction/ArbitraryTransactionTransformer.java +++ b/src/transform/transaction/ArbitraryTransactionTransformer.java @@ -48,12 +48,12 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer { byte[] senderPublicKey = Serialization.deserializePublicKey(byteBuffer); - // V3+ allows payments - List payments = null; + // V3+ allows payments but always return a list of payments, even if empty + List payments = new ArrayList(); + ; if (version != 1) { int paymentsCount = byteBuffer.getInt(); - payments = new ArrayList(); for (int i = 0; i < paymentsCount; ++i) payments.add(PaymentTransformer.fromByteBuffer(byteBuffer)); }