forked from Qortal/qortal
Merge branch 'presence-txn' into LTCv3-with-presence
This commit is contained in:
commit
ddb55210b4
9
pom.xml
9
pom.xml
@ -6,6 +6,7 @@
|
||||
<version>1.3.7</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<skipTests>true</skipTests>
|
||||
<altcoinj.version>bf9fb80</altcoinj.version>
|
||||
<bitcoinj.version>0.15.6</bitcoinj.version>
|
||||
<bouncycastle.version>1.64</bouncycastle.version>
|
||||
@ -317,6 +318,14 @@
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.22.2</version>
|
||||
<configuration>
|
||||
<skipTests>${skipTests}</skipTests>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
|
@ -18,6 +18,8 @@ public class AtStatesTrimmer implements Runnable {
|
||||
Thread.currentThread().setName("AT States trimmer");
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
int trimStartHeight = repository.getATRepository().getAtTrimHeight();
|
||||
|
||||
repository.getATRepository().prepareForAtStateTrimming();
|
||||
repository.saveChanges();
|
||||
|
||||
@ -41,8 +43,6 @@ public class AtStatesTrimmer implements Runnable {
|
||||
long upperTrimmableTimestamp = Math.min(currentTrimmableTimestamp, chainTrimmableTimestamp);
|
||||
int upperTrimmableHeight = repository.getBlockRepository().getHeightFromTimestamp(upperTrimmableTimestamp);
|
||||
|
||||
int trimStartHeight = repository.getATRepository().getAtTrimHeight();
|
||||
|
||||
int upperBatchHeight = trimStartHeight + Settings.getInstance().getAtStatesTrimBatchSize();
|
||||
int upperTrimHeight = Math.min(upperBatchHeight, upperTrimmableHeight);
|
||||
|
||||
@ -53,17 +53,20 @@ public class AtStatesTrimmer implements Runnable {
|
||||
repository.saveChanges();
|
||||
|
||||
if (numAtStatesTrimmed > 0) {
|
||||
final int finalTrimStartHeight = trimStartHeight;
|
||||
LOGGER.debug(() -> String.format("Trimmed %d AT state%s between blocks %d and %d",
|
||||
numAtStatesTrimmed, (numAtStatesTrimmed != 1 ? "s" : ""),
|
||||
trimStartHeight, upperTrimHeight));
|
||||
finalTrimStartHeight, upperTrimHeight));
|
||||
} else {
|
||||
// Can we move onto next batch?
|
||||
if (upperTrimmableHeight > upperBatchHeight) {
|
||||
repository.getATRepository().setAtTrimHeight(upperBatchHeight);
|
||||
trimStartHeight = upperBatchHeight;
|
||||
repository.getATRepository().setAtTrimHeight(trimStartHeight);
|
||||
repository.getATRepository().prepareForAtStateTrimming();
|
||||
repository.saveChanges();
|
||||
|
||||
LOGGER.debug(() -> String.format("Bumping AT state trim height to %d", upperBatchHeight));
|
||||
final int finalTrimStartHeight = trimStartHeight;
|
||||
LOGGER.debug(() -> String.format("Bumping AT state base trim height to %d", finalTrimStartHeight));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -294,8 +294,12 @@ public class BlockMinter extends Thread {
|
||||
newBlock.getMinter().getAddress()));
|
||||
}
|
||||
|
||||
// Notify controller after we're released blockchain lock
|
||||
// Notify network after we're released blockchain lock
|
||||
newBlockMinted = true;
|
||||
|
||||
// Notify Controller
|
||||
repository.discardChanges(); // clear transaction status to prevent deadlocks
|
||||
Controller.getInstance().onNewBlock(newBlock.getBlockData());
|
||||
} catch (DataException e) {
|
||||
// Unable to process block - report and discard
|
||||
LOGGER.error("Unable to process newly minted block?", e);
|
||||
@ -306,12 +310,9 @@ public class BlockMinter extends Thread {
|
||||
}
|
||||
|
||||
if (newBlockMinted) {
|
||||
// Notify Controller and broadcast our new chain to network
|
||||
// Broadcast our new chain to network
|
||||
BlockData newBlockData = newBlock.getBlockData();
|
||||
|
||||
repository.discardChanges(); // clear transaction status to prevent deadlocks
|
||||
Controller.getInstance().onNewBlock(newBlockData);
|
||||
|
||||
Network network = Network.getInstance();
|
||||
network.broadcast(broadcastPeer -> network.buildHeightMessage(broadcastPeer, newBlockData));
|
||||
}
|
||||
|
@ -800,11 +800,14 @@ public class Controller extends Thread {
|
||||
|
||||
List<TransactionData> transactions = repository.getTransactionRepository().getUnconfirmedTransactions();
|
||||
|
||||
for (TransactionData transactionData : transactions)
|
||||
if (now >= Transaction.getDeadline(transactionData)) {
|
||||
LOGGER.info(String.format("Deleting expired, unconfirmed transaction %s", Base58.encode(transactionData.getSignature())));
|
||||
for (TransactionData transactionData : transactions) {
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
|
||||
if (now >= transaction.getDeadline()) {
|
||||
LOGGER.info(() -> String.format("Deleting expired, unconfirmed transaction %s", Base58.encode(transactionData.getSignature())));
|
||||
repository.getTransactionRepository().delete(transactionData);
|
||||
}
|
||||
}
|
||||
|
||||
repository.saveChanges();
|
||||
} catch (DataException e) {
|
||||
|
@ -23,6 +23,8 @@ public class OnlineAccountsSignaturesTrimmer implements Runnable {
|
||||
// Don't even start trimming until initial rush has ended
|
||||
Thread.sleep(INITIAL_SLEEP_PERIOD);
|
||||
|
||||
int trimStartHeight = repository.getBlockRepository().getOnlineAccountsSignaturesTrimHeight();
|
||||
|
||||
while (!Controller.isStopping()) {
|
||||
repository.discardChanges();
|
||||
|
||||
@ -40,8 +42,6 @@ public class OnlineAccountsSignaturesTrimmer implements Runnable {
|
||||
long upperTrimmableTimestamp = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime();
|
||||
int upperTrimmableHeight = repository.getBlockRepository().getHeightFromTimestamp(upperTrimmableTimestamp);
|
||||
|
||||
int trimStartHeight = repository.getBlockRepository().getOnlineAccountsSignaturesTrimHeight();
|
||||
|
||||
int upperBatchHeight = trimStartHeight + Settings.getInstance().getOnlineSignaturesTrimBatchSize();
|
||||
int upperTrimHeight = Math.min(upperBatchHeight, upperTrimmableHeight);
|
||||
|
||||
@ -52,16 +52,20 @@ public class OnlineAccountsSignaturesTrimmer implements Runnable {
|
||||
repository.saveChanges();
|
||||
|
||||
if (numSigsTrimmed > 0) {
|
||||
final int finalTrimStartHeight = trimStartHeight;
|
||||
LOGGER.debug(() -> String.format("Trimmed %d online accounts signature%s between blocks %d and %d",
|
||||
numSigsTrimmed, (numSigsTrimmed != 1 ? "s" : ""),
|
||||
trimStartHeight, upperTrimHeight));
|
||||
finalTrimStartHeight, upperTrimHeight));
|
||||
} else {
|
||||
// Can we move onto next batch?
|
||||
if (upperTrimmableHeight > upperBatchHeight) {
|
||||
repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(upperBatchHeight);
|
||||
trimStartHeight = upperBatchHeight;
|
||||
|
||||
repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(trimStartHeight);
|
||||
repository.saveChanges();
|
||||
|
||||
LOGGER.debug(() -> String.format("Bumping online accounts signatures trim height to %d", upperBatchHeight));
|
||||
final int finalTrimStartHeight = trimStartHeight;
|
||||
LOGGER.debug(() -> String.format("Bumping online accounts signatures base trim height to %d", finalTrimStartHeight));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -387,26 +387,32 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
break;
|
||||
|
||||
case ALICE_WAITING_FOR_P2SH_A:
|
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData);
|
||||
handleAliceWaitingForP2shA(repository, tradeBotData, atData, tradeData);
|
||||
break;
|
||||
|
||||
case BOB_WAITING_FOR_MESSAGE:
|
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData);
|
||||
handleBobWaitingForMessage(repository, tradeBotData, atData, tradeData);
|
||||
break;
|
||||
|
||||
case ALICE_WAITING_FOR_AT_LOCK:
|
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData);
|
||||
handleAliceWaitingForAtLock(repository, tradeBotData, atData, tradeData);
|
||||
break;
|
||||
|
||||
case BOB_WAITING_FOR_P2SH_B:
|
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData);
|
||||
handleBobWaitingForP2shB(repository, tradeBotData, atData, tradeData);
|
||||
break;
|
||||
|
||||
case ALICE_WATCH_P2SH_B:
|
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData);
|
||||
handleAliceWatchingP2shB(repository, tradeBotData, atData, tradeData);
|
||||
break;
|
||||
|
||||
case BOB_WAITING_FOR_AT_REDEEM:
|
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData);
|
||||
handleBobWaitingForAtRedeem(repository, tradeBotData, atData, tradeData);
|
||||
break;
|
||||
|
||||
@ -415,10 +421,12 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
break;
|
||||
|
||||
case ALICE_REFUNDING_B:
|
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData);
|
||||
handleAliceRefundingP2shB(repository, tradeBotData, atData, tradeData);
|
||||
break;
|
||||
|
||||
case ALICE_REFUNDING_A:
|
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData);
|
||||
handleAliceRefundingP2shA(repository, tradeBotData, atData, tradeData);
|
||||
break;
|
||||
|
||||
|
@ -384,14 +384,17 @@ public class LitecoinACCTv1TradeBot implements AcctTradeBot {
|
||||
break;
|
||||
|
||||
case BOB_WAITING_FOR_MESSAGE:
|
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData);
|
||||
handleBobWaitingForMessage(repository, tradeBotData, atData, tradeData);
|
||||
break;
|
||||
|
||||
case ALICE_WAITING_FOR_AT_LOCK:
|
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData);
|
||||
handleAliceWaitingForAtLock(repository, tradeBotData, atData, tradeData);
|
||||
break;
|
||||
|
||||
case BOB_WAITING_FOR_AT_REDEEM:
|
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData);
|
||||
handleBobWaitingForAtRedeem(repository, tradeBotData, atData, tradeData);
|
||||
break;
|
||||
|
||||
@ -400,6 +403,7 @@ public class LitecoinACCTv1TradeBot implements AcctTradeBot {
|
||||
break;
|
||||
|
||||
case ALICE_REFUNDING_A:
|
||||
TradeBot.getInstance().updatePresence(repository, tradeBotData);
|
||||
handleAliceRefundingP2shA(repository, tradeBotData, atData, tradeData);
|
||||
break;
|
||||
|
||||
|
@ -2,6 +2,7 @@ package org.qortal.controller.tradebot;
|
||||
|
||||
import java.awt.TrayIcon.MessageType;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -23,16 +24,25 @@ import org.qortal.crosschain.SupportedBlockchain;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.data.crosschain.TradeBotData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.PresenceTransactionData;
|
||||
import org.qortal.event.Event;
|
||||
import org.qortal.event.EventBus;
|
||||
import org.qortal.event.Listener;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.gui.SysTray;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.PresenceTransaction;
|
||||
import org.qortal.transaction.PresenceTransaction.PresenceType;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
/**
|
||||
* Performing cross-chain trading steps on behalf of user.
|
||||
* <p>
|
||||
@ -73,6 +83,8 @@ public class TradeBot implements Listener {
|
||||
|
||||
private static TradeBot instance;
|
||||
|
||||
private final Map<String, Long> presenceTimestampsByAtAddress = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
private TradeBot() {
|
||||
EventBus.INSTANCE.addListener(event -> TradeBot.getInstance().listen(event));
|
||||
}
|
||||
@ -292,4 +304,41 @@ public class TradeBot implements Listener {
|
||||
return acctTradeBotSupplier.get();
|
||||
}
|
||||
|
||||
// PRESENCE-related
|
||||
/*package*/ void updatePresence(Repository repository, TradeBotData tradeBotData) throws DataException {
|
||||
String key = tradeBotData.getAtAddress();
|
||||
|
||||
long now = NTP.getTime();
|
||||
long threshold = now - PresenceType.TRADE_BOT.getLifetime();
|
||||
|
||||
long timestamp = presenceTimestampsByAtAddress.compute(key, (k, v) -> (v == null || v < threshold) ? now : v);
|
||||
|
||||
// If timestamp hasn't been updated then nothing to do
|
||||
if (timestamp != now)
|
||||
return;
|
||||
|
||||
PrivateKeyAccount tradeNativeAccount = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||
|
||||
int txGroupId = Group.NO_GROUP;
|
||||
byte[] reference = new byte[TransactionTransformer.SIGNATURE_LENGTH];
|
||||
byte[] creatorPublicKey = tradeNativeAccount.getPublicKey();
|
||||
long fee = 0L;
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, creatorPublicKey, fee, null);
|
||||
|
||||
int nonce = 0;
|
||||
byte[] timestampSignature = tradeNativeAccount.sign(Longs.toByteArray(timestamp));
|
||||
|
||||
PresenceTransactionData transactionData = new PresenceTransactionData(baseTransactionData, nonce, PresenceType.TRADE_BOT, timestampSignature);
|
||||
|
||||
PresenceTransaction presenceTransaction = new PresenceTransaction(repository, transactionData);
|
||||
presenceTransaction.computeNonce();
|
||||
|
||||
presenceTransaction.sign(tradeNativeAccount);
|
||||
|
||||
ValidationResult result = presenceTransaction.importAsUnconfirmed();
|
||||
if (result != ValidationResult.OK)
|
||||
LOGGER.debug(() -> String.format("Unable to build trade-bot PRESENCE transaction for %s: %s", tradeBotData.getAtAddress(), result.name()));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,73 @@
|
||||
package org.qortal.data.transaction;
|
||||
|
||||
import javax.xml.bind.Unmarshaller;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import org.qortal.transaction.PresenceTransaction.PresenceType;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
|
||||
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@Schema(allOf = { TransactionData.class })
|
||||
public class PresenceTransactionData extends TransactionData {
|
||||
|
||||
// Properties
|
||||
@Schema(description = "sender's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
|
||||
private byte[] senderPublicKey;
|
||||
|
||||
@Schema(accessMode = AccessMode.READ_ONLY)
|
||||
private int nonce;
|
||||
|
||||
private PresenceType presenceType;
|
||||
|
||||
@Schema(description = "timestamp signature", example = "2yGEbwRFyhPZZckKA")
|
||||
private byte[] timestampSignature;
|
||||
|
||||
// Constructors
|
||||
|
||||
// For JAXB
|
||||
protected PresenceTransactionData() {
|
||||
super(TransactionType.PRESENCE);
|
||||
}
|
||||
|
||||
public void afterUnmarshal(Unmarshaller u, Object parent) {
|
||||
this.creatorPublicKey = this.senderPublicKey;
|
||||
}
|
||||
|
||||
public PresenceTransactionData(BaseTransactionData baseTransactionData,
|
||||
int nonce, PresenceType presenceType, byte[] timestampSignature) {
|
||||
super(TransactionType.PRESENCE, baseTransactionData);
|
||||
|
||||
this.senderPublicKey = baseTransactionData.creatorPublicKey;
|
||||
this.nonce = nonce;
|
||||
this.presenceType = presenceType;
|
||||
this.timestampSignature = timestampSignature;
|
||||
}
|
||||
|
||||
// Getters/Setters
|
||||
|
||||
public byte[] getSenderPublicKey() {
|
||||
return this.senderPublicKey;
|
||||
}
|
||||
|
||||
public int getNonce() {
|
||||
return this.nonce;
|
||||
}
|
||||
|
||||
public void setNonce(int nonce) {
|
||||
this.nonce = nonce;
|
||||
}
|
||||
|
||||
public PresenceType getPresenceType() {
|
||||
return this.presenceType;
|
||||
}
|
||||
|
||||
public byte[] getTimestampSignature() {
|
||||
return this.timestampSignature;
|
||||
}
|
||||
|
||||
}
|
@ -40,7 +40,7 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
|
||||
GroupApprovalTransactionData.class, SetGroupTransactionData.class,
|
||||
UpdateAssetTransactionData.class,
|
||||
AccountFlagsTransactionData.class, RewardShareTransactionData.class,
|
||||
AccountLevelTransactionData.class, ChatTransactionData.class
|
||||
AccountLevelTransactionData.class, ChatTransactionData.class, PresenceTransactionData.class
|
||||
})
|
||||
//All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
|
@ -4,6 +4,10 @@ public abstract class RepositoryManager {
|
||||
|
||||
private static RepositoryFactory repositoryFactory = null;
|
||||
|
||||
public static RepositoryFactory getRepositoryFactory() {
|
||||
return repositoryFactory;
|
||||
}
|
||||
|
||||
public static void setRepositoryFactory(RepositoryFactory newRepositoryFactory) {
|
||||
repositoryFactory = newRepositoryFactory;
|
||||
}
|
||||
|
@ -239,6 +239,18 @@ public interface TransactionRepository {
|
||||
return getUnconfirmedTransactions(null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of unconfirmed transactions with specified type and/or creator.
|
||||
* <p>
|
||||
* At least one of <tt>txType</tt> or <tt>creatorPublicKey</tt> must be non-null.
|
||||
*
|
||||
* @param txType optional
|
||||
* @param creatorPublicKey optional
|
||||
* @return list of transactions, or empty if none.
|
||||
* @throws DataException
|
||||
*/
|
||||
public List<TransactionData> getUnconfirmedTransactions(TransactionType txType, byte[] creatorPublicKey) throws DataException;
|
||||
|
||||
/**
|
||||
* Remove transaction from unconfirmed transactions pile.
|
||||
*
|
||||
|
@ -807,6 +807,13 @@ public class HSQLDBDatabaseUpdates {
|
||||
stmt.execute("ALTER TABLE TradeBotStates ALTER COLUMN hash_of_secret SET NULL");
|
||||
break;
|
||||
|
||||
case 32:
|
||||
// PRESENCE transactions
|
||||
stmt.execute("CREATE TABLE IF NOT EXISTS PresenceTransactions ("
|
||||
+ "signature Signature, nonce INT NOT NULL, presence_type INT NOT NULL, "
|
||||
+ "timestamp_signature Signature NOT NULL, " + TRANSACTION_KEYS + ")");
|
||||
break;
|
||||
|
||||
default:
|
||||
// nothing to do
|
||||
return false;
|
||||
|
@ -60,7 +60,8 @@ public class HSQLDBRepository implements Repository {
|
||||
protected List<String> sqlStatements;
|
||||
protected long sessionId;
|
||||
protected final Map<String, PreparedStatement> preparedStatementCache = new HashMap<>();
|
||||
protected final Object trimHeightsLock = new Object();
|
||||
// We want the same object corresponding to the actual DB
|
||||
protected final Object trimHeightsLock = RepositoryManager.getRepositoryFactory();
|
||||
|
||||
private final ATRepository atRepository = new HSQLDBATRepository(this);
|
||||
private final AccountRepository accountRepository = new HSQLDBAccountRepository(this);
|
||||
|
@ -0,0 +1,57 @@
|
||||
package org.qortal.repository.hsqldb.transaction;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.PresenceTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.hsqldb.HSQLDBRepository;
|
||||
import org.qortal.repository.hsqldb.HSQLDBSaver;
|
||||
import org.qortal.transaction.PresenceTransaction.PresenceType;
|
||||
|
||||
public class HSQLDBPresenceTransactionRepository extends HSQLDBTransactionRepository {
|
||||
|
||||
public HSQLDBPresenceTransactionRepository(HSQLDBRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException {
|
||||
String sql = "SELECT nonce, presence_type, timestamp_signature FROM PresenceTransactions WHERE signature = ?";
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) {
|
||||
if (resultSet == null)
|
||||
return null;
|
||||
|
||||
int nonce = resultSet.getInt(1);
|
||||
int presenceTypeValue = resultSet.getInt(2);
|
||||
PresenceType presenceType = PresenceType.valueOf(presenceTypeValue);
|
||||
|
||||
byte[] timestampSignature = resultSet.getBytes(3);
|
||||
|
||||
return new PresenceTransactionData(baseTransactionData, nonce, presenceType, timestampSignature);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch presence transaction from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save(TransactionData transactionData) throws DataException {
|
||||
PresenceTransactionData presenceTransactionData = (PresenceTransactionData) transactionData;
|
||||
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("PresenceTransactions");
|
||||
|
||||
saveHelper.bind("signature", presenceTransactionData.getSignature())
|
||||
.bind("nonce", presenceTransactionData.getNonce())
|
||||
.bind("presence_type", presenceTransactionData.getPresenceType().value)
|
||||
.bind("timestamp_signature", presenceTransactionData.getTimestampSignature());
|
||||
|
||||
try {
|
||||
saveHelper.execute(this.repository);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to save chat transaction into repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1124,6 +1124,63 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<TransactionData> getUnconfirmedTransactions(TransactionType txType, byte[] creatorPublicKey) throws DataException {
|
||||
if (txType == null && creatorPublicKey == null)
|
||||
throw new IllegalArgumentException("At least one of txType or creatorPublicKey must be non-null");
|
||||
|
||||
StringBuilder sql = new StringBuilder(1024);
|
||||
sql.append("SELECT signature FROM UnconfirmedTransactions ");
|
||||
sql.append("JOIN Transactions USING (signature) ");
|
||||
sql.append("WHERE ");
|
||||
|
||||
List<String> whereClauses = new ArrayList<>();
|
||||
List<Object> bindParams = new ArrayList<>();
|
||||
|
||||
if (txType != null) {
|
||||
whereClauses.add("type = ?");
|
||||
bindParams.add(Integer.valueOf(txType.value));
|
||||
}
|
||||
|
||||
if (creatorPublicKey != null) {
|
||||
whereClauses.add("creator = ?");
|
||||
bindParams.add(creatorPublicKey);
|
||||
}
|
||||
|
||||
final int whereClausesSize = whereClauses.size();
|
||||
for (int wci = 0; wci < whereClausesSize; ++wci) {
|
||||
if (wci != 0)
|
||||
sql.append(" AND ");
|
||||
|
||||
sql.append(whereClauses.get(wci));
|
||||
}
|
||||
|
||||
sql.append("ORDER BY created_when, signature");
|
||||
|
||||
List<TransactionData> transactions = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
|
||||
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(String.format("Unable to fetch unconfirmed transaction %s from repository?", Base58.encode(signature)));
|
||||
|
||||
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 {
|
||||
|
@ -141,7 +141,7 @@ public class ChatTransaction extends Transaction {
|
||||
// If we exist in the repository then we've been imported as unconfirmed,
|
||||
// but we don't want to make it into a block, so return fake non-OK result.
|
||||
if (this.repository.getTransactionRepository().exists(this.chatTransactionData.getSignature()))
|
||||
return ValidationResult.CHAT;
|
||||
return ValidationResult.INVALID_BUT_OK;
|
||||
|
||||
// If we have a recipient, check it is a valid address
|
||||
String recipientAddress = chatTransactionData.getRecipient();
|
||||
@ -188,6 +188,16 @@ public class ChatTransaction extends Transaction {
|
||||
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, difficulty, nonce);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure there's at least a skeleton account so people
|
||||
* can retrieve sender's public key using address, even if all their messages
|
||||
* expire.
|
||||
*/
|
||||
@Override
|
||||
protected void onImportAsUnconfirmed() throws DataException {
|
||||
this.getCreator().ensureAccount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
throw new DataException("CHAT transactions should never be processed");
|
||||
|
231
src/main/java/org/qortal/transaction/PresenceTransaction.java
Normal file
231
src/main/java/org/qortal/transaction/PresenceTransaction.java
Normal file
@ -0,0 +1,231 @@
|
||||
package org.qortal.transaction;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.crosschain.BitcoinACCTv1;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.crypto.MemoryPoW;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.data.transaction.PresenceTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.PresenceTransactionTransformer;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
public class PresenceTransaction extends Transaction {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(PresenceTransaction.class);
|
||||
|
||||
// Properties
|
||||
private PresenceTransactionData presenceTransactionData;
|
||||
|
||||
// Other useful constants
|
||||
public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
|
||||
public static final int POW_DIFFICULTY = 8; // leading zero bits
|
||||
|
||||
public enum PresenceType {
|
||||
REWARD_SHARE(0) {
|
||||
@Override
|
||||
public long getLifetime() {
|
||||
return Controller.ONLINE_TIMESTAMP_MODULUS;
|
||||
}
|
||||
},
|
||||
TRADE_BOT(1) {
|
||||
@Override
|
||||
public long getLifetime() {
|
||||
return 30 * 60 * 1000L; // 30 minutes in milliseconds
|
||||
}
|
||||
};
|
||||
|
||||
public final int value;
|
||||
private static final Map<Integer, PresenceType> map = stream(PresenceType.values()).collect(toMap(type -> type.value, type -> type));
|
||||
|
||||
PresenceType(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public abstract long getLifetime();
|
||||
|
||||
public static PresenceType valueOf(int value) {
|
||||
return map.get(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Constructors
|
||||
|
||||
public PresenceTransaction(Repository repository, TransactionData transactionData) {
|
||||
super(repository, transactionData);
|
||||
|
||||
this.presenceTransactionData = (PresenceTransactionData) this.transactionData;
|
||||
}
|
||||
|
||||
// More information
|
||||
|
||||
@Override
|
||||
public long getDeadline() {
|
||||
return this.transactionData.getTimestamp() + this.presenceTransactionData.getPresenceType().getLifetime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getRecipientAddresses() throws DataException {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// Navigation
|
||||
|
||||
public Account getSender() {
|
||||
return this.getCreator();
|
||||
}
|
||||
|
||||
// Processing
|
||||
|
||||
public void computeNonce() throws DataException {
|
||||
byte[] transactionBytes;
|
||||
|
||||
try {
|
||||
transactionBytes = TransactionTransformer.toBytesForSigning(this.transactionData);
|
||||
} catch (TransformationException e) {
|
||||
throw new RuntimeException("Unable to transform transaction to byte array for verification", e);
|
||||
}
|
||||
|
||||
// Clear nonce from transactionBytes
|
||||
PresenceTransactionTransformer.clearNonce(transactionBytes);
|
||||
|
||||
// Calculate nonce
|
||||
this.presenceTransactionData.setNonce(MemoryPoW.compute2(transactionBytes, POW_BUFFER_SIZE, POW_DIFFICULTY));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether PRESENCE transaction has valid txGroupId.
|
||||
* <p>
|
||||
* We insist on NO_GROUP.
|
||||
*/
|
||||
@Override
|
||||
protected boolean isValidTxGroupId() throws DataException {
|
||||
int txGroupId = this.transactionData.getTxGroupId();
|
||||
|
||||
return txGroupId == Group.NO_GROUP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationResult isFeeValid() throws DataException {
|
||||
if (this.transactionData.getFee() < 0)
|
||||
return ValidationResult.NEGATIVE_FEE;
|
||||
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasValidReference() throws DataException {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationResult isValid() throws DataException {
|
||||
// Nonce checking is done via isSignatureValid() as that method is only called once per import
|
||||
|
||||
// If we exist in the repository then we've been imported as unconfirmed,
|
||||
// but we don't want to make it into a block, so return fake non-OK result.
|
||||
if (this.repository.getTransactionRepository().exists(this.presenceTransactionData.getSignature()))
|
||||
return ValidationResult.INVALID_BUT_OK;
|
||||
|
||||
// We only support TRADE_BOT-type PRESENCE at this time
|
||||
if (PresenceType.TRADE_BOT != this.presenceTransactionData.getPresenceType())
|
||||
return ValidationResult.NOT_YET_RELEASED;
|
||||
|
||||
// Check timestamp signature
|
||||
byte[] timestampSignature = this.presenceTransactionData.getTimestampSignature();
|
||||
byte[] timestampBytes = Longs.toByteArray(this.presenceTransactionData.getTimestamp());
|
||||
if (!Crypto.verify(this.transactionData.getCreatorPublicKey(), timestampSignature, timestampBytes))
|
||||
return ValidationResult.INVALID_TIMESTAMP_SIGNATURE;
|
||||
|
||||
// Check signer is known trade address
|
||||
String signerAddress = Crypto.toAddress(this.transactionData.getCreatorPublicKey());
|
||||
|
||||
byte[] codeHash = BitcoinACCTv1.CODE_BYTES_HASH;
|
||||
boolean isExecutable = true;
|
||||
|
||||
List<ATData> atsData = repository.getATRepository().getATsByFunctionality(codeHash, isExecutable, null, null, null);
|
||||
|
||||
for (ATData atData : atsData) {
|
||||
CrossChainTradeData crossChainTradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
|
||||
|
||||
if (crossChainTradeData.qortalCreatorTradeAddress.equals(signerAddress))
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
return ValidationResult.AT_UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSignatureValid() {
|
||||
byte[] signature = this.transactionData.getSignature();
|
||||
if (signature == null)
|
||||
return false;
|
||||
|
||||
byte[] transactionBytes;
|
||||
|
||||
try {
|
||||
transactionBytes = PresenceTransactionTransformer.toBytesForSigning(this.transactionData);
|
||||
} catch (TransformationException e) {
|
||||
throw new RuntimeException("Unable to transform transaction to byte array for verification", e);
|
||||
}
|
||||
|
||||
if (!Crypto.verify(this.transactionData.getCreatorPublicKey(), signature, transactionBytes))
|
||||
return false;
|
||||
|
||||
int nonce = this.presenceTransactionData.getNonce();
|
||||
|
||||
// Clear nonce from transactionBytes
|
||||
PresenceTransactionTransformer.clearNonce(transactionBytes);
|
||||
|
||||
// Check nonce
|
||||
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, POW_DIFFICULTY, nonce);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any PRESENCE transactions by the same signer that have older timestamps.
|
||||
*/
|
||||
@Override
|
||||
protected void onImportAsUnconfirmed() throws DataException {
|
||||
byte[] creatorPublicKey = this.transactionData.getCreatorPublicKey();
|
||||
List<TransactionData> creatorsPresenceTransactions = this.repository.getTransactionRepository().getUnconfirmedTransactions(TransactionType.PRESENCE, creatorPublicKey);
|
||||
|
||||
if (creatorsPresenceTransactions.isEmpty())
|
||||
return;
|
||||
|
||||
// List should contain oldest transaction first, so remove all but last from repository.
|
||||
creatorsPresenceTransactions.remove(creatorsPresenceTransactions.size() - 1);
|
||||
for (TransactionData transactionData : creatorsPresenceTransactions) {
|
||||
LOGGER.info(() -> String.format("Deleting older PRESENCE transaction %s", Base58.encode(transactionData.getSignature())));
|
||||
this.repository.getTransactionRepository().delete(transactionData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process() throws DataException {
|
||||
throw new DataException("PRESENCE transactions should never be processed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void orphan() throws DataException {
|
||||
throw new DataException("PRESENCE transactions should never be orphaned");
|
||||
}
|
||||
|
||||
}
|
@ -83,7 +83,8 @@ public abstract class Transaction {
|
||||
ENABLE_FORGING(37, false),
|
||||
REWARD_SHARE(38, false),
|
||||
ACCOUNT_LEVEL(39, false),
|
||||
TRANSFER_PRIVS(40, false);
|
||||
TRANSFER_PRIVS(40, false),
|
||||
PRESENCE(41, false);
|
||||
|
||||
public final int value;
|
||||
public final boolean needsApproval;
|
||||
@ -244,7 +245,8 @@ public abstract class Transaction {
|
||||
ACCOUNT_ALREADY_EXISTS(92),
|
||||
INVALID_GROUP_BLOCK_DELAY(93),
|
||||
INCORRECT_NONCE(94),
|
||||
CHAT(999),
|
||||
INVALID_TIMESTAMP_SIGNATURE(95),
|
||||
INVALID_BUT_OK(999),
|
||||
NOT_YET_RELEASED(1000);
|
||||
|
||||
public final int value;
|
||||
@ -798,13 +800,7 @@ public abstract class Transaction {
|
||||
repository.getTransactionRepository().save(transactionData);
|
||||
repository.getTransactionRepository().unconfirmTransaction(transactionData);
|
||||
|
||||
/*
|
||||
* If CHAT transaction then ensure there's at least a skeleton account so people
|
||||
* can retrieve sender's public key using address, even if all their messages
|
||||
* expire.
|
||||
*/
|
||||
if (transactionData.getType() == TransactionType.CHAT)
|
||||
this.getCreator().ensureAccount();
|
||||
this.onImportAsUnconfirmed();
|
||||
|
||||
repository.saveChanges();
|
||||
|
||||
@ -814,6 +810,17 @@ public abstract class Transaction {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for when a transaction is imported as unconfirmed.
|
||||
* <p>
|
||||
* Called after transaction is added to repository, but before commit.
|
||||
* <p>
|
||||
* Blockchain lock is being held during this time.
|
||||
*/
|
||||
protected void onImportAsUnconfirmed() throws DataException {
|
||||
/* To be optionally overridden */
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether transaction can be added to the blockchain.
|
||||
* <p>
|
||||
|
@ -0,0 +1,108 @@
|
||||
package org.qortal.transform.transaction;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.PresenceTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.transaction.PresenceTransaction.PresenceType;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.utils.Serialization;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
public class PresenceTransactionTransformer extends TransactionTransformer {
|
||||
|
||||
// Property lengths
|
||||
private static final int NONCE_LENGTH = INT_LENGTH;
|
||||
private static final int PRESENCE_TYPE_LENGTH = BYTE_LENGTH;
|
||||
private static final int TIMESTAMP_SIGNATURE_LENGTH = SIGNATURE_LENGTH;
|
||||
|
||||
private static final int EXTRAS_LENGTH = NONCE_LENGTH + PRESENCE_TYPE_LENGTH + TIMESTAMP_SIGNATURE_LENGTH;
|
||||
|
||||
protected static final TransactionLayout layout;
|
||||
|
||||
static {
|
||||
layout = new TransactionLayout();
|
||||
layout.add("txType: " + TransactionType.PRESENCE.valueString, TransformationType.INT);
|
||||
layout.add("timestamp", TransformationType.TIMESTAMP);
|
||||
layout.add("transaction's groupID", TransformationType.INT);
|
||||
layout.add("reference", TransformationType.SIGNATURE);
|
||||
layout.add("sender's public key", TransformationType.PUBLIC_KEY);
|
||||
layout.add("proof-of-work nonce", TransformationType.INT);
|
||||
layout.add("presence type (reward-share=0, trade-bot=1)", TransformationType.BYTE);
|
||||
layout.add("timestamp-signature", TransformationType.SIGNATURE);
|
||||
layout.add("fee", TransformationType.AMOUNT);
|
||||
layout.add("signature", TransformationType.SIGNATURE);
|
||||
}
|
||||
|
||||
public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
|
||||
long timestamp = byteBuffer.getLong();
|
||||
|
||||
int txGroupId = byteBuffer.getInt();
|
||||
|
||||
byte[] reference = new byte[REFERENCE_LENGTH];
|
||||
byteBuffer.get(reference);
|
||||
|
||||
byte[] senderPublicKey = Serialization.deserializePublicKey(byteBuffer);
|
||||
|
||||
int nonce = byteBuffer.getInt();
|
||||
|
||||
PresenceType presenceType = PresenceType.valueOf(byteBuffer.get());
|
||||
|
||||
byte[] timestampSignature = new byte[SIGNATURE_LENGTH];
|
||||
byteBuffer.get(timestampSignature);
|
||||
|
||||
long fee = byteBuffer.getLong();
|
||||
|
||||
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||
byteBuffer.get(signature);
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, senderPublicKey, fee, signature);
|
||||
|
||||
return new PresenceTransactionData(baseTransactionData, nonce, presenceType, timestampSignature);
|
||||
}
|
||||
|
||||
public static int getDataLength(TransactionData transactionData) {
|
||||
return getBaseLength(transactionData) + EXTRAS_LENGTH;
|
||||
}
|
||||
|
||||
public static byte[] toBytes(TransactionData transactionData) throws TransformationException {
|
||||
try {
|
||||
PresenceTransactionData presenceTransactionData = (PresenceTransactionData) transactionData;
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
transformCommonBytes(transactionData, bytes);
|
||||
|
||||
bytes.write(Ints.toByteArray(presenceTransactionData.getNonce()));
|
||||
|
||||
bytes.write(presenceTransactionData.getPresenceType().value);
|
||||
|
||||
bytes.write(presenceTransactionData.getTimestampSignature());
|
||||
|
||||
bytes.write(Longs.toByteArray(presenceTransactionData.getFee()));
|
||||
|
||||
if (presenceTransactionData.getSignature() != null)
|
||||
bytes.write(presenceTransactionData.getSignature());
|
||||
|
||||
return bytes.toByteArray();
|
||||
} catch (IOException | ClassCastException e) {
|
||||
throw new TransformationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearNonce(byte[] transactionBytes) {
|
||||
int nonceIndex = TYPE_LENGTH + TIMESTAMP_LENGTH + GROUPID_LENGTH + REFERENCE_LENGTH + PUBLIC_KEY_LENGTH;
|
||||
|
||||
transactionBytes[nonceIndex++] = (byte) 0;
|
||||
transactionBytes[nonceIndex++] = (byte) 0;
|
||||
transactionBytes[nonceIndex++] = (byte) 0;
|
||||
transactionBytes[nonceIndex++] = (byte) 0;
|
||||
}
|
||||
|
||||
}
|
133
src/test/java/org/qortal/test/PresenceTests.java
Normal file
133
src/test/java/org/qortal/test/PresenceTests.java
Normal file
@ -0,0 +1,133 @@
|
||||
package org.qortal.test;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.crosschain.BitcoinACCTv1;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
import org.qortal.data.transaction.PresenceTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.test.common.BlockUtils;
|
||||
import org.qortal.test.common.Common;
|
||||
import org.qortal.test.common.TransactionUtils;
|
||||
import org.qortal.transaction.DeployAtTransaction;
|
||||
import org.qortal.transaction.PresenceTransaction;
|
||||
import org.qortal.transaction.PresenceTransaction.PresenceType;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.ValidationResult;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import com.google.common.primitives.Longs;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class PresenceTests extends Common {
|
||||
|
||||
private static final byte[] BITCOIN_PKH = new byte[20];
|
||||
private static final byte[] HASH_OF_SECRET_B = new byte[32];
|
||||
|
||||
private PrivateKeyAccount signer;
|
||||
private Repository repository;
|
||||
|
||||
@Before
|
||||
public void beforeTest() throws DataException {
|
||||
Common.useDefaultSettings();
|
||||
|
||||
this.repository = RepositoryManager.getRepository();
|
||||
this.signer = Common.getTestAccount(this.repository, "bob");
|
||||
|
||||
// We need to create corresponding test trade offer
|
||||
byte[] creationBytes = BitcoinACCTv1.buildQortalAT(this.signer.getAddress(), BITCOIN_PKH, HASH_OF_SECRET_B,
|
||||
0L, 0L,
|
||||
7 * 24 * 60 * 60);
|
||||
|
||||
long txTimestamp = NTP.getTime();
|
||||
byte[] lastReference = this.signer.getLastReference();
|
||||
|
||||
long fee = 0;
|
||||
String name = "QORT-BTC cross-chain trade";
|
||||
String description = "Qortal-Bitcoin cross-chain trade";
|
||||
String atType = "ACCT";
|
||||
String tags = "QORT-BTC ACCT";
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, this.signer.getPublicKey(), fee, null);
|
||||
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, 1L, Asset.QORT);
|
||||
|
||||
Transaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
|
||||
|
||||
fee = deployAtTransaction.calcRecommendedFee();
|
||||
deployAtTransactionData.setFee(fee);
|
||||
|
||||
TransactionUtils.signAndImportValid(this.repository, deployAtTransactionData, this.signer);
|
||||
BlockUtils.mintBlock(this.repository);
|
||||
}
|
||||
|
||||
@After
|
||||
public void afterTest() throws DataException {
|
||||
if (this.repository != null)
|
||||
this.repository.close();
|
||||
|
||||
this.repository = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void validityTests() throws DataException {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
byte[] timestampBytes = Longs.toByteArray(timestamp);
|
||||
|
||||
byte[] timestampSignature = this.signer.sign(timestampBytes);
|
||||
|
||||
assertTrue(isValid(Group.NO_GROUP, this.signer, timestamp, timestampSignature));
|
||||
|
||||
PrivateKeyAccount nonTrader = Common.getTestAccount(repository, "alice");
|
||||
assertFalse(isValid(Group.NO_GROUP, nonTrader, timestamp, timestampSignature));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void newestOnlyTests() throws DataException {
|
||||
long OLDER_TIMESTAMP = System.currentTimeMillis() - 2000L;
|
||||
long NEWER_TIMESTAMP = OLDER_TIMESTAMP + 1000L;
|
||||
|
||||
PresenceTransaction older = buildPresenceTransaction(Group.NO_GROUP, this.signer, OLDER_TIMESTAMP, null);
|
||||
older.computeNonce();
|
||||
TransactionUtils.signAndImportValid(repository, older.getTransactionData(), this.signer);
|
||||
|
||||
assertTrue(this.repository.getTransactionRepository().exists(older.getTransactionData().getSignature()));
|
||||
|
||||
PresenceTransaction newer = buildPresenceTransaction(Group.NO_GROUP, this.signer, NEWER_TIMESTAMP, null);
|
||||
newer.computeNonce();
|
||||
TransactionUtils.signAndImportValid(repository, newer.getTransactionData(), this.signer);
|
||||
|
||||
assertTrue(this.repository.getTransactionRepository().exists(newer.getTransactionData().getSignature()));
|
||||
assertFalse(this.repository.getTransactionRepository().exists(older.getTransactionData().getSignature()));
|
||||
}
|
||||
|
||||
private boolean isValid(int txGroupId, PrivateKeyAccount signer, long timestamp, byte[] timestampSignature) throws DataException {
|
||||
Transaction transaction = buildPresenceTransaction(txGroupId, signer, timestamp, timestampSignature);
|
||||
return transaction.isValidUnconfirmed() == ValidationResult.OK;
|
||||
}
|
||||
|
||||
private PresenceTransaction buildPresenceTransaction(int txGroupId, PrivateKeyAccount signer, long timestamp, byte[] timestampSignature) throws DataException {
|
||||
int nonce = 0;
|
||||
|
||||
byte[] reference = signer.getLastReference();
|
||||
byte[] creatorPublicKey = signer.getPublicKey();
|
||||
long fee = 0L;
|
||||
|
||||
if (timestampSignature == null)
|
||||
timestampSignature = this.signer.sign(Longs.toByteArray(timestamp));
|
||||
|
||||
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, creatorPublicKey, fee, null);
|
||||
PresenceTransactionData transactionData = new PresenceTransactionData(baseTransactionData, nonce, PresenceType.TRADE_BOT, timestampSignature);
|
||||
|
||||
return new PresenceTransaction(this.repository, transactionData);
|
||||
}
|
||||
|
||||
}
|
@ -15,12 +15,18 @@ import org.qortal.test.common.Common;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@ -127,6 +133,131 @@ public class RepositoryTests extends Common {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTrimDeadlock() {
|
||||
ExecutorService executor = Executors.newCachedThreadPool();
|
||||
CountDownLatch readyLatch = new CountDownLatch(1);
|
||||
CountDownLatch updateLatch = new CountDownLatch(1);
|
||||
CountDownLatch syncLatch = new CountDownLatch(1);
|
||||
|
||||
// Open connection 1
|
||||
try (final HSQLDBRepository repository1 = (HSQLDBRepository) RepositoryManager.getRepository()) {
|
||||
// Read AT states trim height
|
||||
int atTrimHeight = repository1.getATRepository().getAtTrimHeight();
|
||||
repository1.discardChanges();
|
||||
|
||||
// Open connection 2
|
||||
try (final HSQLDBRepository repository2 = (HSQLDBRepository) RepositoryManager.getRepository()) {
|
||||
// Read online signatures trim height
|
||||
int onlineSignaturesTrimHeight = repository2.getBlockRepository().getOnlineAccountsSignaturesTrimHeight();
|
||||
repository2.discardChanges();
|
||||
|
||||
Future<Boolean> f2 = executor.submit(() -> {
|
||||
Object trimHeightsLock = extractTrimHeightsLock(repository2);
|
||||
System.out.println(String.format("f2: repository2's trimHeightsLock object: %s", trimHeightsLock));
|
||||
|
||||
// Update online signatures trim height (implicit commit)
|
||||
synchronized (trimHeightsLock) {
|
||||
try {
|
||||
System.out.println("f2: updating online signatures trim height...");
|
||||
// simulate: repository2.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(onlineSignaturesTrimHeight);
|
||||
String updateSql = "UPDATE DatabaseInfo SET online_signatures_trim_height = ?";
|
||||
PreparedStatement pstmt = repository2.prepareStatement(updateSql);
|
||||
pstmt.setInt(1, onlineSignaturesTrimHeight);
|
||||
pstmt.executeUpdate();
|
||||
// But no commit/saveChanges yet to force HSQLDB error
|
||||
|
||||
System.out.println("f2: readyLatch.countDown()");
|
||||
readyLatch.countDown();
|
||||
|
||||
// wait for other thread to be ready to hit sync block
|
||||
System.out.println("f2: waiting for f1 syncLatch...");
|
||||
syncLatch.await();
|
||||
|
||||
// hang on to trimHeightsLock to force other thread to wait (if code is correct), or to fail (if code is faulty)
|
||||
System.out.println("f2: updateLatch.await(<with timeout>)");
|
||||
if (!updateLatch.await(500L, TimeUnit.MILLISECONDS)) { // long enough for other thread to reach synchronized block
|
||||
// wait period expired suggesting no concurrent access, i.e. code is correct
|
||||
System.out.println("f2: updateLatch.await() timed out");
|
||||
|
||||
System.out.println("f2: saveChanges()");
|
||||
repository2.saveChanges();
|
||||
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
|
||||
System.out.println("f2: saveChanges()");
|
||||
repository2.saveChanges();
|
||||
|
||||
// Early exit from wait period suggests concurrent access, i.e. code faulty
|
||||
return Boolean.FALSE;
|
||||
} catch (InterruptedException | SQLException e) {
|
||||
System.out.println("f2: exception: " + e.getMessage());
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
System.out.println("waiting for f2 readyLatch...");
|
||||
readyLatch.await();
|
||||
System.out.println("launching f1...");
|
||||
|
||||
Future<Boolean> f1 = executor.submit(() -> {
|
||||
Object trimHeightsLock = extractTrimHeightsLock(repository1);
|
||||
System.out.println(String.format("f1: repository1's trimHeightsLock object: %s", trimHeightsLock));
|
||||
|
||||
System.out.println("f1: syncLatch.countDown()");
|
||||
syncLatch.countDown();
|
||||
|
||||
// Update AT states trim height (implicit commit)
|
||||
synchronized (trimHeightsLock) {
|
||||
try {
|
||||
System.out.println("f1: updating AT trim height...");
|
||||
// simulate: repository1.getATRepository().setAtTrimHeight(atTrimHeight);
|
||||
String updateSql = "UPDATE DatabaseInfo SET AT_trim_height = ?";
|
||||
PreparedStatement pstmt = repository1.prepareStatement(updateSql);
|
||||
pstmt.setInt(1, atTrimHeight);
|
||||
pstmt.executeUpdate();
|
||||
System.out.println("f1: saveChanges()");
|
||||
repository1.saveChanges();
|
||||
|
||||
System.out.println("f1: updateLatch.countDown()");
|
||||
updateLatch.countDown();
|
||||
|
||||
return Boolean.TRUE;
|
||||
} catch (SQLException e) {
|
||||
System.out.println("f1: exception: " + e.getMessage());
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (Boolean.TRUE != f1.get())
|
||||
fail("concurrency bug - simultaneous update of DatabaseInfo table");
|
||||
|
||||
if (Boolean.TRUE != f2.get())
|
||||
fail("concurrency bug - not synchronized on same object?");
|
||||
} catch (InterruptedException e) {
|
||||
fail("concurrency bug: " + e.getMessage());
|
||||
} catch (ExecutionException e) {
|
||||
fail("concurrency bug: " + e.getMessage());
|
||||
}
|
||||
} catch (DataException e) {
|
||||
fail("database bug");
|
||||
}
|
||||
}
|
||||
|
||||
private static Object extractTrimHeightsLock(HSQLDBRepository repository) {
|
||||
try {
|
||||
Field trimHeightsLockField = repository.getClass().getDeclaredField("trimHeightsLock");
|
||||
trimHeightsLockField.setAccessible(true);
|
||||
return trimHeightsLockField.get(repository);
|
||||
} catch (IllegalArgumentException | NoSuchFieldException | SecurityException | IllegalAccessException e) {
|
||||
fail();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Check that the <i>sub-query</i> used to fetch highest block height is optimized by HSQLDB. */
|
||||
@Test
|
||||
public void testBlockHeightSpeed() throws DataException, SQLException {
|
||||
|
Loading…
Reference in New Issue
Block a user