mirror of
https://github.com/Qortal/qortal.git
synced 2025-02-11 17:55:50 +00:00
More work on transactions/blocks
Added Apache commons-net as maven dependency for NTP support. Added SAVEPOINT and ROLLBACK TO SAVEPOINT support to DB class. Added exists() test to DB class. Add MessageTransactions, with V1/V3 code in one class instead of very similar code split across two classes. Update DB schema to add version. More fleshing out of Assets class. Fleshing out Block class with parse(), generating balance and signature-related methods. More javadoc. More tests.
This commit is contained in:
parent
4ce499c444
commit
948bc95644
5
pom.xml
5
pom.xml
@ -42,5 +42,10 @@
|
||||
<artifactId>log4j-api</artifactId>
|
||||
<version>2.11.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-net</groupId>
|
||||
<artifactId>commons-net</artifactId>
|
||||
<version>3.3</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@ -75,6 +75,14 @@ public class DB {
|
||||
c.prepareStatement("ROLLBACK").execute();
|
||||
}
|
||||
|
||||
public static void createSavepoint(Connection c, String savepointName) throws SQLException {
|
||||
c.prepareStatement("SAVEPOINT " + savepointName).execute();
|
||||
}
|
||||
|
||||
public static void rollbackToSavepoint(Connection c, String savepointName) throws SQLException {
|
||||
c.prepareStatement("ROLLBACK TO SAVEPOINT " + savepointName).execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shutdown database and close all connections in connection pool.
|
||||
* <p>
|
||||
@ -237,4 +245,33 @@ public class DB {
|
||||
return resultSet.getLong(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Efficiently query database for existing of matching row.
|
||||
* <p>
|
||||
* {@code whereClause} is SQL "WHERE" clause containing "?" placeholders suitable for use with PreparedStatements.
|
||||
* <p>
|
||||
* Example call:
|
||||
* <p>
|
||||
* {@code String manufacturer = "Lamborghini";}<br>
|
||||
* {@code int maxMileage = 100_000;}<br>
|
||||
* {@code boolean isAvailable = DB.exists("Cars", "manufacturer = ? AND mileage <= ?", manufacturer, maxMileage);}
|
||||
*
|
||||
* @param tableName
|
||||
* @param whereClause
|
||||
* @param objects
|
||||
* @return true if matching row found in database, false otherwise
|
||||
* @throws SQLException
|
||||
*/
|
||||
public static boolean exists(String tableName, String whereClause, Object... objects) throws SQLException {
|
||||
try (final Connection connection = DB.getConnection()) {
|
||||
PreparedStatement preparedStatement = connection
|
||||
.prepareStatement("SELECT TRUE FROM " + tableName + " WHERE " + whereClause + " ORDER BY NULL LIMIT 1");
|
||||
ResultSet resultSet = DB.checkedExecute(preparedStatement);
|
||||
if (resultSet == null)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -264,15 +264,16 @@ public class DatabaseUpdates {
|
||||
|
||||
case 20:
|
||||
// Message Transactions
|
||||
stmt.execute("CREATE TABLE MessageTransactions (signature Signature, sender QoraPublicKey NOT NULL, recipient QoraAddress NOT NULL, "
|
||||
+ "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, amount QoraAmount NOT NULL, asset_id AssetID NOT NULL, data VARBINARY(4000) NOT NULL, "
|
||||
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
|
||||
stmt.execute(
|
||||
"CREATE TABLE MessageTransactions (signature Signature, version TINYINT NOT NULL, sender QoraPublicKey NOT NULL, recipient QoraAddress NOT NULL, "
|
||||
+ "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, amount QoraAmount NOT NULL, asset_id AssetID NOT NULL, data VARBINARY(4000) NOT NULL, "
|
||||
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
|
||||
break;
|
||||
|
||||
case 21:
|
||||
// Assets (including QORA coin itself)
|
||||
stmt.execute(
|
||||
"CREATE TABLE Assets (asset_id AssetID IDENTITY, owner QoraAddress NOT NULL, asset_name AssetName NOT NULL, description VARCHAR(4000) NOT NULL, "
|
||||
"CREATE TABLE Assets (asset_id AssetID IDENTITY, owner QoraPublicKey NOT NULL, asset_name AssetName NOT NULL, description VARCHAR(4000) NOT NULL, "
|
||||
+ "quantity BIGINT NOT NULL, is_divisible BOOLEAN NOT NULL, reference Signature NOT NULL)");
|
||||
break;
|
||||
|
||||
|
@ -22,7 +22,7 @@ public class Account {
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return address;
|
||||
return this.address;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -9,6 +9,12 @@ public class PrivateKeyAccount extends PublicKeyAccount {
|
||||
private byte[] seed;
|
||||
private Pair<byte[], byte[]> keyPair;
|
||||
|
||||
/**
|
||||
* Create PrivateKeyAccount using byte[32] seed.
|
||||
*
|
||||
* @param seed
|
||||
* byte[32] used to create private/public key pair
|
||||
*/
|
||||
public PrivateKeyAccount(byte[] seed) {
|
||||
this.seed = seed;
|
||||
this.keyPair = Ed25519.createKeyPair(seed);
|
||||
|
@ -1,11 +1,14 @@
|
||||
package qora.assets;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import database.DB;
|
||||
import database.NoDataFoundException;
|
||||
import database.SaveHelper;
|
||||
import qora.account.PublicKeyAccount;
|
||||
import qora.transaction.Transaction;
|
||||
|
||||
/*
|
||||
* TODO:
|
||||
@ -28,6 +31,9 @@ public class Asset {
|
||||
private boolean isDivisible;
|
||||
private byte[] reference;
|
||||
|
||||
// Property lengths
|
||||
private static final int OWNER_LENGTH = Transaction.CREATOR_LENGTH;
|
||||
|
||||
// NOTE: key is Long because it can be null if asset ID/key not yet assigned (which is done by save() method).
|
||||
public Asset(Long assetId, PublicKeyAccount owner, String name, String description, long quantity, boolean isDivisible, byte[] reference) {
|
||||
this.assetId = assetId;
|
||||
@ -44,7 +50,31 @@ public class Asset {
|
||||
this(null, owner, name, description, quantity, isDivisible, reference);
|
||||
}
|
||||
|
||||
// Load/Save
|
||||
// Load/Save/Delete/Exists
|
||||
|
||||
protected Asset(long assetId) throws SQLException {
|
||||
this(DB.checkedExecute("SELECT owner, asset_name, description, quantity, is_divisible, reference FROM Assets WHERE asset_id = ?", assetId));
|
||||
}
|
||||
|
||||
protected Asset(ResultSet rs) throws SQLException {
|
||||
if (rs == null)
|
||||
throw new NoDataFoundException();
|
||||
|
||||
this.owner = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(1), OWNER_LENGTH));
|
||||
this.name = rs.getString(2);
|
||||
this.description = rs.getString(3);
|
||||
this.quantity = rs.getLong(4);
|
||||
this.isDivisible = rs.getBoolean(5);
|
||||
this.reference = DB.getResultSetBytes(rs.getBinaryStream(6), Transaction.REFERENCE_LENGTH);
|
||||
}
|
||||
|
||||
public static Asset fromAssetId(long assetId) throws SQLException {
|
||||
try {
|
||||
return new Asset(assetId);
|
||||
} catch (NoDataFoundException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void save(Connection connection) throws SQLException {
|
||||
SaveHelper saveHelper = new SaveHelper(connection, "Assets");
|
||||
@ -55,4 +85,9 @@ public class Asset {
|
||||
if (this.assetId == null)
|
||||
this.assetId = DB.callIdentity(connection);
|
||||
}
|
||||
|
||||
public static boolean exists(long assetId) throws SQLException {
|
||||
return DB.exists("Assets", "asset_id = ?", assetId);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -49,31 +49,31 @@ public class Order implements Comparable<Order> {
|
||||
}
|
||||
|
||||
public Account getCreator() {
|
||||
return creator;
|
||||
return this.creator;
|
||||
}
|
||||
|
||||
public long getHaveAssetId() {
|
||||
return haveAssetId;
|
||||
return this.haveAssetId;
|
||||
}
|
||||
|
||||
public long getWantAssetId() {
|
||||
return wantAssetId;
|
||||
return this.wantAssetId;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
return this.amount;
|
||||
}
|
||||
|
||||
public BigDecimal getPrice() {
|
||||
return price;
|
||||
return this.price;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public BigDecimal getFulfilled() {
|
||||
return fulfilled;
|
||||
return this.fulfilled;
|
||||
}
|
||||
|
||||
public void setFulfilled(BigDecimal fulfilled) {
|
||||
|
@ -25,23 +25,23 @@ public class Trade {
|
||||
// Getters/setters
|
||||
|
||||
public BigInteger getInitiator() {
|
||||
return initiator;
|
||||
return this.initiator;
|
||||
}
|
||||
|
||||
public BigInteger getTarget() {
|
||||
return target;
|
||||
return this.target;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
return this.amount;
|
||||
}
|
||||
|
||||
public BigDecimal getPrice() {
|
||||
return price;
|
||||
return this.price;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package qora.block;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
@ -29,10 +30,13 @@ import qora.assets.Asset;
|
||||
import qora.assets.Order;
|
||||
import qora.assets.Trade;
|
||||
import qora.transaction.CreateOrderTransaction;
|
||||
import qora.transaction.GenesisTransaction;
|
||||
import qora.transaction.Transaction;
|
||||
import qora.transaction.TransactionFactory;
|
||||
import utils.Base58;
|
||||
import utils.NTP;
|
||||
import utils.ParseException;
|
||||
import utils.Serialization;
|
||||
|
||||
/*
|
||||
* Typical use-case scenarios:
|
||||
@ -58,12 +62,11 @@ import utils.ParseException;
|
||||
|
||||
public class Block {
|
||||
|
||||
// Validation results
|
||||
public static final int VALIDATE_OK = 1;
|
||||
|
||||
// Columns when fetching from database
|
||||
/**
|
||||
* Ordered list of columns when fetching a Block row from database.
|
||||
*/
|
||||
private static final String DB_COLUMNS = "version, reference, transaction_count, total_fees, "
|
||||
+ "transactions_signature, height, generation, generating_balance, generator, generator_signature, " + "AT_data, AT_fees";
|
||||
+ "transactions_signature, height, generation, generating_balance, generator, generator_signature, AT_data, AT_fees";
|
||||
|
||||
// Database properties
|
||||
protected int version;
|
||||
@ -81,6 +84,7 @@ public class Block {
|
||||
|
||||
// Other properties
|
||||
protected List<Transaction> transactions;
|
||||
protected BigDecimal cachedNextGeneratingBalance;
|
||||
|
||||
// Property lengths for serialisation
|
||||
protected static final int VERSION_LENGTH = 4;
|
||||
@ -90,7 +94,7 @@ public class Block {
|
||||
protected static final int TIMESTAMP_LENGTH = 8;
|
||||
protected static final int GENERATING_BALANCE_LENGTH = 8;
|
||||
protected static final int GENERATOR_LENGTH = 32;
|
||||
protected static final int TRANSACTION_COUNT_LENGTH = 8;
|
||||
protected static final int TRANSACTION_COUNT_LENGTH = 4;
|
||||
protected static final int BASE_LENGTH = VERSION_LENGTH + REFERENCE_LENGTH + TIMESTAMP_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH
|
||||
+ TRANSACTIONS_SIGNATURE_LENGTH + GENERATOR_SIGNATURE_LENGTH + TRANSACTION_COUNT_LENGTH;
|
||||
|
||||
@ -98,27 +102,40 @@ public class Block {
|
||||
protected static final int BLOCK_SIGNATURE_LENGTH = GENERATOR_SIGNATURE_LENGTH + TRANSACTIONS_SIGNATURE_LENGTH;
|
||||
public static final int MAX_BLOCK_BYTES = 1048576;
|
||||
protected static final int TRANSACTION_SIZE_LENGTH = 4; // per transaction
|
||||
public static final int MAX_TRANSACTION_BYTES = MAX_BLOCK_BYTES - BASE_LENGTH;
|
||||
protected static final int AT_BYTES_LENGTH = 4;
|
||||
protected static final int AT_FEES_LENGTH = 8;
|
||||
protected static final int AT_LENGTH = AT_FEES_LENGTH + AT_BYTES_LENGTH;
|
||||
|
||||
// Other useful constants
|
||||
/**
|
||||
* Number of blocks between recalculating block's generating balance.
|
||||
*/
|
||||
private static final int BLOCK_RETARGET_INTERVAL = 10;
|
||||
/**
|
||||
* Maximum acceptable timestamp disagreement offset in milliseconds.
|
||||
*/
|
||||
private static final long BLOCK_TIMESTAMP_MARGIN = 500L;
|
||||
|
||||
// Various release timestamps / block heights
|
||||
public static final int MESSAGE_RELEASE_HEIGHT = 99000;
|
||||
public static final int AT_BLOCK_HEIGHT_RELEASE = 99000;
|
||||
public static final long POWFIX_RELEASE_TIMESTAMP = 1456426800000L; // Block Version 3 // 2016-02-25T19:00:00+00:00
|
||||
|
||||
// Constructors
|
||||
|
||||
// For creating a new block from scratch or instantiating one that was previously serialized
|
||||
protected Block(int version, byte[] reference, long timestamp, BigDecimal generatingBalance, PublicKeyAccount generator, byte[] generatorSignature,
|
||||
byte[] transactionsSignature, byte[] atBytes, BigDecimal atFees) {
|
||||
// For creating a new block from scratch
|
||||
public Block(int version, byte[] reference, long timestamp, BigDecimal generatingBalance, PublicKeyAccount generator, byte[] atBytes, BigDecimal atFees) {
|
||||
this.version = version;
|
||||
this.reference = reference;
|
||||
this.timestamp = timestamp;
|
||||
this.generatingBalance = generatingBalance;
|
||||
this.generator = generator;
|
||||
this.generatorSignature = generatorSignature;
|
||||
this.generatorSignature = null;
|
||||
this.height = 0;
|
||||
|
||||
this.transactionCount = 0;
|
||||
this.transactions = new ArrayList<Transaction>();
|
||||
this.transactionsSignature = transactionsSignature;
|
||||
this.transactionsSignature = null;
|
||||
this.totalFees = BigDecimal.ZERO.setScale(8);
|
||||
|
||||
this.atBytes = atBytes;
|
||||
@ -127,6 +144,22 @@ public class Block {
|
||||
this.totalFees = this.totalFees.add(this.atFees);
|
||||
}
|
||||
|
||||
// For instantiating a block that was previously serialized
|
||||
protected Block(int version, byte[] reference, long timestamp, BigDecimal generatingBalance, PublicKeyAccount generator, byte[] generatorSignature,
|
||||
byte[] transactionsSignature, byte[] atBytes, BigDecimal atFees, List<Transaction> transactions) {
|
||||
this(version, reference, timestamp, generatingBalance, generator, atBytes, atFees);
|
||||
|
||||
this.generatorSignature = generatorSignature;
|
||||
|
||||
this.transactionsSignature = transactionsSignature;
|
||||
this.transactionCount = transactions.size();
|
||||
this.transactions = transactions;
|
||||
|
||||
// Add transactions' fees to totalFees
|
||||
for (Transaction transaction : this.transactions)
|
||||
this.totalFees = this.totalFees.add(transaction.getFee());
|
||||
}
|
||||
|
||||
// Getters/setters
|
||||
|
||||
public int getVersion() {
|
||||
@ -207,6 +240,77 @@ public class Block {
|
||||
return blockLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the next block's version.
|
||||
*
|
||||
* @return 1, 2 or 3
|
||||
*/
|
||||
public int getNextBlockVersion() {
|
||||
if (this.height < AT_BLOCK_HEIGHT_RELEASE)
|
||||
return 1;
|
||||
else if (this.timestamp < POWFIX_RELEASE_TIMESTAMP)
|
||||
return 2;
|
||||
else
|
||||
return 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the next block's generating balance.
|
||||
* <p>
|
||||
* Every BLOCK_RETARGET_INTERVAL the generating balance is recalculated.
|
||||
* <p>
|
||||
* If this block starts a new interval then the new generating balance is calculated, cached and returned.<br>
|
||||
* Within this interval, the generating balance stays the same so the current block's generating balance will be returned.
|
||||
*
|
||||
* @return next block's generating balance
|
||||
* @throws SQLException
|
||||
*/
|
||||
public BigDecimal getNextBlockGeneratingBalance() throws SQLException {
|
||||
// This block not at the start of an interval?
|
||||
if (this.height % BLOCK_RETARGET_INTERVAL != 0)
|
||||
return this.generatingBalance;
|
||||
|
||||
// Return cached calculation if we have one
|
||||
if (this.cachedNextGeneratingBalance != null)
|
||||
return this.cachedNextGeneratingBalance;
|
||||
|
||||
// Perform calculation
|
||||
|
||||
// Navigate back to first block in previous interval:
|
||||
// XXX: why can't we simply load using block height?
|
||||
Block firstBlock = this;
|
||||
for (int i = 1; firstBlock != null && i < BLOCK_RETARGET_INTERVAL; ++i)
|
||||
firstBlock = firstBlock.getParent();
|
||||
|
||||
// Couldn't navigate back far enough?
|
||||
if (firstBlock == null)
|
||||
throw new IllegalStateException("Failed to calculate next block's generating balance due to lack of historic blocks");
|
||||
|
||||
// Calculate the actual time period (in ms) over previous interval's blocks.
|
||||
long previousGeneratingTime = this.timestamp - firstBlock.getTimestamp();
|
||||
|
||||
// Calculate expected forging time (in ms) for a whole interval based on this block's generating balance.
|
||||
long expectedGeneratingTime = Block.calcForgingDelay(this.generatingBalance) * BLOCK_RETARGET_INTERVAL * 1000;
|
||||
|
||||
// Finally, scale generating balance such that faster than expected previous intervals produce larger generating balances.
|
||||
BigDecimal multiplier = BigDecimal.valueOf((double) expectedGeneratingTime / (double) previousGeneratingTime);
|
||||
this.cachedNextGeneratingBalance = BlockChain.minMaxBalance(this.generatingBalance.multiply(multiplier));
|
||||
|
||||
return this.cachedNextGeneratingBalance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return expected forging delay, in seconds, since previous block based on block's generating balance.
|
||||
*/
|
||||
public static long calcForgingDelay(BigDecimal generatingBalance) {
|
||||
generatingBalance = BlockChain.minMaxBalance(generatingBalance);
|
||||
|
||||
double percentageOfTotal = generatingBalance.divide(BlockChain.MAX_BALANCE).doubleValue();
|
||||
long actualBlockTime = (long) (BlockChain.MIN_BLOCK_TIME + ((BlockChain.MAX_BLOCK_TIME - BlockChain.MIN_BLOCK_TIME) * (1 - percentageOfTotal)));
|
||||
|
||||
return actualBlockTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return block's transactions.
|
||||
* <p>
|
||||
@ -235,6 +339,10 @@ public class Block {
|
||||
// No need to update totalFees as this will be loaded via the Blocks table
|
||||
} while (rs.next());
|
||||
|
||||
// The number of transactions fetched from database should correspond with Block's transactionCount
|
||||
if (this.transactions.size() != this.transactionCount)
|
||||
throw new IllegalStateException("Block's transactions from database do not match block's transaction count");
|
||||
|
||||
return this.transactions;
|
||||
}
|
||||
|
||||
@ -406,7 +514,7 @@ public class Block {
|
||||
return json;
|
||||
}
|
||||
|
||||
public byte[] toBytes() {
|
||||
public byte[] toBytes() throws SQLException {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(getDataLength());
|
||||
bytes.write(Ints.toByteArray(this.version));
|
||||
@ -430,6 +538,14 @@ public class Block {
|
||||
}
|
||||
}
|
||||
|
||||
// Transactions
|
||||
bytes.write(Ints.toByteArray(this.transactionCount));
|
||||
|
||||
for (Transaction transaction : this.getTransactions()) {
|
||||
bytes.write(Ints.toByteArray(transaction.getDataLength()));
|
||||
bytes.write(transaction.toBytes());
|
||||
}
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
@ -437,70 +553,281 @@ public class Block {
|
||||
}
|
||||
|
||||
public static Block parse(byte[] data) throws ParseException {
|
||||
// TODO
|
||||
return null;
|
||||
if (data == null)
|
||||
return null;
|
||||
|
||||
if (data.length < BASE_LENGTH)
|
||||
throw new ParseException("Byte data too short for Block");
|
||||
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(data);
|
||||
|
||||
int version = byteBuffer.getInt();
|
||||
|
||||
if (version >= 2 && data.length < BASE_LENGTH + AT_LENGTH)
|
||||
throw new ParseException("Byte data too short for V2+ Block");
|
||||
|
||||
long timestamp = byteBuffer.getLong();
|
||||
|
||||
byte[] reference = new byte[REFERENCE_LENGTH];
|
||||
byteBuffer.get(reference);
|
||||
|
||||
BigDecimal generatingBalance = BigDecimal.valueOf(byteBuffer.getLong()).setScale(8);
|
||||
PublicKeyAccount generator = Serialization.deserializePublicKey(byteBuffer);
|
||||
|
||||
byte[] transactionsSignature = new byte[TRANSACTIONS_SIGNATURE_LENGTH];
|
||||
byteBuffer.get(transactionsSignature);
|
||||
byte[] generatorSignature = new byte[GENERATOR_SIGNATURE_LENGTH];
|
||||
byteBuffer.get(generatorSignature);
|
||||
|
||||
byte[] atBytes = null;
|
||||
BigDecimal atFees = null;
|
||||
if (version >= 2) {
|
||||
int atBytesLength = byteBuffer.getInt();
|
||||
|
||||
if (atBytesLength > MAX_BLOCK_BYTES)
|
||||
throw new ParseException("Byte data too long for Block's AT info");
|
||||
|
||||
atBytes = new byte[atBytesLength];
|
||||
byteBuffer.get(atBytes);
|
||||
|
||||
atFees = BigDecimal.valueOf(byteBuffer.getLong()).setScale(8);
|
||||
}
|
||||
|
||||
int transactionCount = byteBuffer.getInt();
|
||||
|
||||
// Parse transactions now, compared to deferred parsing in Gen1, so we can throw ParseException if need be
|
||||
List<Transaction> transactions = new ArrayList<Transaction>();
|
||||
for (int t = 0; t < transactionCount; ++t) {
|
||||
if (byteBuffer.remaining() < TRANSACTION_SIZE_LENGTH)
|
||||
throw new ParseException("Byte data too short for Block Transaction length");
|
||||
|
||||
int transactionLength = byteBuffer.getInt();
|
||||
if (byteBuffer.remaining() < transactionLength)
|
||||
throw new ParseException("Byte data too short for Block Transaction");
|
||||
if (transactionLength > MAX_BLOCK_BYTES)
|
||||
throw new ParseException("Byte data too long for Block Transaction");
|
||||
|
||||
byte[] transactionBytes = new byte[transactionLength];
|
||||
byteBuffer.get(transactionBytes);
|
||||
|
||||
Transaction transaction = Transaction.parse(transactionBytes);
|
||||
transactions.add(transaction);
|
||||
}
|
||||
|
||||
if (byteBuffer.hasRemaining())
|
||||
throw new ParseException("Excess byte data found after parsing Block");
|
||||
|
||||
return new Block(version, reference, timestamp, generatingBalance, generator, generatorSignature, transactionsSignature, atBytes, atFees, transactions);
|
||||
}
|
||||
|
||||
// Processing
|
||||
|
||||
/**
|
||||
* Add a transaction to the block.
|
||||
* <p>
|
||||
* Used when constructing a new block during forging.
|
||||
* <p>
|
||||
* Requires block's {@code generator} being a {@code PrivateKeyAccount} so block's transactions signature can be recalculated.
|
||||
*
|
||||
* @param transaction
|
||||
* @return true if transaction successfully added to block, false otherwise
|
||||
* @throws IllegalStateException
|
||||
* if block's {@code generator} is not a {@code PrivateKeyAccount}.
|
||||
*/
|
||||
public boolean addTransaction(Transaction transaction) {
|
||||
// TODO
|
||||
// Can't add to transactions if we haven't loaded existing ones yet
|
||||
if (this.transactions == null)
|
||||
throw new IllegalStateException("Attempted to add transaction to partially loaded database Block");
|
||||
|
||||
if (!(this.generator instanceof PrivateKeyAccount))
|
||||
throw new IllegalStateException("Block's generator has no private key");
|
||||
|
||||
// Check there is space in block
|
||||
if (this.getDataLength() + transaction.getDataLength() > MAX_BLOCK_BYTES)
|
||||
return false;
|
||||
|
||||
// Add to block
|
||||
this.transactions.add(transaction);
|
||||
|
||||
// Update transaction count
|
||||
this.transactionCount++;
|
||||
|
||||
// Update totalFees
|
||||
this.totalFees.add(transaction.getFee());
|
||||
|
||||
// Update transactions signature
|
||||
return false; // no room
|
||||
calcTransactionsSignature();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public byte[] calcSignature(PrivateKeyAccount signer) {
|
||||
// TODO
|
||||
return null;
|
||||
/**
|
||||
* Recalculate block's generator signature.
|
||||
* <p>
|
||||
* Requires block's {@code generator} being a {@code PrivateKeyAccount}.
|
||||
*
|
||||
* @throws IllegalStateException
|
||||
* if block's {@code generator} is not a {@code PrivateKeyAccount}.
|
||||
*/
|
||||
public void calcGeneratorSignature() {
|
||||
if (!(this.generator instanceof PrivateKeyAccount))
|
||||
throw new IllegalStateException("Block's generator has no private key");
|
||||
|
||||
this.generatorSignature = ((PrivateKeyAccount) this.generator).sign(this.getBytesForGeneratorSignature());
|
||||
}
|
||||
|
||||
private byte[] getBytesForSignature() {
|
||||
private byte[] getBytesForGeneratorSignature() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(REFERENCE_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH);
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + GENERATING_BALANCE_LENGTH + GENERATOR_LENGTH);
|
||||
|
||||
// Only copy the generator signature from reference, which is the first 64 bytes.
|
||||
bytes.write(Arrays.copyOf(this.reference, GENERATOR_SIGNATURE_LENGTH));
|
||||
|
||||
bytes.write(Longs.toByteArray(this.generatingBalance.longValue()));
|
||||
|
||||
// We're padding here just in case the generator is the genesis account whose public key is only 8 bytes long.
|
||||
bytes.write(Bytes.ensureCapacity(this.generator.getPublicKey(), GENERATOR_LENGTH, 0));
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSignatureValid() {
|
||||
// Check generator's signature first
|
||||
if (!this.generator.verify(this.generatorSignature, getBytesForSignature()))
|
||||
return false;
|
||||
/**
|
||||
* Recalculate block's transactions signature.
|
||||
* <p>
|
||||
* Requires block's {@code generator} being a {@code PrivateKeyAccount}.
|
||||
*
|
||||
* @throws IllegalStateException
|
||||
* if block's {@code generator} is not a {@code PrivateKeyAccount}.
|
||||
*/
|
||||
public void calcTransactionsSignature() {
|
||||
if (!(this.generator instanceof PrivateKeyAccount))
|
||||
throw new IllegalStateException("Block's generator has no private key");
|
||||
|
||||
// Check transactions signature
|
||||
this.transactionsSignature = ((PrivateKeyAccount) this.generator).sign(this.getBytesForTransactionsSignature());
|
||||
}
|
||||
|
||||
private byte[] getBytesForTransactionsSignature() {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(GENERATOR_SIGNATURE_LENGTH + this.transactionCount * Transaction.SIGNATURE_LENGTH);
|
||||
|
||||
try {
|
||||
bytes.write(this.generatorSignature);
|
||||
|
||||
for (Transaction transaction : this.getTransactions()) {
|
||||
if (!transaction.isSignatureValid())
|
||||
return false;
|
||||
return null;
|
||||
|
||||
bytes.write(transaction.getSignature());
|
||||
}
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException | SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.generator.verify(this.transactionsSignature, bytes.toByteArray()))
|
||||
public boolean isSignatureValid() {
|
||||
// Check generator's signature first
|
||||
if (!this.generator.verify(this.generatorSignature, getBytesForGeneratorSignature()))
|
||||
return false;
|
||||
|
||||
// Check transactions signature
|
||||
if (!this.generator.verify(this.transactionsSignature, getBytesForTransactionsSignature()))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether Block is valid. Expected to be called within SQL Transaction.
|
||||
* <p>
|
||||
* Performs various tests like checking for parent block, correct block timestamp, version, generating balance, etc.<br>
|
||||
* Also checks block's transactions using an HSQLDB "SAVEPOINT" and hence needs to be called within an ongoing SQL Transaction.
|
||||
*
|
||||
* @param connection
|
||||
* @return true if block is valid, false otherwise.
|
||||
* @throws SQLException
|
||||
*/
|
||||
public boolean isValid(Connection connection) throws SQLException {
|
||||
// TODO
|
||||
return false;
|
||||
|
||||
// Check parent blocks exists
|
||||
if (this.reference == null)
|
||||
return false;
|
||||
|
||||
Block parentBlock = this.getParent();
|
||||
if (parentBlock == null)
|
||||
return false;
|
||||
|
||||
// Check timestamp is valid, i.e. later than parent timestamp and not in the future, within ~500ms margin
|
||||
if (this.timestamp < parentBlock.getTimestamp() || this.timestamp - BLOCK_TIMESTAMP_MARGIN > NTP.getTime())
|
||||
return false;
|
||||
|
||||
// Legacy gen1 test: check timestamp ms is the same as parent timestamp ms?
|
||||
if (this.timestamp % 1000 != parentBlock.getTimestamp() % 1000)
|
||||
return false;
|
||||
|
||||
// Check block version
|
||||
if (this.version != parentBlock.getNextBlockVersion())
|
||||
return false;
|
||||
if (this.version < 2 && (this.atBytes != null || this.atBytes.length > 0 || this.atFees != null || this.atFees.compareTo(BigDecimal.ZERO) > 0))
|
||||
return false;
|
||||
|
||||
// Check generating balance
|
||||
if (this.generatingBalance != parentBlock.getNextBlockGeneratingBalance())
|
||||
return false;
|
||||
|
||||
// Check generator's proof of stake against block's generating balance
|
||||
// TODO
|
||||
|
||||
// Check CIYAM AT
|
||||
if (this.atBytes != null && this.atBytes.length > 0) {
|
||||
// TODO
|
||||
// try {
|
||||
// AT_Block atBlock = AT_Controller.validateATs(this.getBlockATs(), BlockChain.getHeight() + 1);
|
||||
// this.atFees = atBlock.getTotalFees();
|
||||
// } catch (NoSuchAlgorithmException | AT_Exception e) {
|
||||
// return false;
|
||||
// }
|
||||
}
|
||||
|
||||
// Check transactions
|
||||
DB.createSavepoint(connection, "BLOCK_TRANSACTIONS");
|
||||
// XXX: we might need to catch SQLExceptions and not rollback which could cause a new exception?
|
||||
// OR: catch, attempt to rollback and then re-throw caught exception?
|
||||
// OR: don't catch, attempt to rollback, catch exception during rollback then return false?
|
||||
try {
|
||||
for (Transaction transaction : this.getTransactions()) {
|
||||
// GenesisTransactions are not allowed (GenesisBlock overrides isValid() to allow them)
|
||||
if (transaction instanceof GenesisTransaction)
|
||||
return false;
|
||||
|
||||
// Check timestamp and deadline
|
||||
if (transaction.getTimestamp() > this.timestamp || transaction.getDeadline() <= this.timestamp)
|
||||
return false;
|
||||
|
||||
// Check transaction is even valid
|
||||
// NOTE: in Gen1 there was an extra block height passed to DeployATTransaction.isValid
|
||||
if (transaction.isValid(connection) != Transaction.ValidationResult.OK)
|
||||
return false;
|
||||
|
||||
// Process transaction to make sure other transactions validate properly
|
||||
try {
|
||||
transaction.process(connection);
|
||||
} catch (Exception e) {
|
||||
// LOGGER.error("Exception during transaction processing, tx " + Base58.encode(transaction.getSignature()), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// Revert back to savepoint
|
||||
DB.rollbackToSavepoint(connection, "BLOCK_TRANSACTIONS");
|
||||
}
|
||||
|
||||
// Block is valid
|
||||
return true;
|
||||
}
|
||||
|
||||
public void process(Connection connection) throws SQLException {
|
||||
@ -519,6 +846,7 @@ public class Block {
|
||||
Block latestBlock = Block.fromHeight(blockchainHeight);
|
||||
if (latestBlock != null)
|
||||
this.reference = latestBlock.getSignature();
|
||||
|
||||
this.height = blockchainHeight + 1;
|
||||
this.save(connection);
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
package qora.block;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
@ -13,6 +14,23 @@ import qora.assets.Asset;
|
||||
*/
|
||||
public class BlockChain {
|
||||
|
||||
/**
|
||||
* Minimum Qora balance.
|
||||
*/
|
||||
public static final BigDecimal MIN_BALANCE = BigDecimal.valueOf(1L).setScale(8);
|
||||
/**
|
||||
* Maximum Qora balance.
|
||||
*/
|
||||
public static final BigDecimal MAX_BALANCE = BigDecimal.valueOf(10_000_000_000L).setScale(8);
|
||||
/**
|
||||
* Minimum target time between blocks, in seconds.
|
||||
*/
|
||||
public static final long MIN_BLOCK_TIME = 60;
|
||||
/**
|
||||
* Maximum target time between blocks, in seconds.
|
||||
*/
|
||||
public static final long MAX_BLOCK_TIME = 300;
|
||||
|
||||
/**
|
||||
* Some sort start-up/initialization/checking method.
|
||||
*
|
||||
@ -84,4 +102,17 @@ public class BlockChain {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return Qora balance adjusted to within min/max limits.
|
||||
*/
|
||||
public static BigDecimal minMaxBalance(BigDecimal balance) {
|
||||
if (balance.compareTo(MIN_BALANCE) < 0)
|
||||
return MIN_BALANCE;
|
||||
|
||||
if (balance.compareTo(MAX_BALANCE) > 0)
|
||||
return MAX_BALANCE;
|
||||
|
||||
return balance;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ import com.google.common.primitives.Bytes;
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
import qora.account.GenesisAccount;
|
||||
import qora.account.PrivateKeyAccount;
|
||||
import qora.crypto.Crypto;
|
||||
import qora.transaction.GenesisTransaction;
|
||||
import qora.transaction.Transaction;
|
||||
@ -33,10 +32,9 @@ public class GenesisBlock extends Block {
|
||||
// Constructors
|
||||
protected GenesisBlock() {
|
||||
super(GENESIS_BLOCK_VERSION, GENESIS_REFERENCE, GENESIS_TIMESTAMP, GENESIS_GENERATING_BALANCE, GENESIS_GENERATOR, GENESIS_GENERATOR_SIGNATURE,
|
||||
GENESIS_TRANSACTIONS_SIGNATURE, null, null);
|
||||
GENESIS_TRANSACTIONS_SIGNATURE, null, null, new ArrayList<Transaction>());
|
||||
|
||||
this.height = 1;
|
||||
this.transactions = new ArrayList<Transaction>();
|
||||
|
||||
// Genesis transactions
|
||||
addGenesisTransaction("QUD9y7NZqTtNwvSAUfewd7zKUGoVivVnTW", "7032468.191");
|
||||
@ -246,7 +244,7 @@ public class GenesisBlock extends Block {
|
||||
}
|
||||
|
||||
/**
|
||||
* Refuse to calculate genesis block signature!
|
||||
* Refuse to calculate genesis block's generator signature!
|
||||
* <p>
|
||||
* This is not possible as there is no private key for the genesis account and so no way to sign data.
|
||||
* <p>
|
||||
@ -255,8 +253,22 @@ public class GenesisBlock extends Block {
|
||||
* @throws IllegalStateException
|
||||
*/
|
||||
@Override
|
||||
public byte[] calcSignature(PrivateKeyAccount signer) {
|
||||
throw new IllegalStateException("There is no private key for genesis transactions");
|
||||
public void calcGeneratorSignature() {
|
||||
throw new IllegalStateException("There is no private key for genesis account");
|
||||
}
|
||||
|
||||
/**
|
||||
* Refuse to calculate genesis block's transactions signature!
|
||||
* <p>
|
||||
* This is not possible as there is no private key for the genesis account and so no way to sign data.
|
||||
* <p>
|
||||
* <b>Always throws IllegalStateException.</b>
|
||||
*
|
||||
* @throws IllegalStateException
|
||||
*/
|
||||
@Override
|
||||
public void calcTransactionsSignature() {
|
||||
throw new IllegalStateException("There is no private key for genesis account");
|
||||
}
|
||||
|
||||
/**
|
||||
|
374
src/qora/transaction/MessageTransaction.java
Normal file
374
src/qora/transaction/MessageTransaction.java
Normal file
@ -0,0 +1,374 @@
|
||||
package qora.transaction;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
import com.google.common.hash.HashCode;
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
import database.DB;
|
||||
import database.NoDataFoundException;
|
||||
import database.SaveHelper;
|
||||
import qora.account.Account;
|
||||
import qora.account.PublicKeyAccount;
|
||||
import qora.assets.Asset;
|
||||
import qora.block.Block;
|
||||
import qora.block.BlockChain;
|
||||
import qora.crypto.Crypto;
|
||||
import utils.Base58;
|
||||
import utils.ParseException;
|
||||
import utils.Serialization;
|
||||
|
||||
public class MessageTransaction extends Transaction {
|
||||
|
||||
// Properties
|
||||
protected int version;
|
||||
protected PublicKeyAccount sender;
|
||||
protected Account recipient;
|
||||
protected Long assetId;
|
||||
protected BigDecimal amount;
|
||||
protected byte[] data;
|
||||
protected boolean isText;
|
||||
protected boolean isEncrypted;
|
||||
|
||||
// Property lengths
|
||||
private static final int SENDER_LENGTH = 32;
|
||||
private static final int AMOUNT_LENGTH = 8;
|
||||
private static final int ASSET_ID_LENGTH = 8;
|
||||
private static final int DATA_SIZE_LENGTH = 4;
|
||||
private static final int IS_TEXT_LENGTH = 1;
|
||||
private static final int IS_ENCRYPTED_LENGTH = 1;
|
||||
private static final int TYPELESS_DATALESS_LENGTH_V1 = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + AMOUNT_LENGTH + DATA_SIZE_LENGTH
|
||||
+ IS_TEXT_LENGTH + IS_ENCRYPTED_LENGTH;
|
||||
private static final int TYPELESS_DATALESS_LENGTH_V3 = BASE_TYPELESS_LENGTH + SENDER_LENGTH + RECIPIENT_LENGTH + ASSET_ID_LENGTH + AMOUNT_LENGTH
|
||||
+ DATA_SIZE_LENGTH + IS_TEXT_LENGTH + IS_ENCRYPTED_LENGTH;
|
||||
|
||||
// Other property lengths
|
||||
private static final int MAX_DATA_SIZE = 4000;
|
||||
|
||||
// Constructors
|
||||
public MessageTransaction(PublicKeyAccount sender, String recipient, Long assetId, BigDecimal amount, BigDecimal fee, byte[] data, boolean isText,
|
||||
boolean isEncrypted, long timestamp, byte[] reference, byte[] signature) {
|
||||
super(TransactionType.MESSAGE, fee, sender, timestamp, reference, signature);
|
||||
|
||||
this.version = Transaction.getVersionByTimestamp(this.timestamp);
|
||||
this.sender = sender;
|
||||
this.recipient = new Account(recipient);
|
||||
|
||||
if (assetId != null)
|
||||
this.assetId = assetId;
|
||||
else
|
||||
this.assetId = Asset.QORA;
|
||||
|
||||
this.amount = amount;
|
||||
this.data = data;
|
||||
this.isText = isText;
|
||||
this.isEncrypted = isEncrypted;
|
||||
}
|
||||
|
||||
// Getters/Setters
|
||||
|
||||
public int getVersion() {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
public Account getSender() {
|
||||
return this.sender;
|
||||
}
|
||||
|
||||
public Account getRecipient() {
|
||||
return this.recipient;
|
||||
}
|
||||
|
||||
public Long getAssetId() {
|
||||
return this.assetId;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return this.amount;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
public boolean isText() {
|
||||
return this.isText;
|
||||
}
|
||||
|
||||
public boolean isEncrypted() {
|
||||
return this.isEncrypted;
|
||||
}
|
||||
|
||||
// More information
|
||||
|
||||
public int getDataLength() {
|
||||
if (this.version == 1)
|
||||
return TYPE_LENGTH + TYPELESS_DATALESS_LENGTH_V1 + this.data.length;
|
||||
else
|
||||
return TYPE_LENGTH + TYPELESS_DATALESS_LENGTH_V3 + this.data.length;
|
||||
}
|
||||
|
||||
// Load/Save
|
||||
|
||||
/**
|
||||
* Load MessageTransaction from DB using signature.
|
||||
*
|
||||
* @param signature
|
||||
* @throws NoDataFoundException
|
||||
* if no matching row found
|
||||
* @throws SQLException
|
||||
*/
|
||||
protected MessageTransaction(byte[] signature) throws SQLException {
|
||||
super(TransactionType.MESSAGE, signature);
|
||||
|
||||
ResultSet rs = DB.checkedExecute(
|
||||
"SELECT version, sender, recipient, is_text, is_encrypted, amount, asset_id, data FROM MessageTransactions WHERE signature = ?", signature);
|
||||
if (rs == null)
|
||||
throw new NoDataFoundException();
|
||||
|
||||
this.version = rs.getInt(1);
|
||||
this.sender = new PublicKeyAccount(DB.getResultSetBytes(rs.getBinaryStream(2), CREATOR_LENGTH));
|
||||
this.recipient = new Account(rs.getString(3));
|
||||
this.isText = rs.getBoolean(4);
|
||||
this.isEncrypted = rs.getBoolean(5);
|
||||
this.amount = rs.getBigDecimal(6).setScale(8);
|
||||
this.assetId = rs.getLong(7);
|
||||
this.data = DB.getResultSetBytes(rs.getBinaryStream(8));
|
||||
}
|
||||
|
||||
/**
|
||||
* Load MessageTransaction from DB using signature
|
||||
*
|
||||
* @param signature
|
||||
* @return MessageTransaction, or null if not found
|
||||
* @throws SQLException
|
||||
*/
|
||||
public static MessageTransaction fromSignature(byte[] signature) throws SQLException {
|
||||
try {
|
||||
return new MessageTransaction(signature);
|
||||
} catch (NoDataFoundException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(Connection connection) throws SQLException {
|
||||
super.save(connection);
|
||||
|
||||
SaveHelper saveHelper = new SaveHelper(connection, "MessageTransactions");
|
||||
saveHelper.bind("signature", this.signature).bind("version", this.version).bind("sender", this.sender.getPublicKey())
|
||||
.bind("recipient", this.recipient.getAddress()).bind("is_text", this.isText).bind("is_encrypted", this.isEncrypted).bind("amount", this.amount)
|
||||
.bind("asset_id", this.assetId).bind("data", this.data);
|
||||
saveHelper.execute();
|
||||
}
|
||||
|
||||
// Converters
|
||||
|
||||
protected static Transaction parse(ByteBuffer byteBuffer) throws ParseException {
|
||||
if (byteBuffer.remaining() < TIMESTAMP_LENGTH)
|
||||
throw new ParseException("Byte data too short for MessageTransaction");
|
||||
|
||||
long timestamp = byteBuffer.getLong();
|
||||
int version = Transaction.getVersionByTimestamp(timestamp);
|
||||
|
||||
int minimumRemaining = version == 1 ? TYPELESS_DATALESS_LENGTH_V1 : TYPELESS_DATALESS_LENGTH_V3;
|
||||
minimumRemaining -= TIMESTAMP_LENGTH; // Already read above
|
||||
|
||||
if (byteBuffer.remaining() < minimumRemaining)
|
||||
throw new ParseException("Byte data too short for MessageTransaction");
|
||||
|
||||
byte[] reference = new byte[REFERENCE_LENGTH];
|
||||
byteBuffer.get(reference);
|
||||
PublicKeyAccount sender = Serialization.deserializePublicKey(byteBuffer);
|
||||
String recipient = Serialization.deserializeRecipient(byteBuffer);
|
||||
|
||||
long assetId;
|
||||
if (version == 1)
|
||||
assetId = Asset.QORA;
|
||||
else
|
||||
assetId = byteBuffer.getLong();
|
||||
|
||||
BigDecimal amount = Serialization.deserializeBigDecimal(byteBuffer);
|
||||
|
||||
int dataSize = byteBuffer.getInt(0);
|
||||
// Don't allow invalid dataSize here to avoid run-time issues
|
||||
if (dataSize > MAX_DATA_SIZE)
|
||||
throw new ParseException("MessageTransaction data size too large");
|
||||
|
||||
byte[] data = new byte[dataSize];
|
||||
byteBuffer.get(data);
|
||||
|
||||
boolean isEncrypted = byteBuffer.get() != 0;
|
||||
boolean isText = byteBuffer.get() != 0;
|
||||
|
||||
BigDecimal fee = Serialization.deserializeBigDecimal(byteBuffer);
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
byteBuffer.get(signature);
|
||||
|
||||
return new MessageTransaction(sender, recipient, assetId, amount, fee, data, isText, isEncrypted, timestamp, reference, signature);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public JSONObject toJSON() throws SQLException {
|
||||
JSONObject json = getBaseJSON();
|
||||
|
||||
json.put("version", this.version);
|
||||
json.put("sender", this.sender.getAddress());
|
||||
json.put("senderPublicKey", HashCode.fromBytes(this.sender.getPublicKey()).toString());
|
||||
json.put("recipient", this.recipient.getAddress());
|
||||
json.put("amount", this.amount.toPlainString());
|
||||
json.put("assetId", this.assetId);
|
||||
json.put("isText", this.isText);
|
||||
json.put("isEncrypted", this.isEncrypted);
|
||||
|
||||
// We can only show plain text as unencoded
|
||||
if (this.isText && !this.isEncrypted)
|
||||
json.put("data", new String(this.data, Charset.forName("UTF-8")));
|
||||
else
|
||||
json.put("data", HashCode.fromBytes(this.data).toString());
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
public byte[] toBytes() {
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream(getDataLength());
|
||||
bytes.write(Ints.toByteArray(this.type.value));
|
||||
bytes.write(Longs.toByteArray(this.timestamp));
|
||||
bytes.write(this.reference);
|
||||
bytes.write(this.sender.getPublicKey());
|
||||
bytes.write(Base58.decode(this.recipient.getAddress()));
|
||||
|
||||
if (this.version != 1)
|
||||
bytes.write(Longs.toByteArray(this.assetId));
|
||||
|
||||
bytes.write(Serialization.serializeBigDecimal(this.amount));
|
||||
|
||||
bytes.write(Ints.toByteArray(this.data.length));
|
||||
bytes.write(this.data);
|
||||
|
||||
bytes.write((byte) (this.isEncrypted ? 1 : 0));
|
||||
bytes.write((byte) (this.isText ? 1 : 0));
|
||||
|
||||
bytes.write(Serialization.serializeBigDecimal(this.fee));
|
||||
bytes.write(this.signature);
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Processing
|
||||
|
||||
public ValidationResult isValid(Connection connection) throws SQLException {
|
||||
// Lowest cost checks first
|
||||
|
||||
// Are message transactions even allowed at this point?
|
||||
if (this.version != Transaction.getVersionByTimestamp(this.timestamp))
|
||||
return ValidationResult.NOT_YET_RELEASED;
|
||||
|
||||
if (BlockChain.getHeight() < Block.MESSAGE_RELEASE_HEIGHT)
|
||||
return ValidationResult.NOT_YET_RELEASED;
|
||||
|
||||
// Check data length
|
||||
if (this.data.length < 1 || this.data.length > MAX_DATA_SIZE)
|
||||
return ValidationResult.INVALID_DATA_LENGTH;
|
||||
|
||||
// Check recipient is a valid address
|
||||
if (!Crypto.isValidAddress(this.recipient.getAddress()))
|
||||
return ValidationResult.INVALID_ADDRESS;
|
||||
|
||||
if (this.version == 1) {
|
||||
// Check amount is positive (V1)
|
||||
if (this.amount.compareTo(BigDecimal.ZERO) <= 0)
|
||||
return ValidationResult.NEGATIVE_AMOUNT;
|
||||
} else {
|
||||
// Check amount is not negative (V3) as sending messages without a payment is OK
|
||||
if (this.amount.compareTo(BigDecimal.ZERO) < 0)
|
||||
return ValidationResult.NEGATIVE_AMOUNT;
|
||||
}
|
||||
|
||||
// Check fee is positive
|
||||
if (this.fee.compareTo(BigDecimal.ZERO) <= 0)
|
||||
return ValidationResult.NEGATIVE_FEE;
|
||||
|
||||
// Check reference is correct
|
||||
if (!Arrays.equals(this.sender.getLastReference(), this.reference))
|
||||
return ValidationResult.INVALID_REFERENCE;
|
||||
|
||||
// Does asset exist? (This test not present in gen1)
|
||||
if (this.assetId != Asset.QORA && !Asset.exists(this.assetId))
|
||||
return ValidationResult.ASSET_DOES_NOT_EXIST;
|
||||
|
||||
// If asset is QORA then we need to check amount + fee in one go
|
||||
if (this.assetId == Asset.QORA) {
|
||||
// Check sender has enough funds for amount + fee in QORA
|
||||
if (this.sender.getBalance(Asset.QORA, 1).compareTo(this.amount.add(this.fee)) == -1)
|
||||
return ValidationResult.NO_BALANCE;
|
||||
} else {
|
||||
// Check sender has enough funds for amount in whatever asset
|
||||
if (this.sender.getBalance(this.assetId, 1).compareTo(this.amount) == -1)
|
||||
return ValidationResult.NO_BALANCE;
|
||||
|
||||
// Check sender has enough funds for fee in QORA
|
||||
if (this.sender.getBalance(Asset.QORA, 1).compareTo(this.fee) == -1)
|
||||
return ValidationResult.NO_BALANCE;
|
||||
}
|
||||
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
public void process(Connection connection) throws SQLException {
|
||||
this.save(connection);
|
||||
|
||||
// Update sender's balance due to amount
|
||||
this.sender.setConfirmedBalance(connection, this.assetId, this.sender.getConfirmedBalance(this.assetId).subtract(this.amount));
|
||||
// Update sender's balance due to fee
|
||||
this.sender.setConfirmedBalance(connection, Asset.QORA, this.sender.getConfirmedBalance(Asset.QORA).subtract(this.fee));
|
||||
|
||||
// Update recipient's balance
|
||||
this.recipient.setConfirmedBalance(connection, this.assetId, this.recipient.getConfirmedBalance(this.assetId).add(this.amount));
|
||||
|
||||
// Update sender's reference
|
||||
this.sender.setLastReference(connection, this.signature);
|
||||
|
||||
// For QORA amounts only: if recipient has no reference yet, then this is their starting reference
|
||||
if (this.assetId == Asset.QORA && this.recipient.getLastReference() == null)
|
||||
this.recipient.setLastReference(connection, this.signature);
|
||||
}
|
||||
|
||||
public void orphan(Connection connection) throws SQLException {
|
||||
this.delete(connection);
|
||||
|
||||
// Update sender's balance due to amount
|
||||
this.sender.setConfirmedBalance(connection, this.assetId, this.sender.getConfirmedBalance(this.assetId).add(this.amount));
|
||||
// Update sender's balance due to fee
|
||||
this.sender.setConfirmedBalance(connection, Asset.QORA, this.sender.getConfirmedBalance(Asset.QORA).add(this.fee));
|
||||
|
||||
// Update recipient's balance
|
||||
this.recipient.setConfirmedBalance(connection, this.assetId, this.recipient.getConfirmedBalance(this.assetId).subtract(this.amount));
|
||||
|
||||
// Update sender's reference
|
||||
this.sender.setLastReference(connection, this.reference);
|
||||
|
||||
/*
|
||||
* For QORA amounts only: If recipient's last reference is this transaction's signature, then they can't have made any transactions of their own (which
|
||||
* would have changed their last reference) thus this is their first reference so remove it.
|
||||
*/
|
||||
if (this.assetId == Asset.QORA && Arrays.equals(this.recipient.getLastReference(), this.signature))
|
||||
this.recipient.setLastReference(connection, null);
|
||||
}
|
||||
|
||||
}
|
@ -49,7 +49,8 @@ public abstract class Transaction {
|
||||
|
||||
// Validation results
|
||||
public enum ValidationResult {
|
||||
OK(1), INVALID_ADDRESS(2), NEGATIVE_AMOUNT(3), NEGATIVE_FEE(4), NO_BALANCE(5), INVALID_REFERENCE(6);
|
||||
OK(1), INVALID_ADDRESS(2), NEGATIVE_AMOUNT(3), NEGATIVE_FEE(4), NO_BALANCE(5), INVALID_REFERENCE(6), INVALID_DATA_LENGTH(27), ASSET_DOES_NOT_EXIST(
|
||||
29), NOT_YET_RELEASED(1000);
|
||||
|
||||
public final int value;
|
||||
|
||||
@ -84,7 +85,7 @@ public abstract class Transaction {
|
||||
// Property lengths for serialisation
|
||||
protected static final int TYPE_LENGTH = 4;
|
||||
protected static final int TIMESTAMP_LENGTH = 8;
|
||||
protected static final int REFERENCE_LENGTH = 64;
|
||||
public static final int REFERENCE_LENGTH = 64;
|
||||
protected static final int FEE_LENGTH = 8;
|
||||
public static final int SIGNATURE_LENGTH = 64;
|
||||
protected static final int BASE_TYPELESS_LENGTH = TIMESTAMP_LENGTH + REFERENCE_LENGTH + FEE_LENGTH + SIGNATURE_LENGTH;
|
||||
@ -141,6 +142,13 @@ public abstract class Transaction {
|
||||
return this.timestamp + (24 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return length of byte[] if {@link Transactions#toBytes()} is called.
|
||||
* <p>
|
||||
* Used to allocate byte[]s or during serialization.
|
||||
*
|
||||
* @return length of serialized transaction
|
||||
*/
|
||||
public abstract int getDataLength();
|
||||
|
||||
public boolean hasMinimumFee() {
|
||||
@ -170,6 +178,14 @@ public abstract class Transaction {
|
||||
return recommendedFee.setScale(8);
|
||||
}
|
||||
|
||||
public static int getVersionByTimestamp(long timestamp) {
|
||||
if (timestamp < Block.POWFIX_RELEASE_TIMESTAMP) {
|
||||
return 1;
|
||||
} else {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get block height for this transaction in the blockchain.
|
||||
*
|
||||
@ -240,6 +256,8 @@ public abstract class Transaction {
|
||||
}
|
||||
|
||||
protected void delete(Connection connection) throws SQLException {
|
||||
// NOTE: The corresponding row in sub-table is deleted automatically by the database thanks to "ON DELETE CASCADE" in the sub-table's FOREIGN KEY
|
||||
// definition.
|
||||
DB.checkedExecute("DELETE FROM Transactions WHERE signature = ?", this.signature);
|
||||
}
|
||||
|
||||
@ -290,6 +308,13 @@ public abstract class Transaction {
|
||||
|
||||
// Converters
|
||||
|
||||
/**
|
||||
* Deserialize a byte[] into corresponding Transaction subclass.
|
||||
*
|
||||
* @param data
|
||||
* @return subclass of Transaction, e.g. PaymentTransaction
|
||||
* @throws ParseException
|
||||
*/
|
||||
public static Transaction parse(byte[] data) throws ParseException {
|
||||
if (data == null)
|
||||
return null;
|
||||
@ -310,6 +335,9 @@ public abstract class Transaction {
|
||||
case PAYMENT:
|
||||
return PaymentTransaction.parse(byteBuffer);
|
||||
|
||||
case MESSAGE:
|
||||
return MessageTransaction.parse(byteBuffer);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -349,6 +377,8 @@ public abstract class Transaction {
|
||||
|
||||
/**
|
||||
* Serialize transaction as byte[], stripping off trailing signature.
|
||||
* <p>
|
||||
* Used by signature-related methods such as {@link Transaction#calcSignature(PrivateKeyAccount)} and {@link Transaction#isSignatureValid()}
|
||||
*
|
||||
* @return byte[]
|
||||
*/
|
||||
@ -370,10 +400,43 @@ public abstract class Transaction {
|
||||
return this.creator.verify(this.signature, this.toBytesLessSignature());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether transaction can be added to the blockchain.
|
||||
* <p>
|
||||
* Checks if transaction can have {@link Transaction#process(Connection)} called.
|
||||
* <p>
|
||||
* Expected to be called within an ongoing SQL Transaction, typically by {@link Block#process(Connection)}, hence the need for the Connection parameter.
|
||||
* <p>
|
||||
* Transactions that have already been processed will return false.
|
||||
*
|
||||
* @param connection
|
||||
* @return true if transaction can be processed, false otherwise
|
||||
* @throws SQLException
|
||||
*/
|
||||
public abstract ValidationResult isValid(Connection connection) throws SQLException;
|
||||
|
||||
/**
|
||||
* Actually process a transaction, updating the blockchain.
|
||||
* <p>
|
||||
* Processes transaction, updating balances, references, assets, etc. as appropriate.
|
||||
* <p>
|
||||
* Expected to be called within an ongoing SQL Transaction, typically by {@link Block#process(Connection)}, hence the need for the Connection parameter.
|
||||
*
|
||||
* @param connection
|
||||
* @throws SQLException
|
||||
*/
|
||||
public abstract void process(Connection connection) throws SQLException;
|
||||
|
||||
/**
|
||||
* Undo transaction, updating the blockchain.
|
||||
* <p>
|
||||
* Undoes transaction, updating balances, references, assets, etc. as appropriate.
|
||||
* <p>
|
||||
* Expected to be called within an ongoing SQL Transaction, typically by {@link Block#process(Connection)}, hence the need for the Connection parameter.
|
||||
*
|
||||
* @param connection
|
||||
* @throws SQLException
|
||||
*/
|
||||
public abstract void orphan(Connection connection) throws SQLException;
|
||||
|
||||
}
|
||||
|
@ -67,7 +67,6 @@ public class blocks extends common {
|
||||
assertFalse(transaction.getFee().compareTo(BigDecimal.ZERO) == 0);
|
||||
assertNotNull(transaction.getReference());
|
||||
assertTrue(transaction.isSignatureValid());
|
||||
assertEquals(Transaction.ValidationResult.OK, transaction.isValid(connection));
|
||||
}
|
||||
|
||||
// Attempt to load first transaction directly from database
|
||||
@ -77,7 +76,21 @@ public class blocks extends common {
|
||||
assertFalse(transaction.getFee().compareTo(BigDecimal.ZERO) == 0);
|
||||
assertNotNull(transaction.getReference());
|
||||
assertTrue(transaction.isSignatureValid());
|
||||
assertEquals(Transaction.ValidationResult.OK, transaction.isValid(connection));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBlockSerialization() throws SQLException {
|
||||
try (final Connection connection = DB.getConnection()) {
|
||||
// Block 949 has lots of varied transactions
|
||||
// Blocks 390 & 754 have only payment transactions
|
||||
Block block = Block.fromHeight(754);
|
||||
assertNotNull("Block 754 is required for this test", block);
|
||||
assertTrue(block.isSignatureValid());
|
||||
|
||||
byte[] bytes = block.toBytes();
|
||||
|
||||
assertEquals(block.getDataLength(), bytes.length);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,7 @@ import com.google.common.io.CharStreams;
|
||||
|
||||
import database.DB;
|
||||
import qora.block.BlockChain;
|
||||
import qora.transaction.Transaction;
|
||||
import utils.Base58;
|
||||
|
||||
public class migrate extends common {
|
||||
@ -141,7 +142,7 @@ public class migrate extends common {
|
||||
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", "sender", "recipient", "is_text", "is_encrypted", "amount", "asset_id", "data"));
|
||||
+ 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"));
|
||||
@ -265,7 +266,8 @@ public class migrate extends common {
|
||||
fail();
|
||||
}
|
||||
|
||||
txPStmt.setTimestamp(5, new Timestamp((Long) transaction.get("timestamp")));
|
||||
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)
|
||||
@ -558,18 +560,19 @@ public class migrate extends common {
|
||||
}
|
||||
|
||||
messagePStmt.setBinaryStream(1, new ByteArrayInputStream(txSignature));
|
||||
messagePStmt.setBinaryStream(2, new ByteArrayInputStream(addressToPublicKey((String) transaction.get("creator"))));
|
||||
messagePStmt.setString(3, (String) transaction.get("recipient"));
|
||||
messagePStmt.setBoolean(4, isText);
|
||||
messagePStmt.setBoolean(5, isEncrypted);
|
||||
messagePStmt.setBigDecimal(6, BigDecimal.valueOf(Double.valueOf((String) transaction.get("amount")).doubleValue()));
|
||||
messagePStmt.setInt(2, Transaction.getVersionByTimestamp(transactionTimestamp));
|
||||
messagePStmt.setBinaryStream(3, new ByteArrayInputStream(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(7, ((Long) transaction.get("asset")).longValue());
|
||||
messagePStmt.setLong(8, ((Long) transaction.get("asset")).longValue());
|
||||
else
|
||||
messagePStmt.setLong(7, 0L); // QORA simulated asset
|
||||
messagePStmt.setLong(8, 0L); // QORA simulated asset
|
||||
|
||||
messagePStmt.setBinaryStream(8, messageDataStream);
|
||||
messagePStmt.setBinaryStream(9, messageDataStream);
|
||||
|
||||
messagePStmt.execute();
|
||||
messagePStmt.clearParameters();
|
||||
|
@ -2,12 +2,16 @@ package test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import qora.account.PrivateKeyAccount;
|
||||
import qora.block.Block;
|
||||
import qora.block.GenesisBlock;
|
||||
import utils.Base58;
|
||||
import utils.NTP;
|
||||
|
||||
public class signatures extends common {
|
||||
|
||||
@ -22,4 +26,24 @@ public class signatures extends common {
|
||||
assertEquals(expected58, Base58.encode(block.getSignature()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBlockSignature() throws SQLException {
|
||||
int version = 3;
|
||||
byte[] reference = Base58.decode(
|
||||
"BSfgEr6r1rXGGJCv8criR5NcBWfpHdJnm9x5unPwxvojEKCESv1wH1zJm7yvCeC48wshymYtARbHdUojbqWCCWW7h2UTc8g5oEx59C9M41dM7H48My8gVkcEZdxR1of3VgpE5UcowFp3kFC12hVcD9hUttJ2i2nZWMwprbFtUGyVv1U");
|
||||
long timestamp = NTP.getTime() - 5000;
|
||||
BigDecimal generatingBalance = BigDecimal.valueOf(10_000_000L).setScale(8);
|
||||
PrivateKeyAccount generator = new PrivateKeyAccount(
|
||||
new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 });
|
||||
byte[] atBytes = null;
|
||||
BigDecimal atFees = null;
|
||||
|
||||
Block block = new Block(version, reference, timestamp, generatingBalance, generator, atBytes, atFees);
|
||||
|
||||
block.calcGeneratorSignature();
|
||||
block.calcTransactionsSignature();
|
||||
|
||||
assertTrue(block.isSignatureValid());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -44,6 +44,8 @@ public class transactions extends common {
|
||||
Transaction parsedTransaction = Transaction.parse(bytes);
|
||||
|
||||
assertTrue(Arrays.equals(transaction.getSignature(), parsedTransaction.getSignature()));
|
||||
|
||||
assertEquals(transaction.getDataLength(), bytes.length);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -63,4 +65,10 @@ public class transactions extends common {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMessageSerialization() throws SQLException, ParseException {
|
||||
// Message transactions went live block 99000
|
||||
// Some transactions to be found in block 99001/2/5/6
|
||||
}
|
||||
|
||||
}
|
60
src/utils/NTP.java
Normal file
60
src/utils/NTP.java
Normal file
@ -0,0 +1,60 @@
|
||||
package utils;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
import org.apache.commons.net.ntp.NTPUDPClient;
|
||||
import org.apache.commons.net.ntp.TimeInfo;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public final class NTP {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(NTP.class);
|
||||
private static final long TIME_TILL_UPDATE = 1000 * 60 * 10;
|
||||
private static final String NTP_SERVER = "pool.ntp.org";
|
||||
|
||||
private static long lastUpdate = 0;
|
||||
private static long offset = 0;
|
||||
|
||||
public static long getTime() {
|
||||
// Every so often use NTP to find out offset between this system's time and internet time
|
||||
if (System.currentTimeMillis() > lastUpdate + TIME_TILL_UPDATE) {
|
||||
updateOffset();
|
||||
lastUpdate = System.currentTimeMillis();
|
||||
|
||||
// Log new value of offset
|
||||
// TODO: LOGGER.info(Lang.getInstance().translate("Adjusting time with %offset% milliseconds.").replace("%offset%", String.valueOf(offset)));
|
||||
LOGGER.info("Adjusting time with %offset% milliseconds.".replace("%offset%", String.valueOf(offset)));
|
||||
}
|
||||
|
||||
// Return time that is nearer internet time
|
||||
return System.currentTimeMillis() + offset;
|
||||
}
|
||||
|
||||
private static void updateOffset() {
|
||||
// Create NTP client
|
||||
NTPUDPClient client = new NTPUDPClient();
|
||||
|
||||
// Set communications timeout
|
||||
client.setDefaultTimeout(10000);
|
||||
try {
|
||||
// Open client (create socket, etc.)
|
||||
client.open();
|
||||
|
||||
// Get time info from NTP server
|
||||
InetAddress hostAddr = InetAddress.getByName(NTP_SERVER);
|
||||
TimeInfo info = client.getTime(hostAddr);
|
||||
info.computeDetails();
|
||||
|
||||
// Cache offset between this system's time and internet time
|
||||
if (info.getOffset() != null)
|
||||
offset = info.getOffset();
|
||||
} catch (Exception e) {
|
||||
// Error while communicating with NTP server - ignored
|
||||
}
|
||||
|
||||
// We're done with NTP client
|
||||
client.close();
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user