3
0
mirror of https://github.com/Qortal/qortal.git synced 2025-02-11 17:55:50 +00:00

More work on integrating CIYAM AT v2

Now using ATv2 dated 20181101172102

ATData now uses byte[] creatorPublicKey instead of String creator.

TransactionData now has hashCode() and equals() methods,
which is needed for new Transaction Comparator,
used to sort transactions within a block,
AT-first, then timestamp, then signature.

AT-Transactions generate their own signatures using SHA2-256 of serialized data.

Arbitrary Transactions try to clean up their files when orphaned.

Deploy AT Transactions now check creation bytes (even for old v1 ATs).

Deprecated Transaction.getBlock() as it doesn't seem used
and would be better to simply have getHeight() rather than
a method that 'knows too much' about Blocks/BlockData.
Corresponding TransactionRepository.getBlockDataFromSignature()
also deprecated.

Loads more comments.

Tidied up some SQL: mainly correcting case,
moving PRIMARY KEY clauses to end of CREATE TABLE,
removing unnecessary columns from indexes.

Added "type" column to TransactionCreatorIndex so users can find
their transactions and optionally filter by type.

In BlockTransactions table, transaction_signature is now UNIQUE
as a transaction cannot be included in more than one block.

Various AT-related HSQLDB table and index changes.

ArbitraryTransactions transformer fixed to always return a list of payments,
even if empty. (Previously could return null which broke things).

Added simplistic block generator.

NOTE: unit tests broken due to pending upgrade to JUnit 5
This commit is contained in:
catbref 2018-11-02 10:30:51 +00:00
parent 3c8a1713d5
commit 5526f9a7f0
39 changed files with 1390 additions and 776 deletions

Binary file not shown.

View File

@ -1 +0,0 @@
ab1560171ae5c6c15b0dfa8e6cccc7f8

View File

@ -1 +0,0 @@
c293c9656f43b432a08053f19ec5aa0de1cd10ea

View File

@ -5,4 +5,5 @@
<groupId>org.ciyam</groupId>
<artifactId>at</artifactId>
<version>1.0</version>
<description>POM was created from install:install-file</description>
</project>

View File

@ -1 +0,0 @@
42f6e3eb3c6e510f65c963ce97583f05

View File

@ -1 +0,0 @@
490287647d3c69c05bd50ab565ffff86192ff423

View File

@ -7,6 +7,6 @@
<versions>
<version>1.0</version>
</versions>
<lastUpdated>20181015085522</lastUpdated>
<lastUpdated>20181101172102</lastUpdated>
</versioning>
</metadata>

View File

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

View File

@ -1 +0,0 @@
2369bf36c52580a89d5ea71a0f037a82

View File

@ -1 +0,0 @@
6bc38899b93ffce2286ae26f7af0b2d8b69db3cf

73
src/blockgenerator.java Normal file
View File

@ -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();
}
}
});
}
}

View File

@ -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() {

View File

@ -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);
}
}

View File

@ -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<String, byte[]> publicKeyByAddress = new HashMap<String, byte[]>();
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<String, byte[]> 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<String> recipients = new ArrayList<String>();
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();
}
}

View File

@ -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<ATTransaction> 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();
}
}

490
src/qora/at/QoraATAPI.java Normal file
View File

@ -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<ATTransaction> transactions;
// Constructors
public QoraATAPI(Repository repository, ATData atData, long blockTimestamp) {
this.repository = repository;
this.atData = atData;
this.transactions = new ArrayList<ATTransaction>();
this.blockTimestamp = blockTimestamp;
}
// Methods specific to Qora AT processing, not inherited
public List<ATTransaction> 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<Transaction> 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);
}
}
}

View File

@ -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);
}
}

View File

@ -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<Transaction> transactions;
/** Remote/imported/loaded AT states */
protected List<ATStateData> atStates;
protected List<ATStateData> ourAtStates; // Generated locally
/** Locally-generated AT states */
protected List<ATStateData> 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<Transaction>();
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<Transaction>();
this.atStates = new ArrayList<ATStateData>();
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> atStateData = this.repository.getATRepository().getBlockATStatesFromHeight(this.blockData.getHeight());
List<ATStateData> 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.
* <p>
* Note: Block instance must have been constructed with a <tt>PrivateKeyAccount generator</tt> or this call will throw an <tt>IllegalStateException</tt>.
*
* @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.
* <p>
* This needs to be done locally for all blocks, regardless of origin.<br>
* This method is called by <tt>isValid</tt>.
* Typically called by <tt>isValid()</tt> or new block constructor.
* <p>
* After calling, AT-generated transactions are prepended to the block's transactions and AT state data is generated.
* <p>
* This method is not needed if fetching an existing block from the repository.
* Updates <tt>this.ourAtStates</tt> (local version) and <tt>this.ourAtFees</tt> (remote/imported/loaded version).
* <p>
* Updates <tt>this.ourAtStates</tt> and <tt>this.ourAtFees</tt>.
* Note: this method does not store new AT state data into repository - that is handled by <tt>process()</tt>.
* <p>
* 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<ATTransaction> allATTransactions = new ArrayList<ATTransaction>();
this.ourAtStates = new ArrayList<ATStateData>();
this.ourAtFees = BigDecimal.ZERO.setScale(8);
// Find all executable ATs, ordered by earliest creation date first
List<ATData> 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<ATTransaction> 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.
* <p>
* 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.

View File

@ -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<TransactionData> 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();
}
}

View File

@ -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)

View File

@ -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. <userpath>/arbitrary/<sender address>/<block height>/<tx-sig>-<service>.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

View File

@ -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<PollOptionData> pollOptions = createPollTransactionData.getPollOptions();

View File

@ -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
}

View File

@ -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;

View File

@ -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<Transaction> getComparator() {
class TransactionComparator implements Comparator<Transaction> {
// 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);
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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<ATData> 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<ATStateData> getBlockATStatesFromHeight(int height) throws DataException;
/**
* Returns latest ATStateData for an AT.
* <p>
* As ATs don't necessarily run every block, this will return the <tt>ATStateData</tt> 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.
* <p>
* Unlike <tt>getATState</tt>, 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<ATStateData> getBlockATStatesAtHeight(int height) throws DataException;
/**
* Save ATStateData into repository.
* <p>
* Note: Requires at least these <tt>ATStateData</tt> properties to be filled, or an <tt>IllegalArgumentException</tt> will be thrown:
* <p>
* <ul>
* <li><tt>creation</tt></li>
* <li><tt>stateHash</tt></li>
* <li><tt>height</tt></li>
* </ul>
*
* @param atStateData
* @throws IllegalArgumentException
*/
public void save(ATStateData atStateData) throws DataException;
/** Delete AT's state data at this height */

View File

@ -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.
* <p>
* 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<TransactionData> 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.
* <p>
* This essentially links a transaction to a specific block.<br>
* Transactions cannot be mapped to more than one block, so attempts will result in a DataException.
* <p>
* 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.
* <p>
* This essentially unlinks a transaction from a specific block.
* <p>
* 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;
}

View File

@ -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<TransactionData> 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;

View File

@ -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<ATData> getAllExecutableATs() throws DataException {
List<ATData> executableATs = new ArrayList<ATData>();
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<ATStateData> 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<ATStateData> getBlockATStatesAtHeight(int height) throws DataException {
List<ATStateData> atStates = new ArrayList<ATStateData>();
try (ResultSet resultSet = this.repository.checkedExecute("SELECT AT_address, state_hash, fees FROM ATStates WHERE height = ? ORDER BY creation ASC",

View File

@ -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;

View File

@ -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:

View File

@ -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;

View File

@ -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;

View File

@ -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<TransactionData> getAllUnconfirmedTransactions() throws DataException {
List<TransactionData> transactions = new ArrayList<TransactionData>();
// 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");

View File

@ -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());

View File

@ -310,7 +310,7 @@ public class BlockTransformer extends Transformer {
Order order = orderTransaction.getOrder();
List<TradeData> 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?

View File

@ -48,12 +48,12 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer {
byte[] senderPublicKey = Serialization.deserializePublicKey(byteBuffer);
// V3+ allows payments
List<PaymentData> payments = null;
// V3+ allows payments but always return a list of payments, even if empty
List<PaymentData> payments = new ArrayList<PaymentData>();
;
if (version != 1) {
int paymentsCount = byteBuffer.getInt();
payments = new ArrayList<PaymentData>();
for (int i = 0; i < paymentsCount; ++i)
payments.add(PaymentTransformer.fromByteBuffer(byteBuffer));
}