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:
parent
3c8a1713d5
commit
5526f9a7f0
Binary file not shown.
@ -1 +0,0 @@
|
||||
ab1560171ae5c6c15b0dfa8e6cccc7f8
|
@ -1 +0,0 @@
|
||||
c293c9656f43b432a08053f19ec5aa0de1cd10ea
|
@ -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>
|
||||
|
@ -1 +0,0 @@
|
||||
42f6e3eb3c6e510f65c963ce97583f05
|
@ -1 +0,0 @@
|
||||
490287647d3c69c05bd50ab565ffff86192ff423
|
@ -7,6 +7,6 @@
|
||||
<versions>
|
||||
<version>1.0</version>
|
||||
</versions>
|
||||
<lastUpdated>20181015085522</lastUpdated>
|
||||
<lastUpdated>20181101172102</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
||||
|
@ -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>
|
@ -1 +0,0 @@
|
||||
2369bf36c52580a89d5ea71a0f037a82
|
@ -1 +0,0 @@
|
||||
6bc38899b93ffce2286ae26f7af0b2d8b69db3cf
|
73
src/blockgenerator.java
Normal file
73
src/blockgenerator.java
Normal 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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -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() {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
634
src/migrate.java
634
src/migrate.java
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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
490
src/qora/at/QoraATAPI.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
28
src/qora/at/QoraATLogger.java
Normal file
28
src/qora/at/QoraATLogger.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@ -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.
|
||||
|
121
src/qora/block/BlockGenerator.java
Normal file
121
src/qora/block/BlockGenerator.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 */
|
||||
|
@ -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;
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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());
|
||||
|
@ -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?
|
||||
|
@ -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));
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user