diff --git a/src/main/java/org/qortal/controller/tradebot/TradeBot.java b/src/main/java/org/qortal/controller/tradebot/TradeBot.java index 904b87b5..88c720b2 100644 --- a/src/main/java/org/qortal/controller/tradebot/TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/TradeBot.java @@ -338,9 +338,15 @@ public class TradeBot implements Listener { private void expireOldOnlineSignatures() { long now = NTP.getTime(); - synchronized (this.pendingOnlineSignatures) { - this.pendingOnlineSignatures.removeIf(onlineTradeData -> onlineTradeData.getTimestamp() <= now); + int removedCount = 0; + synchronized (this.allOnlineByPubkey) { + int preRemoveCount = this.allOnlineByPubkey.size(); + this.allOnlineByPubkey.values().removeIf(onlineTradeData -> onlineTradeData.getTimestamp() <= now); + removedCount = this.allOnlineByPubkey.size() - preRemoveCount; } + + if (removedCount > 0) + LOGGER.trace("Removed {} old online trade signatures", removedCount); } /*package*/ void updatePresence(Repository repository, TradeBotData tradeBotData, CrossChainTradeData tradeData) @@ -359,28 +365,33 @@ public class TradeBot implements Listener { long now = NTP.getTime(); - // Timestamps are considered good for full lifetime... - long expiry = (now + ONLINE_LIFETIME) % ONLINE_LIFETIME; - // ... but refresh if older than half-lifetime - long threshold = (now + ONLINE_LIFETIME / 2) % (ONLINE_LIFETIME / 2); + // Timestamps are considered good for full lifetime, but we'll refresh if older than half-lifetime + long threshold = (now / (ONLINE_LIFETIME / 2)) * (ONLINE_LIFETIME / 2); + long newExpiry = threshold + ONLINE_LIFETIME / 2; ByteArray pubkeyByteArray = ByteArray.of(tradeNativeAccount.getPublicKey()); // If map's timestamp is missing, or too old, use the new timestamp - otherwise use existing timestamp. - long timestamp = ourTimestampsByPubkey.compute(pubkeyByteArray, (k, v) -> (v == null || v <= threshold) ? expiry : v); + synchronized (this.ourTimestampsByPubkey) { + Long currentTimestamp = this.ourTimestampsByPubkey.get(pubkeyByteArray); - // If timestamp hasn't been updated then nothing to do - if (timestamp != expiry) - return; + if (currentTimestamp != null && currentTimestamp > threshold) + // timestamp still good + return; + + this.ourTimestampsByPubkey.put(pubkeyByteArray, newExpiry); + } // Create signature - byte[] signature = tradeNativeAccount.sign(Longs.toByteArray(timestamp)); + byte[] signature = tradeNativeAccount.sign(Longs.toByteArray(newExpiry)); // Add new online info to queue to be broadcast around network - OnlineTradeData onlineTradeData = new OnlineTradeData(timestamp, tradeNativeAccount.getPublicKey(), signature, atAddress); + OnlineTradeData onlineTradeData = new OnlineTradeData(newExpiry, tradeNativeAccount.getPublicKey(), signature, atAddress); this.pendingOnlineSignatures.add(onlineTradeData); this.allOnlineByPubkey.put(pubkeyByteArray, onlineTradeData); rebuildSafeAllOnline(); + + LOGGER.trace("New signed timestamp {} for our online trade {}", newExpiry, atAddress); } private void rebuildSafeAllOnline() { @@ -400,6 +411,8 @@ public class TradeBot implements Listener { this.pendingOnlineSignatures.clear(); } + LOGGER.trace("Broadcasting {} new online trades", safeOnlineSignatures.size()); + OnlineTradesMessage onlineTradesMessage = new OnlineTradesMessage(safeOnlineSignatures); Network.getInstance().broadcast(peer -> onlineTradesMessage); @@ -415,6 +428,13 @@ public class TradeBot implements Listener { List safeOnlineSignatures = List.copyOf(this.safeAllOnlineByPubkey.values()); + if (safeOnlineSignatures.isEmpty()) + return; + + LOGGER.trace("Broadcasting all {} known online trades. Next broadcast timestamp: {}", + safeOnlineSignatures.size(), nextBroadcastTimestamp + ); + GetOnlineTradesMessage getOnlineTradesMessage = new GetOnlineTradesMessage(safeOnlineSignatures); Network.getInstance().broadcast(peer -> getOnlineTradesMessage); } @@ -427,6 +447,8 @@ public class TradeBot implements Listener { List peersOnlineTrades = getOnlineTradesMessage.getOnlineTrades(); Map entriesUnknownToPeer = new HashMap<>(this.safeAllOnlineByPubkey); + int knownCount = entriesUnknownToPeer.size(); + for (OnlineTradeData peersOnlineTrade : peersOnlineTrades) { ByteArray pubkeyByteArray = ByteArray.of(peersOnlineTrade.getPublicKey()); @@ -436,6 +458,10 @@ public class TradeBot implements Listener { entriesUnknownToPeer.remove(pubkeyByteArray); } + LOGGER.trace("Sending {} known \\ {} peers = {} online trades to peer {}", + knownCount, peersOnlineTrades.size(), entriesUnknownToPeer.size() + ); + // Send complement to peer List safeOnlineSignatures = List.copyOf(entriesUnknownToPeer.values()); Message responseMessage = new OnlineTradesMessage(safeOnlineSignatures); @@ -452,7 +478,7 @@ public class TradeBot implements Listener { long now = NTP.getTime(); // Timestamps after this are too far into the future - long futureThreshold = (now % ONLINE_LIFETIME) + ONLINE_LIFETIME + ONLINE_LIFETIME / 2; + long futureThreshold = (now / ONLINE_LIFETIME + 1) * ONLINE_LIFETIME; // Timestamps before this are too far into the past long pastThreshold = now; @@ -464,46 +490,101 @@ public class TradeBot implements Listener { for (OnlineTradeData peersOnlineTrade : peersOnlineTrades) { long timestamp = peersOnlineTrade.getTimestamp(); - if (timestamp < pastThreshold || timestamp > futureThreshold) + // Ignore if timestamp is out of bounds + if (timestamp < pastThreshold || timestamp > futureThreshold) { + if (timestamp < pastThreshold) + LOGGER.trace("Ignoring online trade {} from peer {} as timestamp {} is too old vs {}", + peersOnlineTrade.getAtAddress(), peer, timestamp, pastThreshold + ); + else + LOGGER.trace("Ignoring online trade {} from peer {} as timestamp {} is too new vs {}", + peersOnlineTrade.getAtAddress(), peer, timestamp, pastThreshold + ); + continue; + } ByteArray pubkeyByteArray = ByteArray.of(peersOnlineTrade.getPublicKey()); - // Ignore if we've previously verified this timestamp+publickey combo + // Ignore if we've previously verified this timestamp+publickey combo or sent timestamp is older OnlineTradeData existingTradeData = this.safeAllOnlineByPubkey.get(pubkeyByteArray); - if (existingTradeData != null && existingTradeData.getTimestamp() == timestamp) + if (existingTradeData != null && timestamp <= existingTradeData.getTimestamp()) { + if (timestamp == existingTradeData.getTimestamp()) + LOGGER.trace("Ignoring online trade {} from peer {} as we have verified timestamp {} before", + peersOnlineTrade.getAtAddress(), peer, timestamp + ); + else + LOGGER.trace("Ignoring online trade {} from peer {} as timestamp {} is older than latest {}", + peersOnlineTrade.getAtAddress(), peer, timestamp, existingTradeData.getTimestamp() + ); + continue; + } // Check timestamp signature byte[] timestampSignature = peersOnlineTrade.getSignature(); byte[] timestampBytes = Longs.toByteArray(timestamp); byte[] publicKey = peersOnlineTrade.getPublicKey(); - if (!Crypto.verify(publicKey, timestampSignature, timestampBytes)) + if (!Crypto.verify(publicKey, timestampSignature, timestampBytes)) { + LOGGER.trace("Ignoring online trade {} from peer {} as signature failed to verify", + peersOnlineTrade.getAtAddress(), peer + ); + continue; + } ATData atData = repository.getATRepository().fromATAddress(peersOnlineTrade.getAtAddress()); - if (atData == null || atData.getIsFrozen() || atData.getIsFinished()) + if (atData == null || atData.getIsFrozen() || atData.getIsFinished()) { + if (atData == null) + LOGGER.trace("Ignoring online trade {} from peer {} as AT doesn't exist", + peersOnlineTrade.getAtAddress(), peer + ); + else + LOGGER.trace("Ignoring online trade {} from peer {} as AT is frozen or finished", + peersOnlineTrade.getAtAddress(), peer + ); + continue; + } ByteArray atCodeHash = new ByteArray(atData.getCodeHash()); Supplier acctSupplier = acctSuppliersByCodeHash.get(atCodeHash); - if (acctSupplier == null) + if (acctSupplier == null) { + LOGGER.trace("Ignoring online trade {} from peer {} as AT isn't a known ACCT?", + peersOnlineTrade.getAtAddress(), peer + ); + continue; + } CrossChainTradeData tradeData = acctSupplier.get().populateTradeData(repository, atData); - if (tradeData == null) + if (tradeData == null) { + LOGGER.trace("Ignoring online trade {} from peer {} as trade data not found?", + peersOnlineTrade.getAtAddress(), peer + ); + continue; + } // Convert signer's public key to address form String signerAddress = Crypto.toAddress(publicKey); // Signer's public key (in address form) must match Bob's / Alice's trade public key (in address form) - if (!signerAddress.equals(tradeData.qortalCreatorTradeAddress) && !signerAddress.equals(tradeData.qortalPartnerAddress)) + if (!signerAddress.equals(tradeData.qortalCreatorTradeAddress) && !signerAddress.equals(tradeData.qortalPartnerAddress)) { + LOGGER.trace("Ignoring online trade {} from peer {} as signer isn't Alice or Bob?", + peersOnlineTrade.getAtAddress(), peer + ); + continue; + } // This is new to us this.allOnlineByPubkey.put(pubkeyByteArray, peersOnlineTrade); ++newCount; + + LOGGER.trace("Added online trade {} from peer {} with timestamp {}", + peersOnlineTrade.getAtAddress(), peer, timestamp + ); } } catch (DataException e) { LOGGER.error("Couldn't process ONLINE_TRADES message due to repository issue", e); @@ -514,4 +595,18 @@ public class TradeBot implements Listener { rebuildSafeAllOnline(); } } + + public void bridgePresence(long timestamp, byte[] publicKey, byte[] signature, String atAddress) { + long expiry = (timestamp / ONLINE_LIFETIME + 1) * ONLINE_LIFETIME; + ByteArray pubkeyByteArray = ByteArray.of(publicKey); + + OnlineTradeData fakeOnlineTradeData = new OnlineTradeData(expiry, publicKey, signature, atAddress); + + OnlineTradeData computedOnlineTradeData = this.allOnlineByPubkey.compute(pubkeyByteArray, (k, v) -> (v == null || v.getTimestamp() < expiry) ? fakeOnlineTradeData : v); + + if (computedOnlineTradeData == fakeOnlineTradeData) { + LOGGER.trace("Bridged online trade {} with timestamp {}", atAddress, expiry); + rebuildSafeAllOnline(); + } + } } diff --git a/src/main/java/org/qortal/data/network/OnlineTradeData.java b/src/main/java/org/qortal/data/network/OnlineTradeData.java index d370c3a3..102030e1 100644 --- a/src/main/java/org/qortal/data/network/OnlineTradeData.java +++ b/src/main/java/org/qortal/data/network/OnlineTradeData.java @@ -19,11 +19,11 @@ public class OnlineTradeData { protected OnlineTradeData() { } - public OnlineTradeData(long timestamp, byte[] publicKey, byte[] signature, String address) { + public OnlineTradeData(long timestamp, byte[] publicKey, byte[] signature, String atAddress) { this.timestamp = timestamp; this.publicKey = publicKey; this.signature = signature; - this.atAddress = address; + this.atAddress = atAddress; } public OnlineTradeData(long timestamp, byte[] publicKey) { diff --git a/src/main/java/org/qortal/network/message/Message.java b/src/main/java/org/qortal/network/message/Message.java index 1ce45aca..ae7e68c9 100644 --- a/src/main/java/org/qortal/network/message/Message.java +++ b/src/main/java/org/qortal/network/message/Message.java @@ -95,8 +95,9 @@ public abstract class Message { ARBITRARY_SIGNATURES(130), - GET_ONLINE_TRADES(140), - ONLINE_TRADES(141); + ONLINE_TRADES(140), + GET_ONLINE_TRADES(141), + ; public final int value; public final Method fromByteBufferMethod; diff --git a/src/main/java/org/qortal/network/message/OnlineTradesMessage.java b/src/main/java/org/qortal/network/message/OnlineTradesMessage.java index 4feef670..74912d8e 100644 --- a/src/main/java/org/qortal/network/message/OnlineTradesMessage.java +++ b/src/main/java/org/qortal/network/message/OnlineTradesMessage.java @@ -4,6 +4,7 @@ import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; import org.qortal.data.network.OnlineTradeData; import org.qortal.transform.Transformer; +import org.qortal.utils.Base58; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -53,11 +54,11 @@ public class OnlineTradesMessage extends Message { byte[] signature = new byte[Transformer.SIGNATURE_LENGTH]; bytes.get(signature); - byte[] addressBytes = new byte[Transformer.ADDRESS_LENGTH]; - bytes.get(addressBytes); - String address = new String(addressBytes, StandardCharsets.UTF_8); + byte[] atAddressBytes = new byte[Transformer.ADDRESS_LENGTH]; + bytes.get(atAddressBytes); + String atAddress = Base58.encode(atAddressBytes); - onlineTrades.add(new OnlineTradeData(timestamp, publicKey, signature, address)); + onlineTrades.add(new OnlineTradeData(timestamp, publicKey, signature, atAddress)); } if (bytes.hasRemaining()) { @@ -108,7 +109,7 @@ public class OnlineTradesMessage extends Message { bytes.write(onlineTradeData.getSignature()); - bytes.write(onlineTradeData.getAtAddress().getBytes(StandardCharsets.UTF_8)); + bytes.write(Base58.decode(onlineTradeData.getAtAddress())); } } } diff --git a/src/main/java/org/qortal/transaction/PresenceTransaction.java b/src/main/java/org/qortal/transaction/PresenceTransaction.java index 0d28d382..566c6979 100644 --- a/src/main/java/org/qortal/transaction/PresenceTransaction.java +++ b/src/main/java/org/qortal/transaction/PresenceTransaction.java @@ -13,6 +13,7 @@ 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.controller.tradebot.TradeBot; import org.qortal.crosschain.ACCT; import org.qortal.crosschain.SupportedBlockchain; import org.qortal.crypto.Crypto; @@ -191,12 +192,16 @@ public class PresenceTransaction extends Transaction { CrossChainTradeData crossChainTradeData = acctSupplier.get().populateTradeData(repository, atData); // OK if signer's public key (in address form) matches Bob's trade public key (in address form) - if (signerAddress.equals(crossChainTradeData.qortalCreatorTradeAddress)) + if (signerAddress.equals(crossChainTradeData.qortalCreatorTradeAddress)) { + TradeBot.getInstance().bridgePresence(this.presenceTransactionData.getTimestamp(), this.transactionData.getCreatorPublicKey(), timestampSignature, atData.getATAddress()); return ValidationResult.OK; + } // OK if signer's public key (in address form) matches Alice's trade public key (in address form) - if (signerAddress.equals(crossChainTradeData.qortalPartnerAddress)) + if (signerAddress.equals(crossChainTradeData.qortalPartnerAddress)) { + TradeBot.getInstance().bridgePresence(this.presenceTransactionData.getTimestamp(), this.transactionData.getCreatorPublicKey(), timestampSignature, atData.getATAddress()); return ValidationResult.OK; + } } return ValidationResult.AT_UNKNOWN;