diff --git a/common/src/main/java/bisq/common/proto/network/NetworkProtoResolver.java b/common/src/main/java/bisq/common/proto/network/NetworkProtoResolver.java index fcbe82760a2..8df36611d58 100644 --- a/common/src/main/java/bisq/common/proto/network/NetworkProtoResolver.java +++ b/common/src/main/java/bisq/common/proto/network/NetworkProtoResolver.java @@ -20,6 +20,8 @@ import bisq.common.proto.ProtoResolver; import bisq.common.proto.ProtobufferException; +import java.time.Clock; + public interface NetworkProtoResolver extends ProtoResolver { NetworkEnvelope fromProto(protobuf.NetworkEnvelope proto) throws ProtobufferException; @@ -27,4 +29,6 @@ public interface NetworkProtoResolver extends ProtoResolver { NetworkPayload fromProto(protobuf.StoragePayload proto); NetworkPayload fromProto(protobuf.StorageEntryWrapper proto); + + Clock getClock(); } diff --git a/core/src/main/java/bisq/core/proto/CoreProtoResolver.java b/core/src/main/java/bisq/core/proto/CoreProtoResolver.java index 4799786b7db..b0ce384192b 100644 --- a/core/src/main/java/bisq/core/proto/CoreProtoResolver.java +++ b/core/src/main/java/bisq/core/proto/CoreProtoResolver.java @@ -59,10 +59,16 @@ import bisq.common.proto.ProtobufferRuntimeException; import bisq.common.proto.persistable.PersistableEnvelope; +import java.time.Clock; + +import lombok.Getter; import lombok.extern.slf4j.Slf4j; @Slf4j public class CoreProtoResolver implements ProtoResolver { + @Getter + protected Clock clock; + @Override public PaymentAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { if (proto != null) { diff --git a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java index 91791a5ba39..c44ce44e80e 100644 --- a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java +++ b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java @@ -88,6 +88,8 @@ import javax.inject.Inject; import javax.inject.Singleton; +import java.time.Clock; + import lombok.extern.slf4j.Slf4j; // TODO Use ProtobufferException instead of ProtobufferRuntimeException @@ -95,7 +97,8 @@ @Singleton public class CoreNetworkProtoResolver extends CoreProtoResolver implements NetworkProtoResolver { @Inject - public CoreNetworkProtoResolver() { + public CoreNetworkProtoResolver(Clock clock) { + this.clock = clock; } @Override diff --git a/monitor/src/main/java/bisq/monitor/metric/P2PNetworkLoad.java b/monitor/src/main/java/bisq/monitor/metric/P2PNetworkLoad.java index d72a68767c5..5ae4dc61ac1 100644 --- a/monitor/src/main/java/bisq/monitor/metric/P2PNetworkLoad.java +++ b/monitor/src/main/java/bisq/monitor/metric/P2PNetworkLoad.java @@ -49,6 +49,8 @@ import org.springframework.core.env.PropertySource; +import java.time.Clock; + import java.io.File; import java.util.Collections; @@ -118,7 +120,7 @@ protected void execute() { // start the network node networkNode = new TorNetworkNode(Integer.parseInt(configuration.getProperty(TOR_PROXY_PORT, "9053")), - new CoreNetworkProtoResolver(), false, + new CoreNetworkProtoResolver(Clock.systemDefaultZone()), false, new AvailableTor(Monitor.TOR_WORKING_DIR, torHiddenServiceDir.getName())); networkNode.start(this); @@ -139,7 +141,7 @@ public String getProperty(String name) { }); CorruptedDatabaseFilesHandler corruptedDatabaseFilesHandler = new CorruptedDatabaseFilesHandler(); int maxConnections = Integer.parseInt(configuration.getProperty(MAX_CONNECTIONS, "12")); - NetworkProtoResolver networkProtoResolver = new CoreNetworkProtoResolver(); + NetworkProtoResolver networkProtoResolver = new CoreNetworkProtoResolver(Clock.systemDefaultZone()); CorePersistenceProtoResolver persistenceProtoResolver = new CorePersistenceProtoResolver(null, networkProtoResolver, storageDir, corruptedDatabaseFilesHandler); DefaultSeedNodeRepository seedNodeRepository = new DefaultSeedNodeRepository(environment, null); diff --git a/monitor/src/main/java/bisq/monitor/metric/P2PSeedNodeSnapshotBase.java b/monitor/src/main/java/bisq/monitor/metric/P2PSeedNodeSnapshotBase.java index 2b7fb9097b3..c47820d06e5 100644 --- a/monitor/src/main/java/bisq/monitor/metric/P2PSeedNodeSnapshotBase.java +++ b/monitor/src/main/java/bisq/monitor/metric/P2PSeedNodeSnapshotBase.java @@ -39,6 +39,8 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.SettableFuture; +import java.time.Clock; + import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -90,7 +92,7 @@ public P2PSeedNodeSnapshotBase(Reporter reporter) { protected void execute() { // start the network node final NetworkNode networkNode = new TorNetworkNode(Integer.parseInt(configuration.getProperty(TOR_PROXY_PORT, "9054")), - new CoreNetworkProtoResolver(), false, + new CoreNetworkProtoResolver(Clock.systemDefaultZone()), false, new AvailableTor(Monitor.TOR_WORKING_DIR, "unused")); // we do not need to start the networkNode, as we do not need the HS //networkNode.start(this); diff --git a/p2p/src/main/java/bisq/network/p2p/P2PService.java b/p2p/src/main/java/bisq/network/p2p/P2PService.java index f0a1fcddb75..d90fdeb2565 100644 --- a/p2p/src/main/java/bisq/network/p2p/P2PService.java +++ b/p2p/src/main/java/bisq/network/p2p/P2PService.java @@ -700,12 +700,12 @@ public void onBroadcastFailed(String errorMessage) { }; boolean result = p2PDataStorage.addProtectedStorageEntry(protectedMailboxStorageEntry, networkNode.getNodeAddress(), listener, true); if (!result) { - //TODO remove and add again with a delay to ensure the data will be broadcasted - // The p2PDataStorage.remove makes probably sense but need to be analysed more. - // Don't change that if it is not 100% clear. sendMailboxMessageListener.onFault("Data already exists in our local database"); - boolean removeResult = p2PDataStorage.remove(protectedMailboxStorageEntry, networkNode.getNodeAddress(), true); - log.debug("remove result=" + removeResult); + + // This should only fail if there are concurrent calls to addProtectedStorageEntry with the + // same ProtectedMailboxStorageEntry. This is an unexpected use case so if it happens we + // want to see it, but it is not worth throwing an exception. + log.error("Unexpected state: adding mailbox message that already exists."); } } catch (CryptoException e) { log.error("Signing at getDataWithSignedSeqNr failed. That should never happen."); @@ -759,7 +759,7 @@ private void delayedRemoveEntryFromMailbox(DecryptedMessageWithPubKey decryptedM expirableMailboxStoragePayload, keyRing.getSignatureKeyPair(), receiversPubKey); - p2PDataStorage.removeMailboxData(protectedMailboxStorageEntry, networkNode.getNodeAddress(), true); + p2PDataStorage.remove(protectedMailboxStorageEntry, networkNode.getNodeAddress(), true); } catch (CryptoException e) { log.error("Signing at getDataWithSignedSeqNr failed. That should never happen."); } diff --git a/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java b/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java index 1d911896025..04313763d17 100644 --- a/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java +++ b/p2p/src/main/java/bisq/network/p2p/storage/P2PDataStorage.java @@ -67,13 +67,12 @@ import com.google.common.annotations.VisibleForTesting; -import org.apache.commons.lang3.StringUtils; - import java.security.KeyPair; import java.security.PublicKey; import java.time.Clock; +import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.HashMap; @@ -98,7 +97,8 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers /** * How many days to keep an entry before it is purged. */ - private static final int PURGE_AGE_DAYS = 10; + @VisibleForTesting + public static final int PURGE_AGE_DAYS = 10; @VisibleForTesting public static int CHECK_TTL_INTERVAL_SEC = 60; @@ -121,6 +121,8 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers private final Set protectedDataStoreListeners = new CopyOnWriteArraySet<>(); private final Clock clock; + protected int maxSequenceNumberMapSizeBeforePurge; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @@ -145,6 +147,7 @@ public P2PDataStorage(NetworkNode networkNode, this.sequenceNumberMapStorage = sequenceNumberMapStorage; sequenceNumberMapStorage.setNumMaxBackupFiles(5); + this.maxSequenceNumberMapSizeBeforePurge = 1000; } @Override @@ -175,40 +178,37 @@ public void shutDown() { removeExpiredEntriesTimer.stop(); } - public void onBootstrapComplete() { - removeExpiredEntriesTimer = UserThread.runPeriodically(() -> { - log.trace("removeExpiredEntries"); - // The moment when an object becomes expired will not be synchronous in the network and we could - // get add network_messages after the object has expired. To avoid repeated additions of already expired - // object when we get it sent from new peers, we don’t remove the sequence number from the map. - // That way an ADD message for an already expired data will fail because the sequence number - // is equal and not larger as expected. - Map temp = new HashMap<>(map); - Set toRemoveSet = new HashSet<>(); - temp.entrySet().stream() - .filter(entry -> entry.getValue().isExpired()) - .forEach(entry -> { - ByteArray hashOfPayload = entry.getKey(); - ProtectedStorageEntry protectedStorageEntry = map.get(hashOfPayload); - if (!(protectedStorageEntry.getProtectedStoragePayload() instanceof PersistableNetworkPayload)) { - toRemoveSet.add(protectedStorageEntry); - log.debug("We found an expired data entry. We remove the protectedData:\n\t" + Utilities.toTruncatedString(protectedStorageEntry)); - map.remove(hashOfPayload); - } - }); + @VisibleForTesting + void removeExpiredEntries() { + log.trace("removeExpiredEntries"); + // The moment when an object becomes expired will not be synchronous in the network and we could + // get add network_messages after the object has expired. To avoid repeated additions of already expired + // object when we get it sent from new peers, we don’t remove the sequence number from the map. + // That way an ADD message for an already expired data will fail because the sequence number + // is equal and not larger as expected. + ArrayList> toRemoveList = + map.entrySet().stream() + .filter(entry -> entry.getValue().isExpired(this.clock)) + .collect(Collectors.toCollection(ArrayList::new)); + + // Batch processing can cause performance issues, so we give listeners a chance to deal with it by notifying + // about start and end of iteration. + hashMapChangedListeners.forEach(HashMapChangedListener::onBatchRemoveExpiredDataStarted); + toRemoveList.forEach(mapEntry -> { + ProtectedStorageEntry protectedStorageEntry = mapEntry.getValue(); + ByteArray payloadHash = mapEntry.getKey(); + + log.debug("We found an expired data entry. We remove the protectedData:\n\t" + Utilities.toTruncatedString(protectedStorageEntry)); + removeFromMapAndDataStore(protectedStorageEntry, payloadHash); + }); + hashMapChangedListeners.forEach(HashMapChangedListener::onBatchRemoveExpiredDataCompleted); - // Batch processing can cause performance issues, so we give listeners a chance to deal with it by notifying - // about start and end of iteration. - hashMapChangedListeners.forEach(HashMapChangedListener::onBatchRemoveExpiredDataStarted); - toRemoveSet.forEach(protectedStorageEntry -> { - hashMapChangedListeners.forEach(l -> l.onRemoved(protectedStorageEntry)); - removeFromProtectedDataStore(protectedStorageEntry); - }); - hashMapChangedListeners.forEach(HashMapChangedListener::onBatchRemoveExpiredDataCompleted); + if (sequenceNumberMap.size() > this.maxSequenceNumberMapSizeBeforePurge) + sequenceNumberMap.setMap(getPurgedSequenceNumberMap(sequenceNumberMap.getMap())); + } - if (sequenceNumberMap.size() > 1000) - sequenceNumberMap.setMap(getPurgedSequenceNumberMap(sequenceNumberMap.getMap())); - }, CHECK_TTL_INTERVAL_SEC); + public void onBootstrapComplete() { + removeExpiredEntriesTimer = UserThread.runPeriodically(this::removeExpiredEntries, CHECK_TTL_INTERVAL_SEC); } public Map getAppendOnlyDataStoreMap() { @@ -233,7 +233,7 @@ public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { } else if (networkEnvelope instanceof RemoveDataMessage) { remove(((RemoveDataMessage) networkEnvelope).getProtectedStorageEntry(), peersNodeAddress, false); } else if (networkEnvelope instanceof RemoveMailboxDataMessage) { - removeMailboxData(((RemoveMailboxDataMessage) networkEnvelope).getProtectedMailboxStorageEntry(), peersNodeAddress, false); + remove(((RemoveMailboxDataMessage) networkEnvelope).getProtectedMailboxStorageEntry(), peersNodeAddress, false); } else if (networkEnvelope instanceof RefreshOfferMessage) { refreshTTL((RefreshOfferMessage) networkEnvelope, peersNodeAddress, false); } else if (networkEnvelope instanceof AddPersistableNetworkPayloadMessage) { @@ -283,10 +283,10 @@ public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection // TODO investigate what causes the disconnections. // Usually the are: SOCKET_TIMEOUT ,TERMINATED (EOFException) protectedStorageEntry.backDate(); - if (protectedStorageEntry.isExpired()) { + if (protectedStorageEntry.isExpired(this.clock)) { log.info("We found an expired data entry which we have already back dated. " + "We remove the protectedStoragePayload:\n\t" + Utilities.toTruncatedString(protectedStorageEntry.getProtectedStoragePayload(), 100)); - doRemoveProtectedExpirableData(protectedStorageEntry, hashOfPayload); + removeFromMapAndDataStore(protectedStorageEntry, hashOfPayload); } } else { log.debug("Remove data ignored as we don't have an entry for that data."); @@ -314,33 +314,41 @@ public boolean addPersistableNetworkPayload(PersistableNetworkPayload payload, boolean reBroadcast, boolean checkDate) { log.trace("addPersistableNetworkPayload payload={}", payload); - byte[] hash = payload.getHash(); - if (payload.verifyHashSize()) { - ByteArray hashAsByteArray = new ByteArray(hash); - boolean containsKey = getAppendOnlyDataStoreMap().containsKey(hashAsByteArray); - if (!containsKey || reBroadcast) { - if (!(payload instanceof DateTolerantPayload) || !checkDate || ((DateTolerantPayload) payload).isDateInTolerance(clock)) { - if (!containsKey) { - appendOnlyDataStoreService.put(hashAsByteArray, payload); - appendOnlyDataStoreListeners.forEach(e -> e.onAdded(payload)); - } - if (allowBroadcast) - broadcaster.broadcast(new AddPersistableNetworkPayloadMessage(payload), sender, null, isDataOwner); - - return true; - } else { - log.warn("Publish date of payload is not matching our current time and outside of our tolerance.\n" + - "Payload={}; now={}", payload.toString(), new Date()); - return false; - } - } else { - log.trace("We have that payload already in our map."); - return false; - } - } else { - log.warn("We got a hash exceeding our permitted size"); + + // Payload hash size does not match expectation for that type of message. + if (!payload.verifyHashSize()) { + log.warn("addPersistableNetworkPayload failed due to unexpected hash size"); return false; } + + ByteArray hashAsByteArray = new ByteArray(payload.getHash()); + boolean payloadHashAlreadyInStore = getAppendOnlyDataStoreMap().containsKey(hashAsByteArray); + + // Store already knows about this payload. Ignore it unless the caller specifically requests a republish. + if (payloadHashAlreadyInStore && !reBroadcast) { + log.trace("addPersistableNetworkPayload failed due to duplicate payload"); + return false; + } + + // DateTolerantPayloads are only checked for tolerance from the onMessage handler (checkDate == true). If not in + // tolerance, ignore it. + if (checkDate && payload instanceof DateTolerantPayload && !((DateTolerantPayload) payload).isDateInTolerance((clock))) { + log.warn("addPersistableNetworkPayload failed due to payload time outside tolerance.\n" + + "Payload={}; now={}", payload.toString(), new Date()); + return false; + } + + // Add the payload and publish the state update to the appendOnlyDataStoreListeners + if (!payloadHashAlreadyInStore) { + appendOnlyDataStoreService.put(hashAsByteArray, payload); + appendOnlyDataStoreListeners.forEach(e -> e.onAdded(payload)); + } + + // Broadcast the payload if requested by caller + if (allowBroadcast) + broadcaster.broadcast(new AddPersistableNetworkPayloadMessage(payload), sender, null, isDataOwner); + + return true; } // When we receive initial data we skip several checks to improve performance. We requested only missing entries so we @@ -380,50 +388,41 @@ public boolean addProtectedStorageEntry(ProtectedStorageEntry protectedStorageEn return false; } - boolean sequenceNrValid = isSequenceNrValid(protectedStorageEntry.getSequenceNumber(), hashOfPayload); - boolean result = sequenceNrValid && - checkPublicKeys(protectedStorageEntry, true) - && checkSignature(protectedStorageEntry); + // If we have seen a more recent operation for this payload, we ignore the current one + if(!hasSequenceNrIncreased(protectedStorageEntry.getSequenceNumber(), hashOfPayload)) + return false; + + // Verify the ProtectedStorageEntry is well formed and valid for the add operation + if (!protectedStorageEntry.isValidForAddOperation()) + return false; - boolean containsKey = map.containsKey(hashOfPayload); - if (containsKey) { - result = result && checkIfStoredDataPubKeyMatchesNewDataPubKey(protectedStorageEntry.getOwnerPubKey(), hashOfPayload); - } + ProtectedStorageEntry storedEntry = map.get(hashOfPayload); - // printData("before add"); - if (result) { - boolean hasSequenceNrIncreased = hasSequenceNrIncreased(protectedStorageEntry.getSequenceNumber(), hashOfPayload); + // If we have already seen an Entry with the same hash, verify the metadata is equal + if (storedEntry != null && !protectedStorageEntry.matchesRelevantPubKey(storedEntry)) + return false; - if (!containsKey || hasSequenceNrIncreased) { - // At startup we don't have the item so we store it. At updates of the seq nr we store as well. - map.put(hashOfPayload, protectedStorageEntry); - hashMapChangedListeners.forEach(e -> e.onAdded(protectedStorageEntry)); - // printData("after add"); - } else { - log.trace("We got that version of the data already, so we don't store it."); - } + // This is an updated entry. Record it and signal listeners. + map.put(hashOfPayload, protectedStorageEntry); + hashMapChangedListeners.forEach(e -> e.onAdded(protectedStorageEntry)); - if (hasSequenceNrIncreased) { - sequenceNumberMap.put(hashOfPayload, new MapValue(protectedStorageEntry.getSequenceNumber(), System.currentTimeMillis())); - // We set the delay higher as we might receive a batch of items - sequenceNumberMapStorage.queueUpForSave(SequenceNumberMap.clone(sequenceNumberMap), 2000); + // Record the updated sequence number and persist it. Higher delay so we can batch more items. + sequenceNumberMap.put(hashOfPayload, new MapValue(protectedStorageEntry.getSequenceNumber(), this.clock.millis())); + sequenceNumberMapStorage.queueUpForSave(SequenceNumberMap.clone(sequenceNumberMap), 2000); - if (allowBroadcast) - broadcastProtectedStorageEntry(protectedStorageEntry, sender, listener, isDataOwner); - } else { - log.trace("We got that version of the data already, so we don't broadcast it."); - } + // Optionally, broadcast the add/update depending on the calling environment + if (allowBroadcast) + broadcastProtectedStorageEntry(protectedStorageEntry, sender, listener, isDataOwner); - if (protectedStoragePayload instanceof PersistablePayload) { - ByteArray compactHash = getCompactHashAsByteArray(protectedStoragePayload); - ProtectedStorageEntry previous = protectedDataStoreService.putIfAbsent(compactHash, protectedStorageEntry); - if (previous == null) - protectedDataStoreListeners.forEach(e -> e.onAdded(protectedStorageEntry)); - } - } else { - log.trace("add failed"); + // Persist ProtectedStorageEntrys carrying PersistablePayload payloads and signal listeners on changes + if (protectedStoragePayload instanceof PersistablePayload) { + ByteArray compactHash = P2PDataStorage.getCompactHashAsByteArray(protectedStoragePayload); + ProtectedStorageEntry previous = protectedDataStoreService.putIfAbsent(compactHash, protectedStorageEntry); + if (previous == null) + protectedDataStoreListeners.forEach(e -> e.onAdded(protectedStorageEntry)); } - return result; + + return true; } private void broadcastProtectedStorageEntry(ProtectedStorageEntry protectedStorageEntry, @@ -436,40 +435,44 @@ private void broadcastProtectedStorageEntry(ProtectedStorageEntry protectedStora public boolean refreshTTL(RefreshOfferMessage refreshTTLMessage, @Nullable NodeAddress sender, boolean isDataOwner) { - ByteArray hashOfPayload = new ByteArray(refreshTTLMessage.getHashOfPayload()); - if (map.containsKey(hashOfPayload)) { - ProtectedStorageEntry storedData = map.get(hashOfPayload); - int sequenceNumber = refreshTTLMessage.getSequenceNumber(); - if (sequenceNumberMap.containsKey(hashOfPayload) && sequenceNumberMap.get(hashOfPayload).sequenceNr == sequenceNumber) { - log.trace("We got that message with that seq nr already from another peer. We ignore that message."); - return true; - } else { - PublicKey ownerPubKey = storedData.getProtectedStoragePayload().getOwnerPubKey(); - byte[] hashOfDataAndSeqNr = refreshTTLMessage.getHashOfDataAndSeqNr(); - byte[] signature = refreshTTLMessage.getSignature(); - // printData("before refreshTTL"); - if (hasSequenceNrIncreased(sequenceNumber, hashOfPayload) && - checkIfStoredDataPubKeyMatchesNewDataPubKey(ownerPubKey, hashOfPayload) && - checkSignature(ownerPubKey, hashOfDataAndSeqNr, signature)) { - log.debug("refreshDate called for storedData:\n\t" + StringUtils.abbreviate(storedData.toString(), 100)); - storedData.refreshTTL(); - storedData.updateSequenceNumber(sequenceNumber); - storedData.updateSignature(signature); - printData("after refreshTTL"); - sequenceNumberMap.put(hashOfPayload, new MapValue(sequenceNumber, System.currentTimeMillis())); - sequenceNumberMapStorage.queueUpForSave(SequenceNumberMap.clone(sequenceNumberMap), 1000); - - broadcast(refreshTTLMessage, sender, null, isDataOwner); - return true; - } + ByteArray hashOfPayload = new ByteArray(refreshTTLMessage.getHashOfPayload()); + ProtectedStorageEntry storedData = map.get(hashOfPayload); - return false; - } - } else { + if (storedData == null) { log.debug("We don't have data for that refresh message in our map. That is expected if we missed the data publishing."); + return false; } + + ProtectedStorageEntry storedEntry = map.get(hashOfPayload); + ProtectedStorageEntry updatedEntry = new ProtectedStorageEntry( + storedEntry.getProtectedStoragePayload(), + storedEntry.getOwnerPubKey(), + refreshTTLMessage.getSequenceNumber(), + refreshTTLMessage.getSignature(), + this.clock); + + + // If we have seen a more recent operation for this payload, we ignore the current one + if(!hasSequenceNrIncreased(updatedEntry.getSequenceNumber(), hashOfPayload)) + return false; + + // Verify the updated ProtectedStorageEntry is well formed and valid for update + if (!updatedEntry.isValidForAddOperation()) + return false; + + // Update the hash map with the updated entry + map.put(hashOfPayload, updatedEntry); + + // Record the latest sequence number and persist it + sequenceNumberMap.put(hashOfPayload, new MapValue(updatedEntry.getSequenceNumber(), this.clock.millis())); + sequenceNumberMapStorage.queueUpForSave(SequenceNumberMap.clone(sequenceNumberMap), 1000); + + // Always broadcast refreshes + broadcast(refreshTTLMessage, sender, null, isDataOwner); + + return true; } public boolean remove(ProtectedStorageEntry protectedStorageEntry, @@ -477,32 +480,44 @@ public boolean remove(ProtectedStorageEntry protectedStorageEntry, boolean isDataOwner) { ProtectedStoragePayload protectedStoragePayload = protectedStorageEntry.getProtectedStoragePayload(); ByteArray hashOfPayload = get32ByteHashAsByteArray(protectedStoragePayload); - boolean containsKey = map.containsKey(hashOfPayload); - if (!containsKey) + + // If we don't know about the target of this remove, ignore it + ProtectedStorageEntry storedEntry = map.get(hashOfPayload); + if (storedEntry == null) { log.debug("Remove data ignored as we don't have an entry for that data."); - boolean result = containsKey - && checkPublicKeys(protectedStorageEntry, false) - && isSequenceNrValid(protectedStorageEntry.getSequenceNumber(), hashOfPayload) - && checkSignature(protectedStorageEntry) - && checkIfStoredDataPubKeyMatchesNewDataPubKey(protectedStorageEntry.getOwnerPubKey(), hashOfPayload); + return false; + } + + // If we have seen a more recent operation for this payload, ignore this one + if (!hasSequenceNrIncreased(protectedStorageEntry.getSequenceNumber(), hashOfPayload)) + return false; - // printData("before remove"); - if (result) { - doRemoveProtectedExpirableData(protectedStorageEntry, hashOfPayload); - printData("after remove"); - sequenceNumberMap.put(hashOfPayload, new MapValue(protectedStorageEntry.getSequenceNumber(), System.currentTimeMillis())); - sequenceNumberMapStorage.queueUpForSave(SequenceNumberMap.clone(sequenceNumberMap), 300); + // Verify the ProtectedStorageEntry is well formed and valid for the remove operation + if (!protectedStorageEntry.isValidForRemoveOperation()) + return false; - maybeAddToRemoveAddOncePayloads(protectedStoragePayload, hashOfPayload); + // If we have already seen an Entry with the same hash, verify the metadata is the same + if (!protectedStorageEntry.matchesRelevantPubKey(storedEntry)) + return false; - broadcast(new RemoveDataMessage(protectedStorageEntry), sender, null, isDataOwner); + // Valid remove entry, do the remove and signal listeners + removeFromMapAndDataStore(protectedStorageEntry, hashOfPayload); + printData("after remove"); + + // Record the latest sequence number and persist it + sequenceNumberMap.put(hashOfPayload, new MapValue(protectedStorageEntry.getSequenceNumber(), this.clock.millis())); + sequenceNumberMapStorage.queueUpForSave(SequenceNumberMap.clone(sequenceNumberMap), 300); - removeFromProtectedDataStore(protectedStorageEntry); + maybeAddToRemoveAddOncePayloads(protectedStoragePayload, hashOfPayload); + + if (protectedStorageEntry instanceof ProtectedMailboxStorageEntry) { + broadcast(new RemoveMailboxDataMessage((ProtectedMailboxStorageEntry) protectedStorageEntry), sender, null, isDataOwner); } else { - log.debug("remove failed"); + broadcast(new RemoveDataMessage(protectedStorageEntry), sender, null, isDataOwner); } - return result; - } + + return true; +} /** @@ -522,8 +537,7 @@ public void removeInvalidProtectedStorageEntry(ProtectedStorageEntry protectedSt return; } - doRemoveProtectedExpirableData(protectedStorageEntry, hashOfPayload); - removeFromProtectedDataStore(protectedStorageEntry); + removeFromMapAndDataStore(protectedStorageEntry, hashOfPayload); // We do not update the sequence number as that method is only called if we have received an invalid // protectedStorageEntry from a previous add operation. @@ -536,54 +550,6 @@ public void removeInvalidProtectedStorageEntry(ProtectedStorageEntry protectedSt // source (network). } - private void removeFromProtectedDataStore(ProtectedStorageEntry protectedStorageEntry) { - ProtectedStoragePayload protectedStoragePayload = protectedStorageEntry.getProtectedStoragePayload(); - if (protectedStoragePayload instanceof PersistablePayload) { - ByteArray compactHash = getCompactHashAsByteArray(protectedStoragePayload); - ProtectedStorageEntry previous = protectedDataStoreService.remove(compactHash, protectedStorageEntry); - if (previous != null) { - protectedDataStoreListeners.forEach(e -> e.onRemoved(protectedStorageEntry)); - } else { - log.info("We cannot remove the protectedStorageEntry from the persistedEntryMap as it does not exist."); - } - } - } - - @SuppressWarnings("UnusedReturnValue") - public boolean removeMailboxData(ProtectedMailboxStorageEntry protectedMailboxStorageEntry, - @Nullable NodeAddress sender, - boolean isDataOwner) { - ProtectedStoragePayload protectedStoragePayload = protectedMailboxStorageEntry.getProtectedStoragePayload(); - ByteArray hashOfPayload = get32ByteHashAsByteArray(protectedStoragePayload); - boolean containsKey = map.containsKey(hashOfPayload); - if (!containsKey) - log.debug("Remove data ignored as we don't have an entry for that data."); - - int sequenceNumber = protectedMailboxStorageEntry.getSequenceNumber(); - PublicKey receiversPubKey = protectedMailboxStorageEntry.getReceiversPubKey(); - boolean result = containsKey && - isSequenceNrValid(sequenceNumber, hashOfPayload) && - checkPublicKeys(protectedMailboxStorageEntry, false) && - protectedMailboxStorageEntry.getMailboxStoragePayload().getOwnerPubKey().equals(receiversPubKey) && // at remove both keys are the same (only receiver is able to remove data) - checkSignature(protectedMailboxStorageEntry) && - checkIfStoredMailboxDataMatchesNewMailboxData(receiversPubKey, hashOfPayload); - - // printData("before removeMailboxData"); - if (result) { - doRemoveProtectedExpirableData(protectedMailboxStorageEntry, hashOfPayload); - printData("after removeMailboxData"); - sequenceNumberMap.put(hashOfPayload, new MapValue(sequenceNumber, System.currentTimeMillis())); - sequenceNumberMapStorage.queueUpForSave(SequenceNumberMap.clone(sequenceNumberMap), 300); - - maybeAddToRemoveAddOncePayloads(protectedStoragePayload, hashOfPayload); - - broadcast(new RemoveMailboxDataMessage(protectedMailboxStorageEntry), sender, null, isDataOwner); - } else { - log.debug("removeMailboxData failed"); - } - return result; - } - private void maybeAddToRemoveAddOncePayloads(ProtectedStoragePayload protectedStoragePayload, ByteArray hashOfData) { if (protectedStoragePayload instanceof AddOncePayload) { @@ -603,7 +569,7 @@ public ProtectedStorageEntry getProtectedStorageEntry(ProtectedStoragePayload pr byte[] hashOfDataAndSeqNr = P2PDataStorage.get32ByteHash(new DataAndSeqNrPair(protectedStoragePayload, sequenceNumber)); byte[] signature = Sig.sign(ownerStoragePubKey.getPrivate(), hashOfDataAndSeqNr); - return new ProtectedStorageEntry(protectedStoragePayload, ownerStoragePubKey.getPublic(), sequenceNumber, signature); + return new ProtectedStorageEntry(protectedStoragePayload, ownerStoragePubKey.getPublic(), sequenceNumber, signature, this.clock); } public RefreshOfferMessage getRefreshTTLMessage(ProtectedStoragePayload protectedStoragePayload, @@ -635,7 +601,7 @@ public ProtectedMailboxStorageEntry getMailboxDataWithSignedSeqNr(MailboxStorage byte[] hashOfDataAndSeqNr = P2PDataStorage.get32ByteHash(new DataAndSeqNrPair(expirableMailboxStoragePayload, sequenceNumber)); byte[] signature = Sig.sign(storageSignaturePubKey.getPrivate(), hashOfDataAndSeqNr); return new ProtectedMailboxStorageEntry(expirableMailboxStoragePayload, - storageSignaturePubKey.getPublic(), sequenceNumber, signature, receiversPublicKey); + storageSignaturePubKey.getPublic(), sequenceNumber, signature, receiversPublicKey, this.clock); } public void addHashMapChangedListener(HashMapChangedListener hashMapChangedListener) { @@ -670,27 +636,19 @@ public void removeProtectedDataStoreListener(ProtectedDataStoreListener listener // Private /////////////////////////////////////////////////////////////////////////////////////////// - private void doRemoveProtectedExpirableData(ProtectedStorageEntry protectedStorageEntry, ByteArray hashOfPayload) { + private void removeFromMapAndDataStore(ProtectedStorageEntry protectedStorageEntry, ByteArray hashOfPayload) { map.remove(hashOfPayload); - log.trace("Data removed from our map. We broadcast the message to our peers."); hashMapChangedListeners.forEach(e -> e.onRemoved(protectedStorageEntry)); - } - private boolean isSequenceNrValid(int newSequenceNumber, ByteArray hashOfData) { - if (sequenceNumberMap.containsKey(hashOfData)) { - int storedSequenceNumber = sequenceNumberMap.get(hashOfData).sequenceNr; - if (newSequenceNumber >= storedSequenceNumber) { - log.trace("Sequence number is valid (>=). sequenceNumber = " - + newSequenceNumber + " / storedSequenceNumber=" + storedSequenceNumber); - return true; + ProtectedStoragePayload protectedStoragePayload = protectedStorageEntry.getProtectedStoragePayload(); + if (protectedStoragePayload instanceof PersistablePayload) { + ByteArray compactHash = getCompactHashAsByteArray(protectedStoragePayload); + ProtectedStorageEntry previous = protectedDataStoreService.remove(compactHash, protectedStorageEntry); + if (previous != null) { + protectedDataStoreListeners.forEach(e -> e.onRemoved(protectedStorageEntry)); } else { - log.debug("Sequence number is invalid. sequenceNumber = " - + newSequenceNumber + " / storedSequenceNumber=" + storedSequenceNumber + "\n" + - "That can happen if the data owner gets an old delayed data storage message."); - return false; + log.info("We cannot remove the protectedStorageEntry from the persistedEntryMap as it does not exist."); } - } else { - return true; } } @@ -723,86 +681,6 @@ private boolean hasSequenceNrIncreased(int newSequenceNumber, ByteArray hashOfDa } } - private boolean checkSignature(PublicKey ownerPubKey, byte[] hashOfDataAndSeqNr, byte[] signature) { - try { - boolean result = Sig.verify(ownerPubKey, hashOfDataAndSeqNr, signature); - if (!result) - log.warn("Signature verification failed at checkSignature. " + - "That should not happen."); - - return result; - } catch (CryptoException e) { - log.error("Signature verification failed at checkSignature"); - return false; - } - } - - private boolean checkSignature(ProtectedStorageEntry protectedStorageEntry) { - byte[] hashOfDataAndSeqNr = P2PDataStorage.get32ByteHash(new DataAndSeqNrPair(protectedStorageEntry.getProtectedStoragePayload(), protectedStorageEntry.getSequenceNumber())); - return checkSignature(protectedStorageEntry.getOwnerPubKey(), hashOfDataAndSeqNr, protectedStorageEntry.getSignature()); - } - - // Check that the pubkey of the storage entry matches the allowed pubkey for the addition or removal operation - // in the contained mailbox message, or the pubKey of other kinds of network_messages. - private boolean checkPublicKeys(ProtectedStorageEntry protectedStorageEntry, boolean isAddOperation) { - boolean result; - ProtectedStoragePayload protectedStoragePayload = protectedStorageEntry.getProtectedStoragePayload(); - if (protectedStoragePayload instanceof MailboxStoragePayload) { - MailboxStoragePayload payload = (MailboxStoragePayload) protectedStoragePayload; - if (isAddOperation) - result = payload.getSenderPubKeyForAddOperation() != null && - payload.getSenderPubKeyForAddOperation().equals(protectedStorageEntry.getOwnerPubKey()); - else - result = payload.getOwnerPubKey() != null && - payload.getOwnerPubKey().equals(protectedStorageEntry.getOwnerPubKey()); - } else { - result = protectedStorageEntry.getOwnerPubKey() != null && - protectedStoragePayload != null && - protectedStorageEntry.getOwnerPubKey().equals(protectedStoragePayload.getOwnerPubKey()); - } - - if (!result) { - String res1 = protectedStorageEntry.toString(); - String res2 = "null"; - if (protectedStoragePayload != null && - protectedStoragePayload.getOwnerPubKey() != null) - res2 = Utilities.encodeToHex(protectedStoragePayload.getOwnerPubKey().getEncoded(), true); - - log.warn("PublicKey of payload data and ProtectedStorageEntry are not matching. protectedStorageEntry=" + res1 + - "protectedStorageEntry.getStoragePayload().getOwnerPubKey()=" + res2); - } - return result; - } - - private boolean checkIfStoredDataPubKeyMatchesNewDataPubKey(PublicKey ownerPubKey, ByteArray hashOfData) { - ProtectedStorageEntry storedData = map.get(hashOfData); - boolean result = storedData.getOwnerPubKey() != null && storedData.getOwnerPubKey().equals(ownerPubKey); - if (!result) - log.warn("New data entry does not match our stored data. storedData.ownerPubKey=" + - (storedData.getOwnerPubKey() != null ? storedData.getOwnerPubKey().toString() : "null") + - ", ownerPubKey=" + ownerPubKey); - - return result; - } - - private boolean checkIfStoredMailboxDataMatchesNewMailboxData(PublicKey receiversPubKey, ByteArray hashOfData) { - ProtectedStorageEntry storedData = map.get(hashOfData); - if (storedData instanceof ProtectedMailboxStorageEntry) { - ProtectedMailboxStorageEntry entry = (ProtectedMailboxStorageEntry) storedData; - // publicKey is not the same (stored: sender, new: receiver) - boolean result = entry.getReceiversPubKey().equals(receiversPubKey) - && get32ByteHashAsByteArray(entry.getProtectedStoragePayload()).equals(hashOfData); - if (!result) - log.warn("New data entry does not match our stored data. entry.receiversPubKey=" + entry.getReceiversPubKey() - + ", receiversPubKey=" + receiversPubKey); - - return result; - } else { - log.error("We expected a MailboxData but got other type. That must never happen. storedData=" + storedData); - return false; - } - } - private void broadcast(BroadcastMessage message, @Nullable NodeAddress sender, @Nullable BroadcastHandler.Listener listener, boolean isDataOwner) { broadcaster.broadcast(message, sender, listener, isDataOwner); @@ -823,7 +701,7 @@ private static byte[] getCompactHash(ProtectedStoragePayload protectedStoragePay // Get a new map with entries older than PURGE_AGE_DAYS purged from the given map. private Map getPurgedSequenceNumberMap(Map persisted) { Map purged = new HashMap<>(); - long maxAgeTs = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(PURGE_AGE_DAYS); + long maxAgeTs = this.clock.millis() - TimeUnit.DAYS.toMillis(PURGE_AGE_DAYS); persisted.forEach((key, value) -> { if (value.timeStamp > maxAgeTs) purged.put(key, value); diff --git a/p2p/src/main/java/bisq/network/p2p/storage/payload/ProtectedMailboxStorageEntry.java b/p2p/src/main/java/bisq/network/p2p/storage/payload/ProtectedMailboxStorageEntry.java index 509f45645f3..feaa7b19dd6 100644 --- a/p2p/src/main/java/bisq/network/p2p/storage/payload/ProtectedMailboxStorageEntry.java +++ b/p2p/src/main/java/bisq/network/p2p/storage/payload/ProtectedMailboxStorageEntry.java @@ -25,6 +25,8 @@ import java.security.PublicKey; +import java.time.Clock; + import lombok.EqualsAndHashCode; import lombok.Value; import lombok.extern.slf4j.Slf4j; @@ -40,38 +42,162 @@ public ProtectedMailboxStorageEntry(MailboxStoragePayload mailboxStoragePayload, PublicKey ownerPubKey, int sequenceNumber, byte[] signature, - PublicKey receiversPubKey) { - super(mailboxStoragePayload, ownerPubKey, sequenceNumber, signature); + PublicKey receiversPubKey, + Clock clock) { + this(mailboxStoragePayload, + Sig.getPublicKeyBytes(ownerPubKey), + ownerPubKey, + sequenceNumber, + signature, + Sig.getPublicKeyBytes(receiversPubKey), + receiversPubKey, + clock.millis(), + clock); + } + + private ProtectedMailboxStorageEntry(MailboxStoragePayload mailboxStoragePayload, + byte[] ownerPubKeyBytes, + PublicKey ownerPubKey, + int sequenceNumber, + byte[] signature, + byte[] receiversPubKeyBytes, + PublicKey receiversPubKey, + long creationTimeStamp, + Clock clock) { + super(mailboxStoragePayload, + ownerPubKeyBytes, + ownerPubKey, + sequenceNumber, + signature, + creationTimeStamp, + clock); this.receiversPubKey = receiversPubKey; - receiversPubKeyBytes = Sig.getPublicKeyBytes(receiversPubKey); + this.receiversPubKeyBytes = receiversPubKeyBytes; } + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + public MailboxStoragePayload getMailboxStoragePayload() { return (MailboxStoragePayload) getProtectedStoragePayload(); } + /* + * Returns true if this Entry is valid for an add operation. For mailbox Entrys, the entry owner must + * match the valid sender Public Key specified in the payload. (Only sender can add) + */ + @Override + public boolean isValidForAddOperation() { + if (!this.isSignatureValid()) + return false; + + MailboxStoragePayload mailboxStoragePayload = this.getMailboxStoragePayload(); + + // Verify the Entry.receiversPubKey matches the Payload.ownerPubKey. This is a requirement for removal + if (!mailboxStoragePayload.getOwnerPubKey().equals(this.receiversPubKey)) { + log.debug("Entry receiversPubKey does not match payload owner which is a requirement for adding MailboxStoragePayloads"); + return false; + } + + boolean result = mailboxStoragePayload.getSenderPubKeyForAddOperation() != null && + mailboxStoragePayload.getSenderPubKeyForAddOperation().equals(this.getOwnerPubKey()); + + if (!result) { + String res1 = this.toString(); + String res2 = "null"; + if (mailboxStoragePayload != null && mailboxStoragePayload.getOwnerPubKey() != null) + res2 = Utilities.encodeToHex(mailboxStoragePayload.getSenderPubKeyForAddOperation().getEncoded(),true); + + log.warn("ProtectedMailboxStorageEntry::isValidForAddOperation() failed. " + + "Entry owner does not match sender key in payload:\nProtectedStorageEntry=%{}\n" + + "SenderPubKeyForAddOperation=%{}", res1, res2); + } + + return result; + } + + /* + * Returns true if the Entry is valid for a remove operation. For mailbox Entrys, the entry owner must + * match the payload owner. (Only receiver can remove) + */ + @Override + public boolean isValidForRemoveOperation() { + if (!this.isSignatureValid()) + return false; + + MailboxStoragePayload mailboxStoragePayload = this.getMailboxStoragePayload(); + + // Verify the Entry has the correct receiversPubKey for removal + if (!mailboxStoragePayload.getOwnerPubKey().equals(this.receiversPubKey)) { + log.debug("Entry receiversPubKey does not match payload owner which is a requirement for removing MailboxStoragePayloads"); + return false; + } + + boolean result = mailboxStoragePayload.getOwnerPubKey() != null && + mailboxStoragePayload.getOwnerPubKey().equals(this.getOwnerPubKey()); + + if (!result) { + String res1 = this.toString(); + String res2 = "null"; + if (mailboxStoragePayload != null && mailboxStoragePayload.getOwnerPubKey() != null) + res2 = Utilities.encodeToHex(mailboxStoragePayload.getOwnerPubKey().getEncoded(), true); + + log.warn("ProtectedMailboxStorageEntry::isValidForRemoveOperation() failed. " + + "Entry owner does not match Payload owner:\nProtectedStorageEntry={}\n" + + "PayloadOwner={}", res1, res2); + } + + return result; + } + + @Override + /* + * Returns true if the Entry metadata that is expected to stay constant between different versions of the same object + * matches. For ProtectedMailboxStorageEntry, the receiversPubKey must stay the same. + */ + public boolean matchesRelevantPubKey(ProtectedStorageEntry protectedStorageEntry) { + if (!(protectedStorageEntry instanceof ProtectedMailboxStorageEntry)) { + log.error("ProtectedMailboxStorageEntry::isMetadataEquals() failed due to object type mismatch. " + + "ProtectedMailboxStorageEntry required, but got\n" + protectedStorageEntry); + + return false; + } + + ProtectedMailboxStorageEntry protectedMailboxStorageEntry = (ProtectedMailboxStorageEntry) protectedStorageEntry; + + boolean result = protectedMailboxStorageEntry.getReceiversPubKey().equals(this.receiversPubKey); + if (!result) { + log.warn("ProtectedMailboxStorageEntry::isMetadataEquals() failed due to metadata mismatch. " + + "new.receiversPubKey=" + Utilities.bytesAsHexString(protectedMailboxStorageEntry.getReceiversPubKeyBytes()) + + "stored.receiversPubKey=" + Utilities.bytesAsHexString(this.getReceiversPubKeyBytes())); + } + + return result; + } + /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// - private ProtectedMailboxStorageEntry(long creationTimeStamp, - MailboxStoragePayload mailboxStoragePayload, - byte[] ownerPubKey, + private ProtectedMailboxStorageEntry(MailboxStoragePayload mailboxStoragePayload, + byte[] ownerPubKeyBytes, int sequenceNumber, byte[] signature, - byte[] receiversPubKeyBytes) { - super(creationTimeStamp, - mailboxStoragePayload, - ownerPubKey, + byte[] receiversPubKeyBytes, + long creationTimeStamp, + Clock clock) { + this(mailboxStoragePayload, + ownerPubKeyBytes, + Sig.getPublicKeyFromBytes(ownerPubKeyBytes), sequenceNumber, - signature); - - this.receiversPubKeyBytes = receiversPubKeyBytes; - receiversPubKey = Sig.getPublicKeyFromBytes(receiversPubKeyBytes); - - maybeAdjustCreationTimeStamp(); + signature, + receiversPubKeyBytes, + Sig.getPublicKeyFromBytes(receiversPubKeyBytes), + creationTimeStamp, + clock); } public protobuf.ProtectedMailboxStorageEntry toProtoMessage() { @@ -85,20 +211,20 @@ public static ProtectedMailboxStorageEntry fromProto(protobuf.ProtectedMailboxSt NetworkProtoResolver resolver) { ProtectedStorageEntry entry = ProtectedStorageEntry.fromProto(proto.getEntry(), resolver); return new ProtectedMailboxStorageEntry( - entry.getCreationTimeStamp(), (MailboxStoragePayload) entry.getProtectedStoragePayload(), entry.getOwnerPubKey().getEncoded(), entry.getSequenceNumber(), entry.getSignature(), - proto.getReceiversPubKeyBytes().toByteArray()); + proto.getReceiversPubKeyBytes().toByteArray(), + entry.getCreationTimeStamp(), + resolver.getClock()); } @Override public String toString() { return "ProtectedMailboxStorageEntry{" + - "\n receiversPubKeyBytes=" + Utilities.bytesAsHexString(receiversPubKeyBytes) + - ",\n receiversPubKey=" + receiversPubKey + - "\n} " + super.toString(); + "\n\tReceivers Public Key: " + Utilities.bytesAsHexString(receiversPubKeyBytes) + + "\n" + super.toString(); } } diff --git a/p2p/src/main/java/bisq/network/p2p/storage/payload/ProtectedStorageEntry.java b/p2p/src/main/java/bisq/network/p2p/storage/payload/ProtectedStorageEntry.java index b967caf594f..1982052bc2b 100644 --- a/p2p/src/main/java/bisq/network/p2p/storage/payload/ProtectedStorageEntry.java +++ b/p2p/src/main/java/bisq/network/p2p/storage/payload/ProtectedStorageEntry.java @@ -17,16 +17,24 @@ package bisq.network.p2p.storage.payload; +import bisq.network.p2p.storage.P2PDataStorage; + +import bisq.common.crypto.CryptoException; import bisq.common.crypto.Sig; import bisq.common.proto.network.NetworkPayload; import bisq.common.proto.network.NetworkProtoResolver; import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.Utilities; import com.google.protobuf.ByteString; import com.google.protobuf.Message; +import com.google.common.base.Preconditions; + import java.security.PublicKey; +import java.time.Clock; + import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -38,42 +46,62 @@ public class ProtectedStorageEntry implements NetworkPayload, PersistablePayload private final ProtectedStoragePayload protectedStoragePayload; private final byte[] ownerPubKeyBytes; transient private final PublicKey ownerPubKey; - private int sequenceNumber; + private final int sequenceNumber; private byte[] signature; private long creationTimeStamp; public ProtectedStorageEntry(ProtectedStoragePayload protectedStoragePayload, + PublicKey ownerPubKey, + int sequenceNumber, + byte[] signature, + Clock clock) { + this(protectedStoragePayload, + Sig.getPublicKeyBytes(ownerPubKey), + ownerPubKey, + sequenceNumber, + signature, + clock.millis(), + clock); + } + + protected ProtectedStorageEntry(ProtectedStoragePayload protectedStoragePayload, + byte[] ownerPubKeyBytes, PublicKey ownerPubKey, int sequenceNumber, - byte[] signature) { + byte[] signature, + long creationTimeStamp, + Clock clock) { + + Preconditions.checkArgument(!(protectedStoragePayload instanceof PersistableNetworkPayload)); + this.protectedStoragePayload = protectedStoragePayload; - ownerPubKeyBytes = Sig.getPublicKeyBytes(ownerPubKey); + this.ownerPubKeyBytes = ownerPubKeyBytes; this.ownerPubKey = ownerPubKey; this.sequenceNumber = sequenceNumber; this.signature = signature; - this.creationTimeStamp = System.currentTimeMillis(); - } + this.creationTimeStamp = creationTimeStamp; + maybeAdjustCreationTimeStamp(clock); + } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// - protected ProtectedStorageEntry(long creationTimeStamp, - ProtectedStoragePayload protectedStoragePayload, + private ProtectedStorageEntry(ProtectedStoragePayload protectedStoragePayload, byte[] ownerPubKeyBytes, int sequenceNumber, - byte[] signature) { - this.protectedStoragePayload = protectedStoragePayload; - this.ownerPubKeyBytes = ownerPubKeyBytes; - ownerPubKey = Sig.getPublicKeyFromBytes(ownerPubKeyBytes); - - this.sequenceNumber = sequenceNumber; - this.signature = signature; - this.creationTimeStamp = creationTimeStamp; - - maybeAdjustCreationTimeStamp(); + byte[] signature, + long creationTimeStamp, + Clock clock) { + this(protectedStoragePayload, + ownerPubKeyBytes, + Sig.getPublicKeyFromBytes(ownerPubKeyBytes), + sequenceNumber, + signature, + creationTimeStamp, + clock); } public Message toProtoMessage() { @@ -93,11 +121,13 @@ public protobuf.ProtectedStorageEntry toProtectedStorageEntry() { public static ProtectedStorageEntry fromProto(protobuf.ProtectedStorageEntry proto, NetworkProtoResolver resolver) { - return new ProtectedStorageEntry(proto.getCreationTimeStamp(), + return new ProtectedStorageEntry( ProtectedStoragePayload.fromProto(proto.getStoragePayload(), resolver), proto.getOwnerPubKeyBytes().toByteArray(), proto.getSequenceNumber(), - proto.getSignature().toByteArray()); + proto.getSignature().toByteArray(), + proto.getCreationTimeStamp(), + resolver.getClock()); } @@ -105,14 +135,10 @@ public static ProtectedStorageEntry fromProto(protobuf.ProtectedStorageEntry pro // API /////////////////////////////////////////////////////////////////////////////////////////// - public void maybeAdjustCreationTimeStamp() { + public void maybeAdjustCreationTimeStamp(Clock clock) { // We don't allow creation date in the future, but we cannot be too strict as clocks are not synced - if (creationTimeStamp > System.currentTimeMillis()) - creationTimeStamp = System.currentTimeMillis(); - } - - public void refreshTTL() { - creationTimeStamp = System.currentTimeMillis(); + if (creationTimeStamp > clock.millis()) + creationTimeStamp = clock.millis(); } public void backDate() { @@ -120,16 +146,115 @@ public void backDate() { creationTimeStamp -= ((ExpirablePayload) protectedStoragePayload).getTTL() / 2; } - public void updateSequenceNumber(int sequenceNumber) { - this.sequenceNumber = sequenceNumber; - } - + // TODO: only used in tests so find a better way to test and delete public API public void updateSignature(byte[] signature) { this.signature = signature; } - public boolean isExpired() { + public boolean isExpired(Clock clock) { return protectedStoragePayload instanceof ExpirablePayload && - (System.currentTimeMillis() - creationTimeStamp) > ((ExpirablePayload) protectedStoragePayload).getTTL(); + (clock.millis() - creationTimeStamp) > ((ExpirablePayload) protectedStoragePayload).getTTL(); + } + + /* + * Returns true if the Entry is valid for an add operation. For non-mailbox Entrys, the entry owner must + * match the payload owner. + */ + public boolean isValidForAddOperation() { + if (!this.isSignatureValid()) + return false; + + // TODO: The code currently supports MailboxStoragePayload objects inside ProtectedStorageEntry. Fix this. + if (protectedStoragePayload instanceof MailboxStoragePayload) { + MailboxStoragePayload mailboxStoragePayload = (MailboxStoragePayload) this.getProtectedStoragePayload(); + return mailboxStoragePayload.getSenderPubKeyForAddOperation() != null && + mailboxStoragePayload.getSenderPubKeyForAddOperation().equals(this.getOwnerPubKey()); + + } else { + boolean result = this.ownerPubKey != null && + this.protectedStoragePayload != null && + this.ownerPubKey.equals(protectedStoragePayload.getOwnerPubKey()); + + if (!result) { + String res1 = this.toString(); + String res2 = "null"; + if (protectedStoragePayload != null && protectedStoragePayload.getOwnerPubKey() != null) + res2 = Utilities.encodeToHex(protectedStoragePayload.getOwnerPubKey().getEncoded(), true); + + log.warn("ProtectedStorageEntry::isValidForAddOperation() failed. Entry owner does not match Payload owner:\n" + + "ProtectedStorageEntry={}\nPayloadOwner={}", res1, res2); + } + + return result; + } + } + + /* + * Returns true if the Entry is valid for a remove operation. For non-mailbox Entrys, the entry owner must + * match the payload owner. + */ + public boolean isValidForRemoveOperation() { + + // Same requirements as add() + boolean result = this.isValidForAddOperation(); + + if (!result) { + String res1 = this.toString(); + String res2 = "null"; + if (protectedStoragePayload != null && protectedStoragePayload.getOwnerPubKey() != null) + res2 = Utilities.encodeToHex(protectedStoragePayload.getOwnerPubKey().getEncoded(), true); + + log.warn("ProtectedStorageEntry::isValidForRemoveOperation() failed. Entry owner does not match Payload owner:\n" + + "ProtectedStorageEntry={}\nPayloadOwner={}", res1, res2); + } + + return result; + } + + /* + * Returns true if the signature for the Entry is valid for the payload, sequence number, and ownerPubKey + */ + boolean isSignatureValid() { + try { + byte[] hashOfDataAndSeqNr = P2PDataStorage.get32ByteHash( + new P2PDataStorage.DataAndSeqNrPair(this.protectedStoragePayload, this.sequenceNumber)); + + boolean result = Sig.verify(this.ownerPubKey, hashOfDataAndSeqNr, this.signature); + + if (!result) + log.warn("ProtectedStorageEntry::isSignatureValid() failed.\n{}}", this); + + return result; + } catch (CryptoException e) { + log.error("ProtectedStorageEntry::isSignatureValid() exception {}", e.toString()); + return false; + } + } + + /* + * Returns true if the Entry metadata that is expected to stay constant between different versions of the same object + * matches. + */ + public boolean matchesRelevantPubKey(ProtectedStorageEntry protectedStorageEntry) { + boolean result = protectedStorageEntry.getOwnerPubKey().equals(this.ownerPubKey); + + if (!result) { + log.warn("New data entry does not match our stored data. storedData.ownerPubKey=" + + (protectedStorageEntry.getOwnerPubKey() != null ? protectedStorageEntry.getOwnerPubKey().toString() : "null") + + ", ownerPubKey=" + this.ownerPubKey); + } + + return result; + } + + @Override + public String toString() { + return "ProtectedStorageEntry {" + + "\n\tPayload: " + protectedStoragePayload + + "\n\tOwner Public Key: " + Utilities.bytesAsHexString(this.ownerPubKeyBytes) + + "\n\tSequence Number: " + this.sequenceNumber + + "\n\tSignature: " + Utilities.bytesAsHexString(this.signature) + + "\n\tTimestamp: " + this.creationTimeStamp + + "\n} "; } } diff --git a/p2p/src/test/java/bisq/network/p2p/TestUtils.java b/p2p/src/test/java/bisq/network/p2p/TestUtils.java index b483fde4b3c..87d42b20cd2 100644 --- a/p2p/src/test/java/bisq/network/p2p/TestUtils.java +++ b/p2p/src/test/java/bisq/network/p2p/TestUtils.java @@ -27,6 +27,8 @@ import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; +import java.time.Clock; + import java.util.Set; import java.util.concurrent.CountDownLatch; @@ -184,6 +186,9 @@ public NetworkPayload fromProto(protobuf.StoragePayload proto) { public NetworkPayload fromProto(protobuf.StorageEntryWrapper proto) { return null; } + + @Override + public Clock getClock() { return null; } }; } } diff --git a/p2p/src/test/java/bisq/network/p2p/storage/ObsoleteP2PDataStorageTest.java b/p2p/src/test/java/bisq/network/p2p/storage/ObsoleteP2PDataStorageTest.java deleted file mode 100644 index 5cd526aba75..00000000000 --- a/p2p/src/test/java/bisq/network/p2p/storage/ObsoleteP2PDataStorageTest.java +++ /dev/null @@ -1,217 +0,0 @@ -/* left as documentation -package bisq.network.p2p.storage; - -import bisq.network.crypto.EncryptionService; -import bisq.network.p2p.NodeAddress; -import bisq.network.p2p.TestUtils; -import bisq.network.p2p.network.NetworkNode; -import bisq.network.p2p.peers.Broadcaster; -import bisq.network.p2p.storage.payload.ProtectedStoragePayload; - -import bisq.common.crypto.CryptoException; -import bisq.common.crypto.KeyRing; -import bisq.common.crypto.KeyStorage; -import bisq.common.proto.network.NetworkProtoResolver; -import bisq.common.proto.persistable.PersistenceProtoResolver; -import bisq.common.storage.FileUtil; - -import java.security.InvalidKeyException; -import java.security.KeyPair; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.SignatureException; -import java.security.cert.CertificateException; - -import java.nio.file.Path; -import java.nio.file.Paths; - -import java.io.File; -import java.io.IOException; - -import java.util.HashSet; -import java.util.Set; - -import lombok.extern.slf4j.Slf4j; - -import mockit.Mocked; -import mockit.integration.junit4.JMockit; - -import org.junit.After; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.runner.RunWith; - -@Slf4j -@RunWith(JMockit.class) -@Ignore("Use NetworkProtoResolver, PersistenceProtoResolver or ProtoResolver which are all in io.bisq.common.") -public class ObsoleteP2PDataStorageTest { - private final Set seedNodes = new HashSet<>(); - private EncryptionService encryptionService1, encryptionService2; - private P2PDataStorage dataStorage1; - private KeyPair storageSignatureKeyPair1, storageSignatureKeyPair2; - private KeyRing keyRing1, keyRing2; - private ProtectedStoragePayload protectedStoragePayload; - private File dir1; - private File dir2; - - @Mocked - Broadcaster broadcaster; - @Mocked - NetworkNode networkNode; - @Mocked - NetworkProtoResolver networkProtoResolver; - @Mocked - PersistenceProtoResolver persistenceProtoResolver; - - @Before - public void setup() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException { - - dir1 = File.createTempFile("temp_tests1", ""); - //noinspection ResultOfMethodCallIgnored - dir1.delete(); - //noinspection ResultOfMethodCallIgnored - dir1.mkdir(); - dir2 = File.createTempFile("temp_tests2", ""); - //noinspection ResultOfMethodCallIgnored - dir2.delete(); - //noinspection ResultOfMethodCallIgnored - dir2.mkdir(); - - keyRing1 = new KeyRing(new KeyStorage(dir1)); - storageSignatureKeyPair1 = keyRing1.getSignatureKeyPair(); - encryptionService1 = new EncryptionService(keyRing1, TestUtils.getNetworkProtoResolver()); - - // for mailbox - keyRing2 = new KeyRing(new KeyStorage(dir2)); - storageSignatureKeyPair2 = keyRing2.getSignatureKeyPair(); - encryptionService2 = new EncryptionService(keyRing2, TestUtils.getNetworkProtoResolver()); - //dataStorage1 = new P2PDataStorage(broadcaster, networkNode, dir1, persistenceProtoResolver); - } - - @After - public void tearDown() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException { - Path path = Paths.get(TestUtils.test_dummy_dir); - File dir = path.toFile(); - FileUtil.deleteDirectory(dir); - FileUtil.deleteDirectory(dir1); - FileUtil.deleteDirectory(dir2); - } - - /* @Test - public void testProtectedStorageEntryAddAndRemove() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException, NoSuchProviderException { - storagePayload = new AlertPayload(new AlertVO("alert", - false, - "version", - storageSignatureKeyPair1.getPublic().getEncoded(), - "sig", - null)); - - ProtectedStorageEntry data = dataStorage1.getProtectedData(storagePayload, storageSignatureKeyPair1); - assertTrue(dataStorage1.add(data, null, null, true)); - assertEquals(1, dataStorage1.getMap().size()); - - int newSequenceNumber = data.sequenceNumber + 1; - byte[] hashOfDataAndSeqNr = Hash.getHash(new P2PDataStorage.DataAndSeqNrPair(data.getStoragePayload(), newSequenceNumber)); - byte[] signature = Sig.sign(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr); - ProtectedStorageEntry dataToRemove = new ProtectedStorageEntry(data.getStoragePayload(), data.ownerPubKey, newSequenceNumber, signature); - assertTrue(dataStorage1.remove(dataToRemove, null, true)); - assertEquals(0, dataStorage1.getMap().size()); - } - - @Test - public void testProtectedStorageEntryRoundtrip() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException, NoSuchProviderException { - //mockData = new MockData("mockData", keyRing1.getSignatureKeyPair().getPublic()); - storagePayload = getDummyOffer(); - - ProtectedStorageEntry data = dataStorage1.getProtectedData(storagePayload, storageSignatureKeyPair1); - setSignature(data); - assertTrue(checkSignature(data)); - - ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); - data.toEnvelopeProto().writeTo(byteOutputStream); - - //TODO Use NetworkProtoResolver, PersistenceProtoResolver or ProtoResolver which are all in io.bisq.common. - ProtectedStorageEntry protectedStorageEntry = ProtoBufferUtilities.getProtectedStorageEntry(PB.ProtectedStorageEntry.parseFrom(new ByteArrayInputStream(byteOutputStream.toByteArray()))); - - assertTrue(Arrays.equals(Hash.getHash(data.getStoragePayload()), Hash.getHash(protectedStorageEntry.getStoragePayload()))); - assertTrue(data.equals(protectedStorageEntry)); - assertTrue(checkSignature(protectedStorageEntry)); - }*/ - - //TODO Use NetworkProtoResolver, PersistenceProtoResolver or ProtoResolver which are all in io.bisq.common. - /* @Test - public void testOfferRoundtrip() throws InvalidProtocolBufferException { - OfferPayload offer = getDummyOffer(); - try { - String buffer = JsonFormat.printer().print(offer.toEnvelopeProto().getOfferPayload()); - JsonFormat.Parser parser = JsonFormat.parser(); - PB.OfferPayload.Builder builder = PB.OfferPayload.newBuilder(); - parser.merge(buffer, builder); - assertEquals(offer, ProtoBufferUtilities.getOfferPayload(builder.build())); - } catch (IOException e) { - e.printStackTrace(); - fail(); - } - }*/ - - /* @NotNull - private OfferPayload getDummyOffer() { - NodeAddress nodeAddress = new NodeAddress("host", 1000); - NodeAddress nodeAddress2 = new NodeAddress("host1", 1001); - NodeAddress nodeAddress3 = new NodeAddress("host2", 1002); - NodeAddress nodeAddress4 = new NodeAddress("host3", 1002); - return new OfferPayload("id", - System.currentTimeMillis(), - nodeAddress4, - keyRing1.getPubKeyRing(), - OfferPayload.Direction.BUY, - 1200, - 1.5, - true, - 100, - 50, - "BTC", - "USD", - Lists.newArrayList(nodeAddress, - nodeAddress2, - nodeAddress3), - Lists.newArrayList(nodeAddress, - nodeAddress2, - nodeAddress3), - "SEPA", - "accountid", - "feetxId", - "BE", - Lists.newArrayList("BE", "AU"), - "bankid", - Lists.newArrayList("BANK1", "BANK2"), - "version", - 100, - 100, - 100, - 100, - 1000, - 1000, - 1000, - false, - false, - - 1000, - 1000, - false, - "hash", - null); - } - - private void setSignature(ProtectedStorageEntry entry) throws CryptoException { - int newSequenceNumber = entry.sequenceNumber; - byte[] hashOfDataAndSeqNr = Hash.getHash(new P2PDataStorage.DataAndSeqNrPair(entry.getStoragePayload(), newSequenceNumber)); - byte[] signature = Sig.sign(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr); - entry.signature = signature; - } - - private boolean checkSignature(ProtectedStorageEntry entry) throws CryptoException { - byte[] hashOfDataAndSeqNr = Hash.getHash(new P2PDataStorage.DataAndSeqNrPair(entry.getStoragePayload(), entry.sequenceNumber)); - return dataStorage1.checkSignature(entry.ownerPubKey, hashOfDataAndSeqNr, entry.signature); -} - }*/ diff --git a/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageClientAPITest.java b/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageClientAPITest.java new file mode 100644 index 00000000000..867cbea229a --- /dev/null +++ b/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageClientAPITest.java @@ -0,0 +1,242 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.network.p2p.storage; + +import bisq.network.p2p.TestUtils; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.storage.messages.AddDataMessage; +import bisq.network.p2p.storage.messages.RefreshOfferMessage; +import bisq.network.p2p.storage.mocks.ExpirableProtectedStoragePayloadStub; +import bisq.network.p2p.storage.payload.MailboxStoragePayload; +import bisq.network.p2p.storage.payload.ProtectedMailboxStorageEntry; +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; +import bisq.network.p2p.storage.payload.ProtectedStoragePayload; + +import bisq.common.app.Version; +import bisq.common.crypto.CryptoException; + +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; + +import java.util.Optional; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import static bisq.network.p2p.storage.TestState.*; + +/** + * Tests of the P2PDataStore Client API entry points. + * + * These tests validate the client code path that uses the pattern addProtectedStorageEntry(getProtectedStorageEntry()) + * as opposed to the onMessage() handler or DataRequest paths. + */ +public class P2PDataStorageClientAPITest { + private TestState testState; + + @Before + public void setUp() { + this.testState = new TestState(); + + // Deep in the bowels of protobuf we grab the messageID from the version module. This is required to hash the + // full MailboxStoragePayload so make sure it is initialized. + Version.setBaseCryptoNetworkId(1); + } + + // TESTCASE: Adding an entry from the getProtectedStorageEntry API correctly adds the item + @Test + public void getProtectedStorageEntry_NoExist() throws NoSuchAlgorithmException, CryptoException { + KeyPair ownerKeys = TestUtils.generateKeyPair(); + + ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayloadStub(ownerKeys.getPublic()); + ProtectedStorageEntry protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); + + SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); + Assert.assertTrue(this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, TestState.getTestNodeAddress(), null, true)); + + this.testState.verifyProtectedStorageAdd(beforeState, protectedStorageEntry, true, true); + } + + // TESTCASE: Adding an entry from the getProtectedStorageEntry API of an existing item correctly updates the item + @Test + public void getProtectedStorageEntry() throws NoSuchAlgorithmException, CryptoException { + KeyPair ownerKeys = TestUtils.generateKeyPair(); + + ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayloadStub(ownerKeys.getPublic()); + ProtectedStorageEntry protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); + + Assert.assertTrue(this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, TestState.getTestNodeAddress(), null, true)); + + SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); + protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); + this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, TestState.getTestNodeAddress(), null, true); + + this.testState.verifyProtectedStorageAdd(beforeState, protectedStorageEntry, true, true); + } + + // TESTCASE: Adding an entry from the getProtectedStorageEntry API of an existing item (added from onMessage path) correctly updates the item + @Test + public void getProtectedStorageEntry_FirstOnMessageSecondAPI() throws NoSuchAlgorithmException, CryptoException { + KeyPair ownerKeys = TestUtils.generateKeyPair(); + + ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayloadStub(ownerKeys.getPublic()); + ProtectedStorageEntry protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); + + Connection mockedConnection = mock(Connection.class); + when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(TestState.getTestNodeAddress())); + + this.testState.mockedStorage.onMessage(new AddDataMessage(protectedStorageEntry), mockedConnection); + + SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); + protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); + Assert.assertTrue(this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, TestState.getTestNodeAddress(), null, true)); + + this.testState.verifyProtectedStorageAdd(beforeState, protectedStorageEntry, true, true); + } + + // TESTCASE: Updating an entry from the getRefreshTTLMessage API correctly errors if the item hasn't been seen + @Test + public void getRefreshTTLMessage_NoExists() throws NoSuchAlgorithmException, CryptoException { + KeyPair ownerKeys = TestUtils.generateKeyPair(); + + ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayloadStub(ownerKeys.getPublic()); + + RefreshOfferMessage refreshOfferMessage = this.testState.mockedStorage.getRefreshTTLMessage(protectedStoragePayload, ownerKeys); + + SavedTestState beforeState = this.testState.saveTestState(refreshOfferMessage); + Assert.assertFalse(this.testState.mockedStorage.refreshTTL(refreshOfferMessage, TestState.getTestNodeAddress(), true)); + + this.testState.verifyRefreshTTL(beforeState, refreshOfferMessage, false, true); + } + + // TESTCASE: Updating an entry from the getRefreshTTLMessage API correctly "refreshes" the item + @Test + public void getRefreshTTLMessage() throws NoSuchAlgorithmException, CryptoException { + KeyPair ownerKeys = TestUtils.generateKeyPair(); + + ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayloadStub(ownerKeys.getPublic()); + ProtectedStorageEntry protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); + this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, TestState.getTestNodeAddress(), null, true); + + RefreshOfferMessage refreshOfferMessage = this.testState.mockedStorage.getRefreshTTLMessage(protectedStoragePayload, ownerKeys); + this.testState.mockedStorage.refreshTTL(refreshOfferMessage, TestState.getTestNodeAddress(), true); + + refreshOfferMessage = this.testState.mockedStorage.getRefreshTTLMessage(protectedStoragePayload, ownerKeys); + + this.testState.incrementClock(); + + SavedTestState beforeState = this.testState.saveTestState(refreshOfferMessage); + Assert.assertTrue(this.testState.mockedStorage.refreshTTL(refreshOfferMessage, TestState.getTestNodeAddress(), true)); + + this.testState.verifyRefreshTTL(beforeState, refreshOfferMessage, true, true); + } + + // TESTCASE: Updating an entry from the getRefreshTTLMessage API correctly "refreshes" the item when it was originally added from onMessage path + @Test + public void getRefreshTTLMessage_FirstOnMessageSecondAPI() throws NoSuchAlgorithmException, CryptoException { + KeyPair ownerKeys = TestUtils.generateKeyPair(); + + ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayloadStub(ownerKeys.getPublic()); + ProtectedStorageEntry protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); + this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, TestState.getTestNodeAddress(), null, true); + + Connection mockedConnection = mock(Connection.class); + when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(TestState.getTestNodeAddress())); + + this.testState.mockedStorage.onMessage(new AddDataMessage(protectedStorageEntry), mockedConnection); + + RefreshOfferMessage refreshOfferMessage = this.testState.mockedStorage.getRefreshTTLMessage(protectedStoragePayload, ownerKeys); + + this.testState.incrementClock(); + + SavedTestState beforeState = this.testState.saveTestState(refreshOfferMessage); + Assert.assertTrue(this.testState.mockedStorage.refreshTTL(refreshOfferMessage, TestState.getTestNodeAddress(), true)); + + this.testState.verifyRefreshTTL(beforeState, refreshOfferMessage, true, true); + } + + // TESTCASE: Removing a non-existent mailbox entry from the getMailboxDataWithSignedSeqNr API + @Test + public void getMailboxDataWithSignedSeqNr_RemoveNoExist() throws NoSuchAlgorithmException, CryptoException { + KeyPair receiverKeys = TestUtils.generateKeyPair(); + KeyPair senderKeys = TestUtils.generateKeyPair(); + + MailboxStoragePayload mailboxStoragePayload = TestState.buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); + + ProtectedMailboxStorageEntry protectedMailboxStorageEntry = + this.testState.mockedStorage.getMailboxDataWithSignedSeqNr(mailboxStoragePayload, receiverKeys, receiverKeys.getPublic()); + + SavedTestState beforeState = this.testState.saveTestState(protectedMailboxStorageEntry); + Assert.assertFalse(this.testState.mockedStorage.remove(protectedMailboxStorageEntry, TestState.getTestNodeAddress(), true)); + + this.testState.verifyProtectedStorageRemove(beforeState, protectedMailboxStorageEntry, false, true, true, true); + } + + // TESTCASE: Adding, then removing a mailbox message from the getMailboxDataWithSignedSeqNr API + @Test + public void getMailboxDataWithSignedSeqNr_AddThenRemove() throws NoSuchAlgorithmException, CryptoException { + KeyPair receiverKeys = TestUtils.generateKeyPair(); + KeyPair senderKeys = TestUtils.generateKeyPair(); + + MailboxStoragePayload mailboxStoragePayload = TestState.buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); + + ProtectedMailboxStorageEntry protectedMailboxStorageEntry = + this.testState.mockedStorage.getMailboxDataWithSignedSeqNr(mailboxStoragePayload, senderKeys, receiverKeys.getPublic()); + + Assert.assertTrue(this.testState.mockedStorage.addProtectedStorageEntry(protectedMailboxStorageEntry, TestState.getTestNodeAddress(), null, true)); + + protectedMailboxStorageEntry = + this.testState.mockedStorage.getMailboxDataWithSignedSeqNr(mailboxStoragePayload, receiverKeys, receiverKeys.getPublic()); + + SavedTestState beforeState = this.testState.saveTestState(protectedMailboxStorageEntry); + Assert.assertTrue(this.testState.mockedStorage.remove(protectedMailboxStorageEntry, TestState.getTestNodeAddress(), true)); + + this.testState.verifyProtectedStorageRemove(beforeState, protectedMailboxStorageEntry, true, true, true,true); + } + + // TESTCASE: Removing a mailbox message that was added from the onMessage handler + @Test + public void getMailboxDataWithSignedSeqNr_ValidRemoveAddFromMessage() throws NoSuchAlgorithmException, CryptoException { + KeyPair receiverKeys = TestUtils.generateKeyPair(); + KeyPair senderKeys = TestUtils.generateKeyPair(); + + MailboxStoragePayload mailboxStoragePayload = TestState.buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); + + ProtectedMailboxStorageEntry protectedMailboxStorageEntry = + this.testState.mockedStorage.getMailboxDataWithSignedSeqNr(mailboxStoragePayload, senderKeys, receiverKeys.getPublic()); + + Connection mockedConnection = mock(Connection.class); + when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(TestState.getTestNodeAddress())); + + this.testState.mockedStorage.onMessage(new AddDataMessage(protectedMailboxStorageEntry), mockedConnection); + + mailboxStoragePayload = (MailboxStoragePayload) protectedMailboxStorageEntry.getProtectedStoragePayload(); + + protectedMailboxStorageEntry = + this.testState.mockedStorage.getMailboxDataWithSignedSeqNr(mailboxStoragePayload, receiverKeys, receiverKeys.getPublic()); + + SavedTestState beforeState = this.testState.saveTestState(protectedMailboxStorageEntry); + Assert.assertTrue(this.testState.mockedStorage.remove(protectedMailboxStorageEntry, TestState.getTestNodeAddress(), true)); + + this.testState.verifyProtectedStorageRemove(beforeState, protectedMailboxStorageEntry, true, true, true,true); + } +} diff --git a/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageOnMessageHandlerTest.java b/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageOnMessageHandlerTest.java new file mode 100644 index 00000000000..5a73e8acdd2 --- /dev/null +++ b/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageOnMessageHandlerTest.java @@ -0,0 +1,101 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.network.p2p.storage; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.mocks.MockPayload; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.storage.messages.AddPersistableNetworkPayloadMessage; +import bisq.network.p2p.storage.messages.BroadcastMessage; +import bisq.network.p2p.storage.mocks.PersistableNetworkPayloadStub; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; + +import bisq.common.proto.network.NetworkEnvelope; + +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests of the P2PDataStore MessageListener interface failure cases. The success cases are covered in the + * PersistableNetworkPayloadTest and ProtectedStorageEntryTest tests, + */ +public class P2PDataStorageOnMessageHandlerTest { + private TestState testState; + + @Before + public void setup() { + this.testState = new TestState(); + } + + static class UnsupportedBroadcastMessage extends BroadcastMessage { + + UnsupportedBroadcastMessage() { + super(0); + } + } + + @Test + public void invalidBroadcastMessage() { + NetworkEnvelope envelope = new MockPayload("Mock"); + + Connection mockedConnection = mock(Connection.class); + when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(TestState.getTestNodeAddress())); + + this.testState.mockedStorage.onMessage(envelope, mockedConnection); + + verify(this.testState.appendOnlyDataStoreListener, never()).onAdded(any(PersistableNetworkPayload.class)); + verify(this.testState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class), eq(null), anyBoolean()); + } + + @Test + public void unsupportedBroadcastMessage() { + NetworkEnvelope envelope = new UnsupportedBroadcastMessage(); + + Connection mockedConnection = mock(Connection.class); + when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(TestState.getTestNodeAddress())); + + this.testState.mockedStorage.onMessage(envelope, mockedConnection); + + verify(this.testState.appendOnlyDataStoreListener, never()).onAdded(any(PersistableNetworkPayload.class)); + verify(this.testState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class), eq(null), anyBoolean()); + } + + @Test + public void invalidConnectionObject() { + PersistableNetworkPayload persistableNetworkPayload = new PersistableNetworkPayloadStub(true); + NetworkEnvelope envelope = new AddPersistableNetworkPayloadMessage(persistableNetworkPayload); + + Connection mockedConnection = mock(Connection.class); + when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.empty()); + + this.testState.mockedStorage.onMessage(envelope, mockedConnection); + + verify(this.testState.appendOnlyDataStoreListener, never()).onAdded(any(PersistableNetworkPayload.class)); + verify(this.testState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class), eq(null), anyBoolean()); + } +} diff --git a/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStoragePersistableNetworkPayloadTest.java b/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStoragePersistableNetworkPayloadTest.java new file mode 100644 index 00000000000..c8a3d9c6134 --- /dev/null +++ b/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStoragePersistableNetworkPayloadTest.java @@ -0,0 +1,195 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.network.p2p.storage; + +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.storage.messages.AddPersistableNetworkPayloadMessage; +import bisq.network.p2p.storage.mocks.DateTolerantPayloadStub; +import bisq.network.p2p.storage.mocks.PersistableNetworkPayloadStub; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import static bisq.network.p2p.storage.TestState.*; + +/** + * Tests of the P2PDataStore entry points that use the PersistableNetworkPayload type + * + * The abstract base class AddPersistableNetworkPayloadTest defines the common test cases and Payload type + * that needs to be tested is set up through extending the base class and overriding the createInstance() methods to + * give the common tests a different payload to test. + * + * Each subclass (Payload type) can optionally add additional tests that verify functionality only relevant + * to that payload. + * + * Each test case is run through 4 entry points to verify the correct behavior: + * + * 1. RequestData path [addPersistableNetworkPayloadFromInitialRequest] + * 2 & 3 Client API [addPersistableNetworkPayload(reBroadcast=(true && false))] + * 4. onMessage() [onMessage(AddPersistableNetworkPayloadMessage)] + */ +public class P2PDataStoragePersistableNetworkPayloadTest { + + @RunWith(Parameterized.class) + public abstract static class AddPersistableNetworkPayloadTest { + TestState testState; + + @Parameterized.Parameter(0) + public TestCase testCase; + + @Parameterized.Parameter(1) + public boolean allowBroadcast; + + @Parameterized.Parameter(2) + public boolean reBroadcast; + + @Parameterized.Parameter(3) + public boolean checkDate; + + PersistableNetworkPayload persistableNetworkPayload; + + abstract PersistableNetworkPayload createInstance(); + + enum TestCase { + PUBLIC_API, + ON_MESSAGE, + INIT, + } + + boolean expectBroadcastOnStateChange() { + return this.testCase != TestCase.INIT; + } + + boolean expectedIsDataOwner() { + return this.testCase == TestCase.PUBLIC_API; + } + + void doAddAndVerify(PersistableNetworkPayload persistableNetworkPayload, boolean expectedReturnValue, boolean expectedStateChange) { + SavedTestState beforeState = this.testState.saveTestState(persistableNetworkPayload); + + if (this.testCase == TestCase.INIT) { + Assert.assertEquals(expectedReturnValue, this.testState.mockedStorage.addPersistableNetworkPayloadFromInitialRequest(persistableNetworkPayload)); + } else if (this.testCase == TestCase.PUBLIC_API) { + Assert.assertEquals(expectedReturnValue, + this.testState.mockedStorage.addPersistableNetworkPayload(persistableNetworkPayload, TestState.getTestNodeAddress(), true, this.allowBroadcast, this.reBroadcast, this.checkDate)); + } else { // onMessage + Connection mockedConnection = mock(Connection.class); + when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(TestState.getTestNodeAddress())); + + testState.mockedStorage.onMessage(new AddPersistableNetworkPayloadMessage(persistableNetworkPayload), mockedConnection); + } + + this.testState.verifyPersistableAdd(beforeState, persistableNetworkPayload, expectedStateChange, this.expectBroadcastOnStateChange(), this.expectedIsDataOwner()); + } + + @Before + public void setup() { + this.persistableNetworkPayload = this.createInstance(); + + this.testState = new TestState(); + } + + @Parameterized.Parameters(name = "{index}: Test with TestCase={0} allowBroadcast={1} reBroadcast={2} checkDate={3}") + public static Collection data() { + List data = new ArrayList<>(); + + // Init doesn't use other parameters + data.add(new Object[] { TestCase.INIT, false, false, false }); + + // onMessage doesn't use other parameters + data.add(new Object[] { TestCase.ON_MESSAGE, false, false, false }); + + // Client API uses two permutations + // Normal path + data.add(new Object[] { TestCase.PUBLIC_API, true, true, false }); + + // Refresh path + data.add(new Object[] { TestCase.PUBLIC_API, true, false, false }); + + return data; + } + + @Test + public void addPersistableNetworkPayload() { + // First add should succeed regardless of parameters + doAddAndVerify(this.persistableNetworkPayload, true, true); + } + + @Test + public void addPersistableNetworkPayloadDuplicate() { + doAddAndVerify(this.persistableNetworkPayload, true, true); + + // Second call only succeeds if reBroadcast was set or we are adding through the init + // path which just overwrites + boolean expectedReturnValue = this.reBroadcast || this.testCase == TestCase.INIT; + doAddAndVerify(this.persistableNetworkPayload, expectedReturnValue, false); + } + } + + /** + * Runs the common test cases defined in AddPersistableNetworkPayloadTest against a PersistableNetworkPayload + */ + public static class AddPersistableNetworkPayloadStubTest extends AddPersistableNetworkPayloadTest { + @Override + PersistableNetworkPayloadStub createInstance() { + return new PersistableNetworkPayloadStub(true); + } + + @Test + public void invalidHash() { + PersistableNetworkPayload persistableNetworkPayload = new PersistableNetworkPayloadStub(false); + + doAddAndVerify(persistableNetworkPayload, false, false); + } + } + + /** + * Runs the common test cases defined in AddPersistableNetworkPayloadTest against a PersistableNetworkPayload using + * the DateTolerant marker interface. + */ + public static class AddPersistableDateTolerantPayloadTest extends AddPersistableNetworkPayloadTest { + + @Override + DateTolerantPayloadStub createInstance() { + return new DateTolerantPayloadStub(true); + + } + + @Test + public void outOfTolerance() { + PersistableNetworkPayload persistableNetworkPayload = new DateTolerantPayloadStub(false); + + // The onMessage path checks for tolerance + boolean expectedReturn = this.testCase != TestCase.ON_MESSAGE; + + doAddAndVerify(persistableNetworkPayload, expectedReturn, expectedReturn); + } + } +} diff --git a/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageProtectedStorageEntryTest.java b/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageProtectedStorageEntryTest.java new file mode 100644 index 00000000000..2aa8bc03603 --- /dev/null +++ b/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageProtectedStorageEntryTest.java @@ -0,0 +1,630 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.network.p2p.storage; + +import bisq.network.p2p.TestUtils; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.storage.messages.AddDataMessage; +import bisq.network.p2p.storage.messages.RefreshOfferMessage; +import bisq.network.p2p.storage.messages.RemoveDataMessage; +import bisq.network.p2p.storage.messages.RemoveMailboxDataMessage; +import bisq.network.p2p.storage.mocks.*; +import bisq.network.p2p.storage.payload.ProtectedMailboxStorageEntry; +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; +import bisq.network.p2p.storage.payload.ProtectedStoragePayload; + +import bisq.common.app.Version; +import bisq.common.crypto.CryptoException; +import bisq.common.crypto.Sig; + +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import static org.mockito.Mockito.*; + +import static bisq.network.p2p.storage.TestState.*; + + +/** + * Tests of the P2PDataStore entry points that use the ProtectedStorageEntry type + * + * The abstract base class ProtectedStorageEntryTestBase defines the common test cases and each Entry and Payload type + * that needs to be tested is set up through extending the base class and overriding the createInstance() and + * getEntryClass() methods to give the common tests a different combination to test. + * + * Each subclass (Entry & Payload combination) can optionally add additional tests that verify functionality only relevant + * to that combination. + * + * Each test case is run through 2 entry points to validate the correct behavior + * 1. Client API [addProtectedStorageEntry(), refreshTTL(), remove()] + * 2. onMessage() [AddDataMessage, RefreshOfferMessage, RemoveDataMessage] + */ +public class P2PDataStorageProtectedStorageEntryTest { + @RunWith(Parameterized.class) + abstract public static class ProtectedStorageEntryTestBase { + TestState testState; + Class entryClass; + + protected abstract ProtectedStoragePayload createInstance(KeyPair payloadOwnerKeys); + protected abstract Class getEntryClass(); + + // Used for tests of ProtectedStorageEntry and subclasses + private ProtectedStoragePayload protectedStoragePayload; + KeyPair payloadOwnerKeys; + + @Parameterized.Parameter(0) + public boolean useMessageHandler; + + boolean expectIsDataOwner() { + // The onMessage handler variant should always broadcast with isDataOwner == false + // The Client API should always broadcast with isDataOwner == true + return !useMessageHandler; + } + + @Parameterized.Parameters(name = "{index}: Test with useMessageHandler={0}") + public static Collection data() { + List data = new ArrayList<>(); + + boolean[] vals = new boolean[]{true, false}; + + for (boolean useMessageHandler : vals) + data.add(new Object[]{useMessageHandler}); + + return data; + } + + @Before + public void setUp() throws CryptoException, NoSuchAlgorithmException { + this.testState = new TestState(); + + this.payloadOwnerKeys = TestUtils.generateKeyPair(); + this.protectedStoragePayload = createInstance(this.payloadOwnerKeys); + this.entryClass = this.getEntryClass(); + } + + boolean doRemove(ProtectedStorageEntry entry) { + if (this.useMessageHandler) { + Connection mockedConnection = mock(Connection.class); + when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(TestState.getTestNodeAddress())); + + testState.mockedStorage.onMessage(new RemoveDataMessage(entry), mockedConnection); + + return true; + } else { + // XXX: All callers just pass in true, a future patch can remove the argument. + return testState.mockedStorage.remove(entry, TestState.getTestNodeAddress(), true); + } + } + + boolean doAdd(ProtectedStorageEntry protectedStorageEntry) { + if (this.useMessageHandler) { + Connection mockedConnection = mock(Connection.class); + when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(TestState.getTestNodeAddress())); + + testState.mockedStorage.onMessage(new AddDataMessage(protectedStorageEntry), mockedConnection); + + return true; + } else { + // XXX: All external callers just pass in true for isDataOwner and allowBroadcast a future patch can + // remove the argument. + return this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, + TestState.getTestNodeAddress(), null, true); + } + } + + boolean doRefreshTTL(RefreshOfferMessage refreshOfferMessage) { + if (this.useMessageHandler) { + Connection mockedConnection = mock(Connection.class); + when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(TestState.getTestNodeAddress())); + + testState.mockedStorage.onMessage(refreshOfferMessage, mockedConnection); + + return true; + } else { + // XXX: All external callers just pass in true for isDataOwner a future patch can remove the argument. + return this.testState.mockedStorage.refreshTTL(refreshOfferMessage, TestState.getTestNodeAddress(), true); + } + } + + ProtectedStorageEntry getProtectedStorageEntryForAdd(int sequenceNumber, boolean validForAdd, boolean matchesRelevantPubKey) { + ProtectedStorageEntry stub = mock(entryClass); + when(stub.getOwnerPubKey()).thenReturn(this.payloadOwnerKeys.getPublic()); + when(stub.isValidForAddOperation()).thenReturn(validForAdd); + when(stub.matchesRelevantPubKey(any(ProtectedStorageEntry.class))).thenReturn(matchesRelevantPubKey); + when(stub.getSequenceNumber()).thenReturn(sequenceNumber); + when(stub.getProtectedStoragePayload()).thenReturn(protectedStoragePayload); + + return stub; + } + + // Return a ProtectedStorageEntry that will pass all validity checks for add. + ProtectedStorageEntry getProtectedStorageEntryForAdd(int sequenceNumber) { + return getProtectedStorageEntryForAdd(sequenceNumber, true, true); + } + + // Return a ProtectedStorageEntry that will pass all validity checks for remove. + ProtectedStorageEntry getProtectedStorageEntryForRemove(int sequenceNumber, boolean validForRemove, boolean matchesRelevantPubKey) { + ProtectedStorageEntry stub = mock(this.entryClass); + when(stub.getOwnerPubKey()).thenReturn(this.payloadOwnerKeys.getPublic()); + when(stub.isValidForRemoveOperation()).thenReturn(validForRemove); + when(stub.matchesRelevantPubKey(any(ProtectedStorageEntry.class))).thenReturn(matchesRelevantPubKey); + when(stub.getSequenceNumber()).thenReturn(sequenceNumber); + when(stub.getProtectedStoragePayload()).thenReturn(this.protectedStoragePayload); + + return stub; + } + + ProtectedStorageEntry getProtectedStorageEntryForRemove(int sequenceNumber) { + return getProtectedStorageEntryForRemove(sequenceNumber, true, true); + } + + void doProtectedStorageAddAndVerify(ProtectedStorageEntry protectedStorageEntry, + boolean expectedReturnValue, + boolean expectedStateChange) { + + SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); + + boolean addResult = this.doAdd(protectedStorageEntry); + + if (!this.useMessageHandler) + Assert.assertEquals(expectedReturnValue, addResult); + + this.testState.verifyProtectedStorageAdd(beforeState, protectedStorageEntry, expectedStateChange, this.expectIsDataOwner()); + } + + void doProtectedStorageRemoveAndVerify(ProtectedStorageEntry entry, + boolean expectedReturnValue, + boolean expectInternalStateChange) { + + SavedTestState beforeState = this.testState.saveTestState(entry); + + boolean addResult = this.doRemove(entry); + + if (!this.useMessageHandler) + Assert.assertEquals(expectedReturnValue, addResult); + + this.testState.verifyProtectedStorageRemove(beforeState, entry, expectInternalStateChange, true, true, this.expectIsDataOwner()); + } + + /// Valid Add Tests (isValidForAdd() and matchesRelevantPubKey() return true) + @Test + public void addProtectedStorageEntry() { + + ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); + doProtectedStorageAddAndVerify(entryForAdd, true, true); + } + + // TESTCASE: Adding duplicate payload w/ same sequence number + @Test + public void addProtectedStorageEntry_duplicateSeqNrGt0() { + ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); + doProtectedStorageAddAndVerify(entryForAdd, true, true); + doProtectedStorageAddAndVerify(entryForAdd, false, false); + } + + // TESTCASE: Adding duplicate payload w/ 0 sequence number (special branch in code for logging) + @Test + public void addProtectedStorageEntry_duplicateSeqNrEq0() { + ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(0); + doProtectedStorageAddAndVerify(entryForAdd, true, true); + doProtectedStorageAddAndVerify(entryForAdd, false, false); + } + + // TESTCASE: Adding duplicate payload for w/ lower sequence number + @Test + public void addProtectedStorageEntry_lowerSeqNr() { + ProtectedStorageEntry entryForAdd2 = this.getProtectedStorageEntryForAdd(2); + ProtectedStorageEntry entryForAdd1 = this.getProtectedStorageEntryForAdd(1); + doProtectedStorageAddAndVerify(entryForAdd2, true, true); + doProtectedStorageAddAndVerify(entryForAdd1, false, false); + } + + // TESTCASE: Adding duplicate payload for w/ greater sequence number + @Test + public void addProtectedStorageEntry_greaterSeqNr() { + ProtectedStorageEntry entryForAdd2 = this.getProtectedStorageEntryForAdd(1); + ProtectedStorageEntry entryForAdd1 = this.getProtectedStorageEntryForAdd(2); + doProtectedStorageAddAndVerify(entryForAdd2, true, true); + doProtectedStorageAddAndVerify(entryForAdd1, true, true); + } + + // TESTCASE: Add w/ same sequence number after remove of sequence number + // Regression test for old remove() behavior that succeeded if add.seq# == remove.seq# + @Test + public void addProtectectedStorageEntry_afterRemoveSameSeqNr() { + ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); + ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(1); + + doProtectedStorageAddAndVerify(entryForAdd, true, true); + doProtectedStorageRemoveAndVerify(entryForRemove, false, false); + + doProtectedStorageAddAndVerify(entryForAdd, false, false); + } + + // Invalid add tests (isValidForAddOperation() || matchesRelevantPubKey()) returns false + + // TESTCASE: Add fails if Entry is not valid for add + @Test + public void addProtectedStorageEntry_EntryNotisValidForAddOperation() { + ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1, false, true); + doProtectedStorageAddAndVerify(entryForAdd, false, false); + } + + // TESTCASE: Add fails if Entry metadata does not match existing Entry + @Test + public void addProtectedStorageEntry_EntryNotmatchesRelevantPubKey() { + // Add a valid entry + ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); + doProtectedStorageAddAndVerify(entryForAdd, true, true); + + // Add an entry where metadata is different from first add, but otherwise is valid + entryForAdd = this.getProtectedStorageEntryForAdd(2, true, false); + doProtectedStorageAddAndVerify(entryForAdd, false, false); + } + + // TESTCASE: Add fails if Entry metadata does not match existing Entry and is not valid for add + @Test + public void addProtectedStorageEntry_EntryNotmatchesRelevantPubKeyNotisValidForAddOperation() { + // Add a valid entry + ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); + doProtectedStorageAddAndVerify(entryForAdd, true, true); + + // Add an entry where entry is not valid and metadata is different from first add + entryForAdd = this.getProtectedStorageEntryForAdd(2, false, false); + doProtectedStorageAddAndVerify(entryForAdd, false, false); + } + + /// Valid remove tests (isValidForRemove() and isMetadataEquals() return true) + + // TESTCASE: Removing an item after successfully added (remove seq # == add seq #) + @Test + public void remove_seqNrEqAddSeqNr() { + ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); + ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(1); + + doProtectedStorageAddAndVerify(entryForAdd, true, true); + + doProtectedStorageRemoveAndVerify(entryForRemove, false, false); + } + + // TESTCASE: Removing an item after successfully added (remove seq # > add seq #) + @Test + public void remove_seqNrGtAddSeqNr() { + ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); + ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(2); + + doProtectedStorageAddAndVerify(entryForAdd, true, true); + doProtectedStorageRemoveAndVerify(entryForRemove, true, true); + } + + // TESTCASE: Removing an item before it was added + @Test + public void remove_notExists() { + ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(1); + + doProtectedStorageRemoveAndVerify(entryForRemove, false, false); + } + + // TESTCASE: Removing an item after successfully adding (remove seq # < add seq #) + @Test + public void remove_seqNrLessAddSeqNr() { + ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(2); + ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(1); + + doProtectedStorageAddAndVerify(entryForAdd, true, true); + doProtectedStorageRemoveAndVerify(entryForRemove, false, false); + } + + // TESTCASE: Add after removed (same seq #) + @Test + public void add_afterRemoveSameSeqNr() { + ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); + doProtectedStorageAddAndVerify(entryForAdd, true, true); + + ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(2); + doProtectedStorageRemoveAndVerify(entryForRemove, true, true); + + doProtectedStorageAddAndVerify(entryForAdd, false, false); + } + + // TESTCASE: Add after removed (greater seq #) + @Test + public void add_afterRemoveGreaterSeqNr() { + ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); + doProtectedStorageAddAndVerify(entryForAdd, true, true); + + ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(2); + doProtectedStorageRemoveAndVerify(entryForRemove, true, true); + + entryForAdd = this.getProtectedStorageEntryForAdd(3); + doProtectedStorageAddAndVerify(entryForAdd, true, true); + } + + /// Invalid remove tests (isValidForRemoveOperation() || matchesRelevantPubKey()) returns false + + // TESTCASE: Remove fails if Entry isn't valid for remove + @Test + public void remove_EntryNotisValidForRemoveOperation() { + ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); + doProtectedStorageAddAndVerify(entryForAdd, true, true); + + ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(2, false, true); + doProtectedStorageRemoveAndVerify(entryForRemove, false, false); + } + + // TESTCASE: Remove fails if Entry is valid for remove, but metadata doesn't match remove target + @Test + public void remove_EntryNotmatchesRelevantPubKey() { + ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); + doProtectedStorageAddAndVerify(entryForAdd, true, true); + + ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(2, true, false); + doProtectedStorageRemoveAndVerify(entryForRemove, false, false); + } + + // TESTCASE: Remove fails if Entry is not valid for remove and metadata doesn't match remove target + @Test + public void remove_EntryNotisValidForRemoveOperationNotmatchesRelevantPubKey() { + ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); + doProtectedStorageAddAndVerify(entryForAdd, true, true); + + ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(2, false, false); + doProtectedStorageRemoveAndVerify(entryForRemove, false, false); + } + + + // TESTCASE: Add after removed (lower seq #) + @Test + public void add_afterRemoveLessSeqNr() { + ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(2); + doProtectedStorageAddAndVerify(entryForAdd, true, true); + + ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(3); + doProtectedStorageRemoveAndVerify(entryForRemove, true, true); + + entryForAdd = this.getProtectedStorageEntryForAdd(1); + doProtectedStorageAddAndVerify(entryForAdd, false, false); + } + + // TESTCASE: Received remove for nonexistent item that was later received + // XXXBUGXXX: There may be cases where removes are reordered with adds (remove during pending GetDataRequest?). + // The proper behavior may be to not add the late messages, but the current code will successfully add them + // even in the AddOncePayload (mailbox) case. + @Test + public void remove_lateAdd() { + ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); + ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(2); + + doProtectedStorageRemoveAndVerify(entryForRemove, false, false); + + // should be (false, false) + doProtectedStorageAddAndVerify(entryForAdd, true, true); + } + } + + /** + * Runs the common test cases defined in ProtectedStorageEntryTestBase against a ProtectedStorageEntry + * wrapper and ProtectedStoragePayload payload. + */ + public static class ProtectedStorageEntryTest extends ProtectedStorageEntryTestBase { + + @Override + protected ProtectedStoragePayload createInstance(KeyPair payloadOwnerKeys) { + return new ProtectedStoragePayloadStub(payloadOwnerKeys.getPublic()); + } + + @Override + protected Class getEntryClass() { + return ProtectedStorageEntry.class; + } + + static RefreshOfferMessage buildRefreshOfferMessage(ProtectedStoragePayload protectedStoragePayload, + KeyPair ownerKeys, + int sequenceNumber) throws CryptoException { + + P2PDataStorage.ByteArray hashOfPayload = P2PDataStorage.get32ByteHashAsByteArray(protectedStoragePayload); + + byte[] hashOfDataAndSeqNr = P2PDataStorage.get32ByteHash(new P2PDataStorage.DataAndSeqNrPair(protectedStoragePayload, sequenceNumber)); + byte[] signature = Sig.sign(ownerKeys.getPrivate(), hashOfDataAndSeqNr); + return new RefreshOfferMessage(hashOfDataAndSeqNr, signature, hashOfPayload.bytes, sequenceNumber); + } + + RefreshOfferMessage buildRefreshOfferMessage(ProtectedStorageEntry protectedStorageEntry, KeyPair ownerKeys, int sequenceNumber) throws CryptoException { + return buildRefreshOfferMessage(protectedStorageEntry.getProtectedStoragePayload(), ownerKeys, sequenceNumber); + } + + void doRefreshTTLAndVerify(RefreshOfferMessage refreshOfferMessage, boolean expectedReturnValue, boolean expectStateChange) { + SavedTestState beforeState = this.testState.saveTestState(refreshOfferMessage); + + boolean returnValue = this.doRefreshTTL(refreshOfferMessage); + + if (!this.useMessageHandler) + Assert.assertEquals(expectedReturnValue, returnValue); + + this.testState.verifyRefreshTTL(beforeState, refreshOfferMessage, expectStateChange, this.expectIsDataOwner()); + } + + // TESTCASE: Refresh an entry that doesn't exist + @Test + public void refreshTTL_noExist() throws CryptoException { + ProtectedStorageEntry entry = this.getProtectedStorageEntryForAdd(1); + + doRefreshTTLAndVerify(buildRefreshOfferMessage(entry, this.payloadOwnerKeys,1), false, false); + } + + // TESTCASE: Refresh an entry where seq # is equal to last seq # seen + @Test + public void refreshTTL_existingEntry() throws CryptoException { + ProtectedStorageEntry entry = this.getProtectedStorageEntryForAdd(1); + doProtectedStorageAddAndVerify(entry, true, true); + + doRefreshTTLAndVerify(buildRefreshOfferMessage(entry, this.payloadOwnerKeys,1), false, false); + } + + // TESTCASE: Duplicate refresh message (same seq #) + @Test + public void refreshTTL_duplicateRefreshSeqNrEqual() throws CryptoException { + ProtectedStorageEntry entry = this.getProtectedStorageEntryForAdd(1); + doProtectedStorageAddAndVerify(entry, true, true); + + this.testState.incrementClock(); + + doRefreshTTLAndVerify(buildRefreshOfferMessage(entry, this.payloadOwnerKeys, 2), true, true); + + this.testState.incrementClock(); + + doRefreshTTLAndVerify(buildRefreshOfferMessage(entry, this.payloadOwnerKeys, 2), false, false); + } + + // TESTCASE: Duplicate refresh message (greater seq #) + @Test + public void refreshTTL_duplicateRefreshSeqNrGreater() throws CryptoException { + ProtectedStorageEntry entry = this.getProtectedStorageEntryForAdd(1); + doProtectedStorageAddAndVerify(entry, true, true); + + this.testState.incrementClock(); + + doRefreshTTLAndVerify(buildRefreshOfferMessage(entry, this.payloadOwnerKeys,2), true, true); + + this.testState.incrementClock(); + + doRefreshTTLAndVerify(buildRefreshOfferMessage(entry, this.payloadOwnerKeys,3), true, true); + } + + // TESTCASE: Duplicate refresh message (lower seq #) + @Test + public void refreshTTL_duplicateRefreshSeqNrLower() throws CryptoException { + ProtectedStorageEntry entry = this.getProtectedStorageEntryForAdd(1); + doProtectedStorageAddAndVerify(entry, true, true); + + this.testState.incrementClock(); + + doRefreshTTLAndVerify(buildRefreshOfferMessage(entry, this.payloadOwnerKeys,3), true, true); + + this.testState.incrementClock(); + + doRefreshTTLAndVerify(buildRefreshOfferMessage(entry, this.payloadOwnerKeys,2), false, false); + } + + // TESTCASE: Refresh previously removed entry + @Test + public void refreshTTL_refreshAfterRemove() throws CryptoException { + ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); + ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(2); + + doProtectedStorageAddAndVerify(entryForAdd, true, true); + doProtectedStorageRemoveAndVerify(entryForRemove, true, true); + + doRefreshTTLAndVerify(buildRefreshOfferMessage(entryForAdd, this.payloadOwnerKeys,3), false, false); + } + + // TESTCASE: Refresh an entry, but owner doesn't match PubKey of original add owner + @Test + public void refreshTTL_refreshEntryOwnerOriginalOwnerMismatch() throws CryptoException, NoSuchAlgorithmException { + ProtectedStorageEntry entry = this.getProtectedStorageEntryForAdd(1); + doProtectedStorageAddAndVerify(entry, true, true); + + KeyPair notOwner = TestUtils.generateKeyPair(); + doRefreshTTLAndVerify(buildRefreshOfferMessage(entry, notOwner, 2), false, false); + } + } + + /** + * Runs the common test cases defined in ProtectedStorageEntryTestBase against a ProtectedStorageEntry + * wrapper and PersistableExpirableProtectedStoragePayload payload. + */ + public static class PersistableExpirableProtectedStoragePayloadStubTest extends ProtectedStorageEntryTestBase { + @Override + protected ProtectedStoragePayload createInstance(KeyPair payloadOwnerKeys) { + return new PersistableExpirableProtectedStoragePayloadStub(payloadOwnerKeys.getPublic()); + } + + @Override + protected Class getEntryClass() { + return ProtectedStorageEntry.class; + } + + } + + /** + * Runs the common test cases defined in ProtectedStorageEntryTestBase against a ProtectedMailboxStorageEntry + * wrapper and MailboxStoragePayload payload. + */ + public static class MailboxPayloadTest extends ProtectedStorageEntryTestBase { + + @Override + protected ProtectedStoragePayload createInstance(KeyPair payloadOwnerKeys) { + return TestState.buildMailboxStoragePayload(payloadOwnerKeys.getPublic(), payloadOwnerKeys.getPublic()); + } + + @Override + protected Class getEntryClass() { + return ProtectedMailboxStorageEntry.class; + } + + @Override + @Before + public void setUp() throws CryptoException, NoSuchAlgorithmException { + super.setUp(); + + // Deep in the bowels of protobuf we grab the messageID from the version module. This is required to hash the + // full MailboxStoragePayload so make sure it is initialized. + Version.setBaseCryptoNetworkId(1); + } + + @Override + boolean doRemove(ProtectedStorageEntry entry) { + if (this.useMessageHandler) { + Connection mockedConnection = mock(Connection.class); + when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(TestState.getTestNodeAddress())); + + testState.mockedStorage.onMessage(new RemoveMailboxDataMessage((ProtectedMailboxStorageEntry) entry), mockedConnection); + + return true; + } else { + // XXX: All external callers just pass in true, a future patch can remove the argument. + return testState.mockedStorage.remove(entry, TestState.getTestNodeAddress(), true); + } + } + + // TESTCASE: Add after removed when add-once required (greater seq #) + @Override + @Test + public void add_afterRemoveGreaterSeqNr() { + ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); + doProtectedStorageAddAndVerify(entryForAdd, true, true); + + ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(2); + doProtectedStorageRemoveAndVerify(entryForRemove, true, true); + + entryForAdd = this.getProtectedStorageEntryForAdd(3); + doProtectedStorageAddAndVerify(entryForAdd, false, false); + } + } +} diff --git a/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageRemoveExpiredTest.java b/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageRemoveExpiredTest.java new file mode 100644 index 00000000000..7814004cd2e --- /dev/null +++ b/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageRemoveExpiredTest.java @@ -0,0 +1,196 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.network.p2p.storage; + +import bisq.network.p2p.TestUtils; +import bisq.network.p2p.storage.mocks.ExpirableProtectedStoragePayloadStub; +import bisq.network.p2p.storage.mocks.PersistableExpirableProtectedStoragePayloadStub; +import bisq.network.p2p.storage.mocks.PersistableNetworkPayloadStub; +import bisq.network.p2p.storage.mocks.ProtectedStoragePayloadStub; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; +import bisq.network.p2p.storage.payload.ProtectedStoragePayload; + +import bisq.common.app.Version; +import bisq.common.crypto.CryptoException; + +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; + +import java.util.concurrent.TimeUnit; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static bisq.network.p2p.storage.TestState.*; + +/** + * Tests of the P2PDataStore behavior that expires old Entrys periodically. + */ +public class P2PDataStorageRemoveExpiredTest { + private TestState testState; + + @Before + public void setUp() { + this.testState = new TestState(); + + // Deep in the bowels of protobuf we grab the messageID from the version module. This is required to hash the + // full MailboxStoragePayload so make sure it is initialized. + Version.setBaseCryptoNetworkId(1); + } + + // TESTCASE: Correctly skips entries that are not expirable + @Test + public void removeExpiredEntries_SkipsNonExpirableEntries() throws NoSuchAlgorithmException, CryptoException { + KeyPair ownerKeys = TestUtils.generateKeyPair(); + ProtectedStoragePayload protectedStoragePayload = new ProtectedStoragePayloadStub(ownerKeys.getPublic()); + ProtectedStorageEntry protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); + Assert.assertTrue(this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, TestState.getTestNodeAddress(), null, true)); + + SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); + this.testState.mockedStorage.removeExpiredEntries(); + + this.testState.verifyProtectedStorageRemove(beforeState, protectedStorageEntry, false, false, false, false); + } + + // TESTCASE: Correctly skips all PersistableNetworkPayloads since they are not expirable + @Test + public void removeExpiredEntries_skipsPersistableNetworkPayload() { + PersistableNetworkPayload persistableNetworkPayload = new PersistableNetworkPayloadStub(true); + + Assert.assertTrue(this.testState.mockedStorage.addPersistableNetworkPayload(persistableNetworkPayload,getTestNodeAddress(), true, true, false, false)); + + this.testState.mockedStorage.removeExpiredEntries(); + + Assert.assertTrue(this.testState.mockedStorage.getAppendOnlyDataStoreMap().containsKey(new P2PDataStorage.ByteArray(persistableNetworkPayload.getHash()))); + } + + // TESTCASE: Correctly skips non-persistable entries that are not expired + @Test + public void removeExpiredEntries_SkipNonExpiredExpirableEntries() throws CryptoException, NoSuchAlgorithmException { + KeyPair ownerKeys = TestUtils.generateKeyPair(); + ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayloadStub(ownerKeys.getPublic()); + ProtectedStorageEntry protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); + Assert.assertTrue(this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, TestState.getTestNodeAddress(), null, true)); + + SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); + this.testState.mockedStorage.removeExpiredEntries(); + + this.testState.verifyProtectedStorageRemove(beforeState, protectedStorageEntry, false, false, false, false); + } + + // TESTCASE: Correctly expires non-persistable entries that are expired + @Test + public void removeExpiredEntries_ExpiresExpiredExpirableEntries() throws CryptoException, NoSuchAlgorithmException { + KeyPair ownerKeys = TestUtils.generateKeyPair(); + ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayloadStub(ownerKeys.getPublic(), 0); + ProtectedStorageEntry protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); + Assert.assertTrue(this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, TestState.getTestNodeAddress(), null, true)); + + // Increment the clock by an hour which will cause the Payloads to be outside the TTL range + this.testState.incrementClock(); + + SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); + this.testState.mockedStorage.removeExpiredEntries(); + + this.testState.verifyProtectedStorageRemove(beforeState, protectedStorageEntry, true, false, false, false); + } + + // TESTCASE: Correctly skips persistable entries that are not expired + @Test + public void removeExpiredEntries_SkipNonExpiredPersistableExpirableEntries() throws CryptoException, NoSuchAlgorithmException { + KeyPair ownerKeys = TestUtils.generateKeyPair(); + ProtectedStoragePayload protectedStoragePayload = new PersistableExpirableProtectedStoragePayloadStub(ownerKeys.getPublic()); + ProtectedStorageEntry protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); + Assert.assertTrue(this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, TestState.getTestNodeAddress(), null, true)); + + SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); + this.testState.mockedStorage.removeExpiredEntries(); + + this.testState.verifyProtectedStorageRemove(beforeState, protectedStorageEntry, false, false, false, false); + } + + // TESTCASE: Correctly expires persistable entries that are expired + @Test + public void removeExpiredEntries_ExpiresExpiredPersistableExpirableEntries() throws CryptoException, NoSuchAlgorithmException { + KeyPair ownerKeys = TestUtils.generateKeyPair(); + ProtectedStoragePayload protectedStoragePayload = new PersistableExpirableProtectedStoragePayloadStub(ownerKeys.getPublic(), 0); + ProtectedStorageEntry protectedStorageEntry = testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); + Assert.assertTrue(testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, TestState.getTestNodeAddress(), null, true)); + + // Increment the clock by an hour which will cause the Payloads to be outside the TTL range + this.testState.incrementClock(); + + SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); + this.testState.mockedStorage.removeExpiredEntries(); + + this.testState.verifyProtectedStorageRemove(beforeState, protectedStorageEntry, true, false, false, false); + } + + // TESTCASE: Ensure we try to purge old entries sequence number map when size exceeds the maximum size + // and that entries less than PURGE_AGE_DAYS remain + @Test + public void removeExpiredEntries_PurgeSeqNrMap() throws CryptoException, NoSuchAlgorithmException { + final int initialClockIncrement = 5; + + // Add 4 entries to our sequence number map that will be purged + KeyPair purgedOwnerKeys = TestUtils.generateKeyPair(); + ProtectedStoragePayload purgedProtectedStoragePayload = new PersistableExpirableProtectedStoragePayloadStub(purgedOwnerKeys.getPublic(), 0); + ProtectedStorageEntry purgedProtectedStorageEntry = testState.mockedStorage.getProtectedStorageEntry(purgedProtectedStoragePayload, purgedOwnerKeys); + + Assert.assertTrue(testState.mockedStorage.addProtectedStorageEntry(purgedProtectedStorageEntry, TestState.getTestNodeAddress(), null, true)); + + for (int i = 0; i < 4; ++i) { + KeyPair ownerKeys = TestUtils.generateKeyPair(); + ProtectedStoragePayload protectedStoragePayload = new PersistableExpirableProtectedStoragePayloadStub(ownerKeys.getPublic(), 0); + ProtectedStorageEntry tmpEntry = testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); + Assert.assertTrue(testState.mockedStorage.addProtectedStorageEntry(tmpEntry, TestState.getTestNodeAddress(), null, true)); + } + + // Increment the time by 5 days which is less than the purge requirement. This will allow the map to have + // some values that will be purged and others that will stay. + this.testState.clockFake.increment(TimeUnit.DAYS.toMillis(initialClockIncrement)); + + // Add a final entry that will not be purged + KeyPair keepOwnerKeys = TestUtils.generateKeyPair(); + ProtectedStoragePayload keepProtectedStoragePayload = new PersistableExpirableProtectedStoragePayloadStub(keepOwnerKeys.getPublic(), 0); + ProtectedStorageEntry keepProtectedStorageEntry = testState.mockedStorage.getProtectedStorageEntry(keepProtectedStoragePayload, keepOwnerKeys); + + Assert.assertTrue(testState.mockedStorage.addProtectedStorageEntry(keepProtectedStorageEntry, TestState.getTestNodeAddress(), null, true)); + + // P2PDataStorage::PURGE_AGE_DAYS == 10 days + // Advance time past it so they will be valid purge targets + this.testState.clockFake.increment(TimeUnit.DAYS.toMillis(P2PDataStorage.PURGE_AGE_DAYS + 1 - initialClockIncrement)); + + // The first entry (11 days old) should be purged + SavedTestState beforeState = this.testState.saveTestState(purgedProtectedStorageEntry); + this.testState.mockedStorage.removeExpiredEntries(); + this.testState.verifyProtectedStorageRemove(beforeState, purgedProtectedStorageEntry, true, false, false, false); + + // Which means that an addition of a purged entry should succeed. + beforeState = this.testState.saveTestState(purgedProtectedStorageEntry); + Assert.assertTrue(this.testState.mockedStorage.addProtectedStorageEntry(purgedProtectedStorageEntry, TestState.getTestNodeAddress(), null, false)); + this.testState.verifyProtectedStorageAdd(beforeState, purgedProtectedStorageEntry, true, false); + + // The second entry (5 days old) should still exist which means trying to add it again should fail. + beforeState = this.testState.saveTestState(keepProtectedStorageEntry); + Assert.assertFalse(this.testState.mockedStorage.addProtectedStorageEntry(keepProtectedStorageEntry, TestState.getTestNodeAddress(), null, false)); + this.testState.verifyProtectedStorageAdd(beforeState, keepProtectedStorageEntry, false, false); + } +} diff --git a/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageTest.java b/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageTest.java deleted file mode 100644 index 41964e2715c..00000000000 --- a/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStorageTest.java +++ /dev/null @@ -1,1526 +0,0 @@ -package bisq.network.p2p.storage; - -import bisq.network.p2p.NodeAddress; -import bisq.network.p2p.PrefixedSealedAndSignedMessage; -import bisq.network.p2p.TestUtils; -import bisq.network.p2p.mocks.MockPayload; -import bisq.network.p2p.network.CloseConnectionReason; -import bisq.network.p2p.network.Connection; -import bisq.network.p2p.network.NetworkNode; -import bisq.network.p2p.peers.BroadcastHandler; -import bisq.network.p2p.peers.Broadcaster; -import bisq.network.p2p.storage.messages.AddDataMessage; -import bisq.network.p2p.storage.messages.AddPersistableNetworkPayloadMessage; -import bisq.network.p2p.storage.messages.BroadcastMessage; -import bisq.network.p2p.storage.messages.RefreshOfferMessage; -import bisq.network.p2p.storage.messages.RemoveDataMessage; -import bisq.network.p2p.storage.messages.RemoveMailboxDataMessage; -import bisq.network.p2p.storage.mocks.*; -import bisq.network.p2p.storage.payload.ExpirablePayload; -import bisq.network.p2p.storage.payload.MailboxStoragePayload; -import bisq.network.p2p.storage.payload.PersistableNetworkPayload; -import bisq.network.p2p.storage.payload.ProtectedMailboxStorageEntry; -import bisq.network.p2p.storage.payload.ProtectedStorageEntry; -import bisq.network.p2p.storage.payload.ProtectedStoragePayload; -import bisq.network.p2p.storage.payload.RequiresOwnerIsOnlinePayload; -import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreListener; -import bisq.network.p2p.storage.persistence.ProtectedDataStoreListener; -import bisq.network.p2p.storage.persistence.ResourceDataStoreService; -import bisq.network.p2p.storage.persistence.SequenceNumberMap; - -import bisq.common.app.Version; -import bisq.common.crypto.CryptoException; -import bisq.common.crypto.SealedAndSigned; -import bisq.common.crypto.Sig; -import bisq.common.proto.network.NetworkEnvelope; -import bisq.common.proto.persistable.PersistablePayload; -import bisq.common.storage.Storage; - -import java.security.KeyPair; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; - -import java.time.Clock; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.TimeUnit; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.experimental.runners.Enclosed; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import static org.mockito.Mockito.*; - -import org.mockito.ArgumentCaptor; - -@RunWith(Enclosed.class) -public class P2PDataStorageTest { - - // Test class used for validating the ExpirablePayload, RequiresOwnerIsOnlinePayload marker interfaces - static class ExpirableProtectedStoragePayload extends ProtectedStoragePayloadStub implements ExpirablePayload, RequiresOwnerIsOnlinePayload { - private long ttl; - - ExpirableProtectedStoragePayload(KeyPair ownerKeys) { - super(ownerKeys.getPublic()); - ttl = TimeUnit.DAYS.toMillis(90); - } - - ExpirableProtectedStoragePayload(KeyPair ownerKeys, long ttl) { - this(ownerKeys); - this.ttl = ttl; - } - - @Override - public NodeAddress getOwnerNodeAddress() { - return getTestNodeAddress(); - } - - @Override - public long getTTL() { - return this.ttl; - } - } - - // Common state for tests that initializes the P2PDataStore and mocks out the dependencies. Allows - // shared state verification between all tests. - static class TestState { - final P2PDataStorage mockedStorage; - final Broadcaster mockBroadcaster; - - final AppendOnlyDataStoreListener appendOnlyDataStoreListener; - final ProtectedDataStoreListener protectedDataStoreListener; - final HashMapChangedListener hashMapChangedListener; - final Storage mockSeqNrStorage; - - TestState() { - this.mockBroadcaster = mock(Broadcaster.class); - this.mockSeqNrStorage = mock(Storage.class); - - this.mockedStorage = new P2PDataStorage(mock(NetworkNode.class), - this.mockBroadcaster, - new AppendOnlyDataStoreServiceFake(), - new ProtectedDataStoreServiceFake(), mock(ResourceDataStoreService.class), - this.mockSeqNrStorage, Clock.systemUTC()); - - this.appendOnlyDataStoreListener = mock(AppendOnlyDataStoreListener.class); - this.protectedDataStoreListener = mock(ProtectedDataStoreListener.class); - this.hashMapChangedListener = mock(HashMapChangedListener.class); - - this.mockedStorage.addHashMapChangedListener(this.hashMapChangedListener); - this.mockedStorage.addAppendOnlyDataStoreListener(this.appendOnlyDataStoreListener); - this.mockedStorage.addProtectedDataStoreListener(this.protectedDataStoreListener); - } - - void resetState() { - reset(this.mockBroadcaster); - reset(this.appendOnlyDataStoreListener); - reset(this.protectedDataStoreListener); - reset(this.hashMapChangedListener); - reset(this.mockSeqNrStorage); - } - } - - // Represents a snapshot of a TestState allowing easier verification of state before and after an operation. - static class SavedTestState { - final TestState state; - - // Used in PersistableNetworkPayload tests - PersistableNetworkPayload persistableNetworkPayloadBeforeOp; - - // Used in ProtectedStorageEntry tests - ProtectedStorageEntry protectedStorageEntryBeforeOp; - ProtectedStorageEntry protectedStorageEntryBeforeOpDataStoreMap; - - long creationTimestampBeforeUpdate; - - private SavedTestState(TestState state) { - this.state = state; - this.creationTimestampBeforeUpdate = 0; - this.state.resetState(); - } - - SavedTestState(TestState testState, PersistableNetworkPayload persistableNetworkPayload) { - this(testState); - P2PDataStorage.ByteArray hash = new P2PDataStorage.ByteArray(persistableNetworkPayload.getHash()); - this.persistableNetworkPayloadBeforeOp = testState.mockedStorage.getAppendOnlyDataStoreMap().get(hash); - } - - SavedTestState(TestState testState, ProtectedStorageEntry protectedStorageEntry) { - this(testState); - - P2PDataStorage.ByteArray storageHash = P2PDataStorage.getCompactHashAsByteArray(protectedStorageEntry.getProtectedStoragePayload()); - this.protectedStorageEntryBeforeOpDataStoreMap = testState.mockedStorage.getProtectedDataStoreMap().get(storageHash); - - P2PDataStorage.ByteArray hashMapHash = P2PDataStorage.get32ByteHashAsByteArray(protectedStorageEntry.getProtectedStoragePayload()); - this.protectedStorageEntryBeforeOp = testState.mockedStorage.getMap().get(hashMapHash); - - this.creationTimestampBeforeUpdate = (this.protectedStorageEntryBeforeOp != null) ? this.protectedStorageEntryBeforeOp.getCreationTimeStamp() : 0; - } - - SavedTestState(TestState testState, RefreshOfferMessage refreshOfferMessage) { - this(testState); - - P2PDataStorage.ByteArray hashMapHash = new P2PDataStorage.ByteArray(refreshOfferMessage.getHashOfPayload()); - this.protectedStorageEntryBeforeOp = testState.mockedStorage.getMap().get(hashMapHash); - - this.creationTimestampBeforeUpdate = (this.protectedStorageEntryBeforeOp != null) ? this.protectedStorageEntryBeforeOp.getCreationTimeStamp() : 0; - } - } - - private static NodeAddress getTestNodeAddress() { - return new NodeAddress("address", 8080); - } - - - /* - * Helper functions that create Payloads and Entrys for the various tests. This allow fabrication of a variety of - * valid and invalid Entrys that are used to test the correct behavior. - */ - private static ProtectedStorageEntry buildProtectedStorageEntry( - ProtectedStoragePayload protectedStoragePayload, - KeyPair entryOwnerKeys, - KeyPair entrySignerKeys, - int sequenceNumber) throws CryptoException { - byte[] hashOfDataAndSeqNr = P2PDataStorage.get32ByteHash(new P2PDataStorage.DataAndSeqNrPair(protectedStoragePayload, sequenceNumber)); - byte[] signature = Sig.sign(entrySignerKeys.getPrivate(), hashOfDataAndSeqNr); - - return new ProtectedStorageEntry(protectedStoragePayload, entryOwnerKeys.getPublic(), sequenceNumber, signature); - } - - private static MailboxStoragePayload buildMailboxStoragePayload(PublicKey payloadSenderPubKeyForAddOperation, - PublicKey payloadOwnerPubKey) { - - // Create unused, but well-formed sealedAndSigned so that a hash can be taken (internal to P2PDataStorage). Not actually validated. - SealedAndSigned sealedAndSigned = new SealedAndSigned(new byte[] { 0 }, new byte[] { 0 }, new byte[] { 0 }, payloadOwnerPubKey); - PrefixedSealedAndSignedMessage prefixedSealedAndSignedMessage = - new PrefixedSealedAndSignedMessage(new NodeAddress("host", 1000), sealedAndSigned, new byte[] { 0 }, - "UUID"); - - return new MailboxStoragePayload( - prefixedSealedAndSignedMessage, payloadSenderPubKeyForAddOperation, payloadOwnerPubKey); - } - - private static ProtectedStorageEntry buildProtectedMailboxStorageEntry( - PublicKey payloadSenderPubKeyForAddOperation, - PublicKey payloadOwnerPubKey, - PrivateKey entrySigner, - PublicKey entryOwnerPubKey, - PublicKey entryReceiversPubKey, - int sequenceNumber) throws CryptoException { - - MailboxStoragePayload payload = buildMailboxStoragePayload(payloadSenderPubKeyForAddOperation, payloadOwnerPubKey); - - byte[] hashOfDataAndSeqNr = P2PDataStorage.get32ByteHash(new P2PDataStorage.DataAndSeqNrPair(payload, sequenceNumber)); - byte[] signature = Sig.sign(entrySigner, hashOfDataAndSeqNr); - return new ProtectedMailboxStorageEntry(payload, - entryOwnerPubKey, sequenceNumber, signature, entryReceiversPubKey); - } - - private static RefreshOfferMessage buildRefreshOfferMessage(ProtectedStoragePayload protectedStoragePayload, - KeyPair ownerKeys, - int sequenceNumber) throws CryptoException { - - P2PDataStorage.ByteArray hashOfPayload = P2PDataStorage.get32ByteHashAsByteArray(protectedStoragePayload); - - byte[] hashOfDataAndSeqNr = P2PDataStorage.get32ByteHash(new P2PDataStorage.DataAndSeqNrPair(protectedStoragePayload, sequenceNumber)); - byte[] signature = Sig.sign(ownerKeys.getPrivate(), hashOfDataAndSeqNr); - return new RefreshOfferMessage(hashOfDataAndSeqNr, signature, hashOfPayload.bytes, sequenceNumber); - } - - /* - * Common test helpers that verify the correct events were signaled based on the test expectation and before/after states. - */ - private static void verifySequenceNumberMapWriteContains(TestState testState, - P2PDataStorage.ByteArray payloadHash, - int sequenceNumber) { - final ArgumentCaptor captor = ArgumentCaptor.forClass(SequenceNumberMap.class); - verify(testState.mockSeqNrStorage).queueUpForSave(captor.capture(), anyLong()); - - SequenceNumberMap savedMap = captor.getValue(); - Assert.assertEquals(sequenceNumber, savedMap.get(payloadHash).sequenceNr); - } - - private static void verifyPersistableAdd(TestState currentState, - SavedTestState beforeState, - PersistableNetworkPayload persistableNetworkPayload, - boolean expectedStateChange, - boolean expectedBroadcastAndListenersSignaled, - boolean expectedIsDataOwner) { - P2PDataStorage.ByteArray hash = new P2PDataStorage.ByteArray(persistableNetworkPayload.getHash()); - - if (expectedStateChange) { - // Payload is accessible from get() - Assert.assertEquals(persistableNetworkPayload, currentState.mockedStorage.getAppendOnlyDataStoreMap().get(hash)); - } else { - // On failure, just ensure the state remained the same as before the add - if (beforeState.persistableNetworkPayloadBeforeOp != null) - Assert.assertEquals(beforeState.persistableNetworkPayloadBeforeOp, currentState.mockedStorage.getAppendOnlyDataStoreMap().get(hash)); - else - Assert.assertNull(currentState.mockedStorage.getAppendOnlyDataStoreMap().get(hash)); - } - - if (expectedStateChange && expectedBroadcastAndListenersSignaled) { - // Broadcast Called - verify(currentState.mockBroadcaster).broadcast(any(AddPersistableNetworkPayloadMessage.class), any(NodeAddress.class), - eq(null), eq(expectedIsDataOwner)); - - // Verify the listeners were updated once - verify(currentState.appendOnlyDataStoreListener).onAdded(persistableNetworkPayload); - - } else { - verify(currentState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class), any(BroadcastHandler.Listener.class), anyBoolean()); - - // Verify the listeners were never updated - verify(currentState.appendOnlyDataStoreListener, never()).onAdded(persistableNetworkPayload); - } - } - - private static void verifyProtectedStorageAdd(TestState currentState, - SavedTestState beforeState, - ProtectedStorageEntry protectedStorageEntry, - boolean expectedStateChange, - boolean expectedIsDataOwner) { - P2PDataStorage.ByteArray hashMapHash = P2PDataStorage.get32ByteHashAsByteArray(protectedStorageEntry.getProtectedStoragePayload()); - P2PDataStorage.ByteArray storageHash = P2PDataStorage.getCompactHashAsByteArray(protectedStorageEntry.getProtectedStoragePayload()); - - if (expectedStateChange) { - Assert.assertEquals(protectedStorageEntry, currentState.mockedStorage.getMap().get(hashMapHash)); - - // PersistablePayload payloads need to be written to disk and listeners signaled... unless the hash already exists in the protectedDataStore. - // Note: this behavior is different from the HashMap listeners that are signaled on an increase in seq #, even if the hash already exists. - // TODO: Should the behavior be identical between this and the HashMap listeners? - // TODO: Do we want ot overwrite stale values in order to persist updated sequence numbers and timestamps? - if (protectedStorageEntry.getProtectedStoragePayload() instanceof PersistablePayload && beforeState.protectedStorageEntryBeforeOpDataStoreMap == null) { - Assert.assertEquals(protectedStorageEntry, currentState.mockedStorage.getProtectedDataStoreMap().get(storageHash)); - verify(currentState.protectedDataStoreListener).onAdded(protectedStorageEntry); - } else { - Assert.assertEquals(beforeState.protectedStorageEntryBeforeOpDataStoreMap, currentState.mockedStorage.getProtectedDataStoreMap().get(storageHash)); - verify(currentState.protectedDataStoreListener, never()).onAdded(protectedStorageEntry); - } - - verify(currentState.hashMapChangedListener).onAdded(protectedStorageEntry); - - final ArgumentCaptor captor = ArgumentCaptor.forClass(BroadcastMessage.class); - verify(currentState.mockBroadcaster).broadcast(captor.capture(), any(NodeAddress.class), - eq(null), eq(expectedIsDataOwner)); - - BroadcastMessage broadcastMessage = captor.getValue(); - Assert.assertTrue(broadcastMessage instanceof AddDataMessage); - Assert.assertEquals(protectedStorageEntry, ((AddDataMessage) broadcastMessage).getProtectedStorageEntry()); - - verifySequenceNumberMapWriteContains(currentState, P2PDataStorage.get32ByteHashAsByteArray(protectedStorageEntry.getProtectedStoragePayload()), protectedStorageEntry.getSequenceNumber()); - } else { - Assert.assertEquals(beforeState.protectedStorageEntryBeforeOp, currentState.mockedStorage.getMap().get(hashMapHash)); - Assert.assertEquals(beforeState.protectedStorageEntryBeforeOpDataStoreMap, currentState.mockedStorage.getProtectedDataStoreMap().get(storageHash)); - - verify(currentState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class), any(BroadcastHandler.Listener.class), anyBoolean()); - - // Internal state didn't change... nothing should be notified - verify(currentState.hashMapChangedListener, never()).onAdded(protectedStorageEntry); - verify(currentState.protectedDataStoreListener, never()).onAdded(protectedStorageEntry); - verify(currentState.mockSeqNrStorage, never()).queueUpForSave(any(SequenceNumberMap.class), anyLong()); - } - } - - private static void verifyProtectedStorageRemove(TestState currentState, - SavedTestState beforeState, - ProtectedStorageEntry protectedStorageEntry, - boolean expectedStateChange, - boolean expectedIsDataOwner) { - P2PDataStorage.ByteArray hashMapHash = P2PDataStorage.get32ByteHashAsByteArray(protectedStorageEntry.getProtectedStoragePayload()); - P2PDataStorage.ByteArray storageHash = P2PDataStorage.getCompactHashAsByteArray(protectedStorageEntry.getProtectedStoragePayload()); - - if (expectedStateChange) { - Assert.assertNull(currentState.mockedStorage.getMap().get(hashMapHash)); - - if (protectedStorageEntry.getProtectedStoragePayload() instanceof PersistablePayload) { - Assert.assertNull(currentState.mockedStorage.getProtectedDataStoreMap().get(storageHash)); - - verify(currentState.protectedDataStoreListener).onRemoved(protectedStorageEntry); - } - - verify(currentState.hashMapChangedListener).onRemoved(protectedStorageEntry); - - verify(currentState.mockBroadcaster).broadcast(any(BroadcastMessage.class), any(NodeAddress.class), eq(null), eq(expectedIsDataOwner)); - - verifySequenceNumberMapWriteContains(currentState, P2PDataStorage.get32ByteHashAsByteArray(protectedStorageEntry.getProtectedStoragePayload()), protectedStorageEntry.getSequenceNumber()); - } else { - Assert.assertEquals(beforeState.protectedStorageEntryBeforeOp, currentState.mockedStorage.getMap().get(hashMapHash)); - - verify(currentState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class), any(BroadcastHandler.Listener.class), anyBoolean()); - verify(currentState.hashMapChangedListener, never()).onAdded(protectedStorageEntry); - verify(currentState.protectedDataStoreListener, never()).onAdded(protectedStorageEntry); - verify(currentState.mockSeqNrStorage, never()).queueUpForSave(any(SequenceNumberMap.class), anyLong()); - } - } - - private static void verifyRefreshTTL(TestState currentState, - SavedTestState beforeState, - RefreshOfferMessage refreshOfferMessage, - boolean expectedStateChange, - boolean expectedIsDataOwner) { - P2PDataStorage.ByteArray payloadHash = new P2PDataStorage.ByteArray(refreshOfferMessage.getHashOfPayload()); - - ProtectedStorageEntry entryAfterRefresh = currentState.mockedStorage.getMap().get(payloadHash); - - if (expectedStateChange) { - Assert.assertNotNull(entryAfterRefresh); - Assert.assertEquals(refreshOfferMessage.getSequenceNumber(), entryAfterRefresh.getSequenceNumber()); - Assert.assertEquals(refreshOfferMessage.getSignature(), entryAfterRefresh.getSignature()); - Assert.assertTrue(entryAfterRefresh.getCreationTimeStamp() > beforeState.creationTimestampBeforeUpdate); - - final ArgumentCaptor captor = ArgumentCaptor.forClass(BroadcastMessage.class); - verify(currentState.mockBroadcaster).broadcast(captor.capture(), any(NodeAddress.class), - eq(null), eq(expectedIsDataOwner)); - - BroadcastMessage broadcastMessage = captor.getValue(); - Assert.assertTrue(broadcastMessage instanceof RefreshOfferMessage); - Assert.assertEquals(refreshOfferMessage, broadcastMessage); - - verifySequenceNumberMapWriteContains(currentState, payloadHash, refreshOfferMessage.getSequenceNumber()); - } else { - - // Verify the existing entry is unchanged - if (beforeState.protectedStorageEntryBeforeOp != null) { - Assert.assertEquals(entryAfterRefresh, beforeState.protectedStorageEntryBeforeOp); - Assert.assertEquals(beforeState.protectedStorageEntryBeforeOp.getSequenceNumber(), entryAfterRefresh.getSequenceNumber()); - Assert.assertEquals(beforeState.protectedStorageEntryBeforeOp.getSignature(), entryAfterRefresh.getSignature()); - Assert.assertEquals(beforeState.creationTimestampBeforeUpdate, entryAfterRefresh.getCreationTimeStamp()); - } - - verify(currentState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class), any(BroadcastHandler.Listener.class), anyBoolean()); - verify(currentState.mockSeqNrStorage, never()).queueUpForSave(any(SequenceNumberMap.class), anyLong()); - } - } - - static class UnsupportedBroadcastMessage extends BroadcastMessage { - - UnsupportedBroadcastMessage() { - super(0); - } - } - - public static class OnMessageHandlerTest { - TestState testState; - - @Before - public void setup() { - this.testState = new TestState(); - } - - @Test - public void invalidBroadcastMessage() { - NetworkEnvelope envelope = new MockPayload("Mock"); - - Connection mockedConnection = mock(Connection.class); - when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(getTestNodeAddress())); - - this.testState.mockedStorage.onMessage(envelope, mockedConnection); - - verify(this.testState.appendOnlyDataStoreListener, never()).onAdded(any(PersistableNetworkPayload.class)); - verify(this.testState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class), eq(null), anyBoolean()); - } - - @Test - public void unsupportedBroadcastMessage() { - NetworkEnvelope envelope = new UnsupportedBroadcastMessage(); - - Connection mockedConnection = mock(Connection.class); - when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(getTestNodeAddress())); - - this.testState.mockedStorage.onMessage(envelope, mockedConnection); - - verify(this.testState.appendOnlyDataStoreListener, never()).onAdded(any(PersistableNetworkPayload.class)); - verify(this.testState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class), eq(null), anyBoolean()); - } - - @Test - public void invalidConnectionObject() { - PersistableNetworkPayload persistableNetworkPayload = new PersistableNetworkPayloadStub(true); - NetworkEnvelope envelope = new AddPersistableNetworkPayloadMessage(persistableNetworkPayload); - - Connection mockedConnection = mock(Connection.class); - when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.empty()); - - this.testState.mockedStorage.onMessage(envelope, mockedConnection); - - verify(this.testState.appendOnlyDataStoreListener, never()).onAdded(any(PersistableNetworkPayload.class)); - verify(this.testState.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class), eq(null), anyBoolean()); - } - } - - - /* - * Run each test case through all 4 entry points to validate the correct behavior: - * 1. addPersistableNetworkPayloadFromInitialRequest() - * 2. addPersistableNetworkPayload(reBroadcast=false) - * 3. addPersistableNetworkPayload(reBroadcast=true) - * 4. onMessage() - */ - @RunWith(Parameterized.class) - public abstract static class AddPersistableNetworkPayloadTest { - TestState testState; - - @Parameterized.Parameter(0) - public TestCase testCase; - - @Parameterized.Parameter(1) - public boolean allowBroadcast; - - @Parameterized.Parameter(2) - public boolean reBroadcast; - - @Parameterized.Parameter(3) - public boolean checkDate; - - PersistableNetworkPayload persistableNetworkPayload; - - abstract PersistableNetworkPayload createInstance(); - - enum TestCase { - PUBLIC_API, - ON_MESSAGE, - INIT, - } - - boolean expectBroadcastOnStateChange() { - return this.testCase != TestCase.INIT; - } - - boolean expectedIsDataOwner() { - return this.testCase == TestCase.PUBLIC_API; - } - - void doAddAndVerify(PersistableNetworkPayload persistableNetworkPayload, boolean expectedReturnValue, boolean expectedStateChange) { - SavedTestState beforeState = new SavedTestState(this.testState, persistableNetworkPayload); - - if (this.testCase == TestCase.INIT) { - Assert.assertEquals(expectedReturnValue, this.testState.mockedStorage.addPersistableNetworkPayloadFromInitialRequest(persistableNetworkPayload)); - } else if (this.testCase == TestCase.PUBLIC_API) { - Assert.assertEquals(expectedReturnValue, - this.testState.mockedStorage.addPersistableNetworkPayload(persistableNetworkPayload, getTestNodeAddress(), true, this.allowBroadcast, this.reBroadcast, this.checkDate)); - } else { // onMessage - Connection mockedConnection = mock(Connection.class); - when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(getTestNodeAddress())); - - testState.mockedStorage.onMessage(new AddPersistableNetworkPayloadMessage(persistableNetworkPayload), mockedConnection); - } - - verifyPersistableAdd(this.testState, beforeState, persistableNetworkPayload, expectedStateChange, this.expectBroadcastOnStateChange(), this.expectedIsDataOwner()); - } - - @Before - public void setup() { - this.persistableNetworkPayload = this.createInstance(); - - this.testState = new TestState(); - } - - @Parameterized.Parameters(name = "{index}: Test with TestCase={0} allowBroadcast={1} reBroadcast={2} checkDate={3}") - public static Collection data() { - List data = new ArrayList<>(); - - // Init doesn't use other parameters - data.add(new Object[] { TestCase.INIT, false, false, false }); - - // onMessage doesn't use other parameters - data.add(new Object[] { TestCase.ON_MESSAGE, false, false, false }); - - // Client API uses two permutations - // Normal path - data.add(new Object[] { TestCase.PUBLIC_API, true, true, false }); - - // Refresh path - data.add(new Object[] { TestCase.PUBLIC_API, true, false, false }); - - return data; - } - - @Test - public void addPersistableNetworkPayload() { - // First add should succeed regardless of parameters - doAddAndVerify(this.persistableNetworkPayload, true, true); - } - - @Test - public void addPersistableNetworkPayloadDuplicate() { - doAddAndVerify(this.persistableNetworkPayload, true, true); - - // Second call only succeeds if reBroadcast was set or we are adding through the init - // path which just overwrites - boolean expectedReturnValue = this.reBroadcast || this.testCase == TestCase.INIT; - doAddAndVerify(this.persistableNetworkPayload, expectedReturnValue, false); - } - } - - public static class AddPersistableNetworkPayloadStubTest extends AddPersistableNetworkPayloadTest { - @Override - PersistableNetworkPayloadStub createInstance() { - return new PersistableNetworkPayloadStub(true); - } - - @Test - public void invalidHash() { - PersistableNetworkPayload persistableNetworkPayload = new PersistableNetworkPayloadStub(false); - - doAddAndVerify(persistableNetworkPayload, false, false); - } - } - - public static class AddPersistableDateTolerantPayloadTest extends AddPersistableNetworkPayloadTest { - - @Override - DateTolerantPayloadStub createInstance() { - return new DateTolerantPayloadStub(true); - - } - - @Test - public void outOfTolerance() { - PersistableNetworkPayload persistableNetworkPayload = new DateTolerantPayloadStub(false); - - // The onMessage path checks for tolerance - boolean expectedReturn = this.testCase != TestCase.ON_MESSAGE; - - doAddAndVerify(persistableNetworkPayload, expectedReturn, expectedReturn); - } - } - - /* - * Run each test through both entry points to validate the correct behavior: - * 1. Client API [addProtectedStorageEntry(), refreshTTL(), remove()] - * 2. onMessage() [AddDataMessage, RefreshOfferMessage, RemoveDataMessage] - * - * These Base tests do not handle the mailbox case. Those are found in the MailboxPayloadTest subclass that - * extends these tests to reuse the common test cases. - */ - @RunWith(Parameterized.class) - abstract public static class ProtectedStorageEntryTestBase { - TestState testState; - - protected abstract ProtectedStoragePayload createInstance(KeyPair payloadOwnerKeys); - - // Used for tests of ProtectedStorageEntry and subclasses - private ProtectedStoragePayload protectedStoragePayload; - KeyPair payloadOwnerKeys; - - @Parameterized.Parameter(0) - public boolean useMessageHandler; - - boolean expectIsDataOwner() { - // The onMessage handler variant should always broadcast with isDataOwner == false - // The Client API should always broadcast with isDataOwner == true - return !useMessageHandler; - } - - @Parameterized.Parameters(name = "{index}: Test with useMessageHandler={0}") - public static Collection data() { - List data = new ArrayList<>(); - - boolean[] vals = new boolean[]{true, false}; - - for (boolean useMessageHandler : vals) - data.add(new Object[]{useMessageHandler}); - - return data; - } - - @Before - public void setUp() throws CryptoException, NoSuchAlgorithmException { - this.testState = new TestState(); - - this.payloadOwnerKeys = TestUtils.generateKeyPair(); - this.protectedStoragePayload = createInstance(this.payloadOwnerKeys); - } - - boolean doRemove(ProtectedStorageEntry entry) { - if (this.useMessageHandler) { - Connection mockedConnection = mock(Connection.class); - when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(getTestNodeAddress())); - - testState.mockedStorage.onMessage(new RemoveDataMessage(entry), mockedConnection); - - return true; - } else { - // XXX: All callers just pass in true, a future patch can remove the argument. - return testState.mockedStorage.remove(entry, getTestNodeAddress(), true); - } - } - - boolean doAdd(ProtectedStorageEntry protectedStorageEntry) { - if (this.useMessageHandler) { - Connection mockedConnection = mock(Connection.class); - when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(getTestNodeAddress())); - - testState.mockedStorage.onMessage(new AddDataMessage(protectedStorageEntry), mockedConnection); - - return true; - } else { - // XXX: All external callers just pass in true for isDataOwner and allowBroadcast a future patch can - // remove the argument. - return this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, - getTestNodeAddress(), null, true); - } - } - - boolean doRefreshTTL(RefreshOfferMessage refreshOfferMessage) { - if (this.useMessageHandler) { - Connection mockedConnection = mock(Connection.class); - when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(getTestNodeAddress())); - - testState.mockedStorage.onMessage(refreshOfferMessage, mockedConnection); - - return true; - } else { - // XXX: All external callers just pass in true for isDataOwner a future patch can remove the argument. - return this.testState.mockedStorage.refreshTTL(refreshOfferMessage, getTestNodeAddress(), true); - } - } - - // Return a ProtectedStorageEntry that is valid for add. - // Overridden for the MailboxPayloadTests since the add and remove owners are different - ProtectedStorageEntry getProtectedStorageEntryForAdd(int sequenceNumber) throws CryptoException { - - // Entry signed and owned by same owner as payload - return buildProtectedStorageEntry(this.protectedStoragePayload, this.payloadOwnerKeys, this.payloadOwnerKeys, sequenceNumber); - } - - // Return a ProtectedStorageEntry that is valid for remove. - // Overridden for the MailboxPayloadTests since the add and remove owners are different - ProtectedStorageEntry getProtectedStorageEntryForRemove(int sequenceNumber) throws CryptoException { - - // Entry signed and owned by same owner as payload - return buildProtectedStorageEntry(this.protectedStoragePayload, this.payloadOwnerKeys, this.payloadOwnerKeys, sequenceNumber); - } - - void doProtectedStorageAddAndVerify(ProtectedStorageEntry protectedStorageEntry, - boolean expectedReturnValue, - boolean expectedStateChange) { - - SavedTestState beforeState = new SavedTestState(this.testState, protectedStorageEntry); - - boolean addResult = this.doAdd(protectedStorageEntry); - - if (!this.useMessageHandler) - Assert.assertEquals(expectedReturnValue, addResult); - - verifyProtectedStorageAdd(this.testState, beforeState, protectedStorageEntry, expectedStateChange, this.expectIsDataOwner()); - } - - void doProtectedStorageRemoveAndVerify(ProtectedStorageEntry entry, - boolean expectedReturnValue, - boolean expectInternalStateChange) { - - SavedTestState beforeState = new SavedTestState(this.testState, entry); - - boolean addResult = this.doRemove(entry); - - if (!this.useMessageHandler) - Assert.assertEquals(expectedReturnValue, addResult); - - verifyProtectedStorageRemove(this.testState, beforeState, entry, expectInternalStateChange, this.expectIsDataOwner()); - } - - // TESTCASE: Adding a well-formed entry is successful - @Test - public void addProtectedStorageEntry() throws CryptoException { - - ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); - doProtectedStorageAddAndVerify(entryForAdd, true, true); - } - - // TESTCASE: Adding duplicate payload w/ same sequence number - // TODO: Should adds() of existing sequence #s return false since they don't update state? - @Test - public void addProtectedStorageEntry_duplicateSeqNrGt0() throws CryptoException { - ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); - doProtectedStorageAddAndVerify(entryForAdd, true, true); - doProtectedStorageAddAndVerify(entryForAdd, true, false); - } - - // TESTCASE: Adding duplicate payload w/ 0 sequence number (special branch in code for logging) - @Test - public void addProtectedStorageEntry_duplicateSeqNrEq0() throws CryptoException { - ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(0); - doProtectedStorageAddAndVerify(entryForAdd, true, true); - doProtectedStorageAddAndVerify(entryForAdd, true, false); - } - - // TESTCASE: Adding duplicate payload for w/ lower sequence number - @Test - public void addProtectedStorageEntry_lowerSeqNr() throws CryptoException { - ProtectedStorageEntry entryForAdd2 = this.getProtectedStorageEntryForAdd(2); - ProtectedStorageEntry entryForAdd1 = this.getProtectedStorageEntryForAdd(1); - doProtectedStorageAddAndVerify(entryForAdd2, true, true); - doProtectedStorageAddAndVerify(entryForAdd1, false, false); - } - - // TESTCASE: Adding duplicate payload for w/ greater sequence number - @Test - public void addProtectedStorageEntry_greaterSeqNr() throws CryptoException { - ProtectedStorageEntry entryForAdd2 = this.getProtectedStorageEntryForAdd(1); - ProtectedStorageEntry entryForAdd1 = this.getProtectedStorageEntryForAdd(2); - doProtectedStorageAddAndVerify(entryForAdd2, true, true); - doProtectedStorageAddAndVerify(entryForAdd1, true, true); - } - - // TESTCASE: Add w/ same sequence number after remove of sequence number - // XXXBUGXXX: Since removes aren't required to increase the sequence number, duplicate adds - // can occur that will cause listeners to be signaled. Any well-intentioned nodes will create remove messages - // that increment the seq #, but this may just fall into a larger effort to protect against malicious nodes. -/* @Test - public void addProtectectedStorageEntry_afterRemoveSameSeqNr() throws CryptoException { - ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); - ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(1); - - doProtectedStorageAddAndVerify(entryForAdd, true, true); - doProtectedStorageRemoveAndVerify(entryForRemove, true, true); - - // Should be false, false. Instead, the hashmap is updated and hashmap listeners are signaled. - // Broadcast isn't called - doProtectedStorageAddAndVerify(entryForAdd, false, false); - }*/ - - // TESTCASE: Entry signature does not match entry owner - @Test - public void addProtectedStorageEntry_EntrySignatureDoesntMatchEntryOwner() throws CryptoException { - ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(2); - - entryForAdd.updateSignature(new byte[] { 0 }); - doProtectedStorageAddAndVerify(entryForAdd, false, false); - } - - // TESTCASE: Payload owner and entry owner are not compatible for add operation - @Test - public void addProtectedStorageEntry_payloadOwnerEntryOwnerNotCompatible() throws NoSuchAlgorithmException, CryptoException { - KeyPair notOwner = TestUtils.generateKeyPair(); - - // For standard ProtectedStorageEntrys the entry owner must match the payload owner for adds - ProtectedStorageEntry entryForAdd = buildProtectedStorageEntry( - this.protectedStoragePayload, notOwner, notOwner, 1); - - doProtectedStorageAddAndVerify(entryForAdd, false, false); - } - - // TESTCASE: Two valid, different adds have identical payloads. Ensure the second add does not overwrite the first even if seq # increases - // Need to refactor a bit to test this. Specifically, we need a way to generate two Entrys - // that pass ownerPubKey & signature checks, but have a collision with the hash of the payload. This isn't - // possible to fabricate with the current structure. - /* @Test - public void addProtectedStorageEntry_PayloadHashCollision_Fails() { - // TODO: Add test - }*/ - - // TESTCASE: Removing an item after successfully added (remove seq # == add seq #) - // XXXBUGXXX A state change shouldn't occur. Any well-intentioned nodes will create remove messages - // that increment the seq #, but this may just fall into a larger effort to protect against malicious nodes. - @Test - public void remove_seqNrEqAddSeqNr() throws CryptoException { - ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); - ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(1); - - doProtectedStorageAddAndVerify(entryForAdd, true, true); - - // should be (false, false) - doProtectedStorageRemoveAndVerify(entryForRemove, true, true); - } - - // TESTCASE: Removing an item after successfully added (remove seq # > add seq #) - @Test - public void remove_seqNrGtAddSeqNr() throws CryptoException { - ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); - ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(2); - - doProtectedStorageAddAndVerify(entryForAdd, true, true); - doProtectedStorageRemoveAndVerify(entryForRemove, true, true); - } - - // TESTCASE: Removing an item before it was added - @Test - public void remove_notExists() throws CryptoException { - ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(1); - - doProtectedStorageRemoveAndVerify(entryForRemove, false, false); - } - - // TESTCASE: Removing an item after successfully adding (remove seq # < add seq #) - @Test - public void remove_seqNrLessAddSeqNr() throws CryptoException { - ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(2); - ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(1); - - doProtectedStorageAddAndVerify(entryForAdd, true, true); - doProtectedStorageRemoveAndVerify(entryForRemove, false, false); - } - - // TESTCASE: Removing an item after successfully added (invalid remove entry signature) - @Test - public void remove_invalidEntrySig() throws CryptoException { - ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); - doProtectedStorageAddAndVerify(entryForAdd, true, true); - - ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(1); - entryForRemove.updateSignature(new byte[] { 0 }); - doProtectedStorageRemoveAndVerify(entryForRemove, false, false); - } - - // TESTCASE: Payload owner and entry owner are not compatible for remove operation - @Test - public void remove_payloadOwnerEntryOwnerNotCompatible() throws NoSuchAlgorithmException, CryptoException { - ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); - doProtectedStorageAddAndVerify(entryForAdd, true, true); - - KeyPair notOwner = TestUtils.generateKeyPair(); - - // For standard ProtectedStorageEntrys the entry owner must match the payload owner for removes - ProtectedStorageEntry entryForRemove = buildProtectedStorageEntry( - this.protectedStoragePayload, notOwner, notOwner, 1); - - doProtectedStorageRemoveAndVerify(entryForRemove, false, false); - } - - // TESTCASE: Add after removed (same seq #) - @Test - public void add_afterRemoveSameSeqNr() throws CryptoException { - ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); - doProtectedStorageAddAndVerify(entryForAdd, true, true); - - ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(2); - doProtectedStorageRemoveAndVerify(entryForRemove, true, true); - - doProtectedStorageAddAndVerify(entryForAdd, false, false); - } - - // TESTCASE: Add after removed (greater seq #) - @Test - public void add_afterRemoveGreaterSeqNr() throws CryptoException { - ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); - doProtectedStorageAddAndVerify(entryForAdd, true, true); - - ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(2); - doProtectedStorageRemoveAndVerify(entryForRemove, true, true); - - entryForAdd = this.getProtectedStorageEntryForAdd(3); - doProtectedStorageAddAndVerify(entryForAdd, true, true); - } - - // TESTCASE: Add after removed (lower seq #) - @Test - public void add_afterRemoveLessSeqNr() throws CryptoException { - ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(2); - doProtectedStorageAddAndVerify(entryForAdd, true, true); - - ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(3); - doProtectedStorageRemoveAndVerify(entryForRemove, true, true); - - entryForAdd = this.getProtectedStorageEntryForAdd(1); - doProtectedStorageAddAndVerify(entryForAdd, false, false); - } - - // TESTCASE: Received remove for nonexistent item that was later received - // XXXBUGXXX: There may be cases where removes are reordered with adds (remove during pending GetDataRequest?). - // The proper behavior may be to not add the late messages, but the current code will successfully add them - // even in the AddOncePayload (mailbox) case. - @Test - public void remove_lateAdd() throws CryptoException { - ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); - ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(2); - - doProtectedStorageRemoveAndVerify(entryForRemove, false, false); - - // should be (false, false) - doProtectedStorageAddAndVerify(entryForAdd, true, true); - } - } - - // Runs the ProtectedStorageEntryTestBase tests against a basic (no marker interfaces) ProtectedStoragePayload - public static class ProtectedStorageEntryTest extends ProtectedStorageEntryTestBase { - - @Override - protected ProtectedStoragePayload createInstance(KeyPair payloadOwnerKeys) { - return new ProtectedStoragePayloadStub(payloadOwnerKeys.getPublic()); - } - - RefreshOfferMessage buildRefreshOfferMessage(ProtectedStorageEntry protectedStorageEntry, KeyPair ownerKeys, int sequenceNumber) throws CryptoException { - return P2PDataStorageTest.buildRefreshOfferMessage(protectedStorageEntry.getProtectedStoragePayload(), ownerKeys, sequenceNumber); - } - - void doRefreshTTLAndVerify(RefreshOfferMessage refreshOfferMessage, boolean expectedReturnValue, boolean expectStateChange) { - SavedTestState beforeState = new SavedTestState(this.testState, refreshOfferMessage); - - boolean returnValue = this.doRefreshTTL(refreshOfferMessage); - - if (!this.useMessageHandler) - Assert.assertEquals(expectedReturnValue, returnValue); - - verifyRefreshTTL(this.testState, beforeState, refreshOfferMessage, expectStateChange, this.expectIsDataOwner()); - } - - // TESTCASE: Refresh an entry that doesn't exist - @Test - public void refreshTTL_noExist() throws CryptoException { - ProtectedStorageEntry entry = this.getProtectedStorageEntryForAdd(1); - - doRefreshTTLAndVerify(buildRefreshOfferMessage(entry, this.payloadOwnerKeys,1), false, false); - } - - // TESTCASE: Refresh an entry where seq # is equal to last seq # seen - @Test - public void refreshTTL_existingEntry() throws CryptoException { - ProtectedStorageEntry entry = this.getProtectedStorageEntryForAdd(1); - doProtectedStorageAddAndVerify(entry, true, true); - - doRefreshTTLAndVerify(buildRefreshOfferMessage(entry, this.payloadOwnerKeys,1), true, false); - } - - // TESTCASE: Duplicate refresh message (same seq #) - @Test - public void refreshTTL_duplicateRefreshSeqNrEqual() throws CryptoException { - ProtectedStorageEntry entry = this.getProtectedStorageEntryForAdd(1); - doProtectedStorageAddAndVerify(entry, true, true); - - doRefreshTTLAndVerify(buildRefreshOfferMessage(entry, this.payloadOwnerKeys, 2), true, true); - doRefreshTTLAndVerify(buildRefreshOfferMessage(entry, this.payloadOwnerKeys, 2), true, false); - } - - // TESTCASE: Duplicate refresh message (greater seq #) - @Test - public void refreshTTL_duplicateRefreshSeqNrGreater() throws CryptoException { - ProtectedStorageEntry entry = this.getProtectedStorageEntryForAdd(1); - doProtectedStorageAddAndVerify(entry, true, true); - - doRefreshTTLAndVerify(buildRefreshOfferMessage(entry, this.payloadOwnerKeys,2), true, true); - doRefreshTTLAndVerify(buildRefreshOfferMessage(entry, this.payloadOwnerKeys,3), true, true); - } - - // TESTCASE: Duplicate refresh message (lower seq #) - @Test - public void refreshTTL_duplicateRefreshSeqNrLower() throws CryptoException { - ProtectedStorageEntry entry = this.getProtectedStorageEntryForAdd(1); - doProtectedStorageAddAndVerify(entry, true, true); - - doRefreshTTLAndVerify(buildRefreshOfferMessage(entry, this.payloadOwnerKeys,3), true, true); - doRefreshTTLAndVerify(buildRefreshOfferMessage(entry, this.payloadOwnerKeys,2), false, false); - } - - // TESTCASE: Refresh previously removed entry - @Test - public void refreshTTL_refreshAfterRemove() throws CryptoException { - ProtectedStorageEntry entry = this.getProtectedStorageEntryForAdd(1); - doProtectedStorageAddAndVerify(entry, true, true); - doProtectedStorageRemoveAndVerify(entry, true, true); - - doRefreshTTLAndVerify(buildRefreshOfferMessage(entry, this.payloadOwnerKeys,3), false, false); - } - - // TESTCASE: Refresh an entry, but owner doesn't match PubKey of original add owner - @Test - public void refreshTTL_refreshEntryOwnerOriginalOwnerMismatch() throws CryptoException, NoSuchAlgorithmException { - ProtectedStorageEntry entry = this.getProtectedStorageEntryForAdd(1); - doProtectedStorageAddAndVerify(entry, true, true); - - KeyPair notOwner = TestUtils.generateKeyPair(); - doRefreshTTLAndVerify(buildRefreshOfferMessage(entry, notOwner, 2), false, false); - } - } - - // Runs the ProtectedStorageEntryTestBase tests against the PersistablePayload marker class - public static class PersistableProtectedStoragePayloadTest extends ProtectedStorageEntryTestBase { - private static class PersistableProtectedStoragePayload extends ProtectedStoragePayloadStub implements PersistablePayload { - - PersistableProtectedStoragePayload(PublicKey ownerPubKey) { - super(ownerPubKey); - } - } - - @Override - protected ProtectedStoragePayload createInstance(KeyPair payloadOwnerKeys) { - return new PersistableProtectedStoragePayload(payloadOwnerKeys.getPublic()); - } - } - - /* - * Runs the ProtectedStorageEntryTestBase tests against the MailboxPayload. The rules for add/remove are different - * so a few of the functions used in common tests are overridden so the test cases can be deduplicated. Additional - * tests that just apply to the mailbox case are also added below. - */ - public static class MailboxPayloadTest extends ProtectedStorageEntryTestBase { - - private KeyPair senderKeys; - private KeyPair receiverKeys; - - @Override - @Before - public void setUp() throws CryptoException, NoSuchAlgorithmException { - super.setUp(); - - this.senderKeys = TestUtils.generateKeyPair(); - this.receiverKeys = TestUtils.generateKeyPair(); - - // Deep in the bowels of protobuf we grab the messageID from the version module. This is required to hash the - // full MailboxStoragePayload so make sure it is initialized. - Version.setBaseCryptoNetworkId(1); - } - - @Override - boolean doRemove(ProtectedStorageEntry entry) { - if (this.useMessageHandler) { - Connection mockedConnection = mock(Connection.class); - when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(getTestNodeAddress())); - - testState.mockedStorage.onMessage(new RemoveMailboxDataMessage((ProtectedMailboxStorageEntry) entry), mockedConnection); - - return true; - } else { - // XXX: All external callers just pass in true, a future patch can remove the argument. - return testState.mockedStorage.removeMailboxData((ProtectedMailboxStorageEntry) entry, getTestNodeAddress(), true); - } - } - - @Override - ProtectedStorageEntry getProtectedStorageEntryForAdd(int sequenceNumber) throws CryptoException { - return buildProtectedMailboxStorageEntry(senderKeys.getPublic(), receiverKeys.getPublic(), senderKeys.getPrivate(), senderKeys.getPublic(), receiverKeys.getPublic(), sequenceNumber); - } - - @Override - ProtectedStorageEntry getProtectedStorageEntryForRemove(int sequenceNumber) throws CryptoException { - return buildProtectedMailboxStorageEntry(senderKeys.getPublic(), receiverKeys.getPublic(), receiverKeys.getPrivate(), receiverKeys.getPublic(), receiverKeys.getPublic(), sequenceNumber); - } - - @Override - protected ProtectedStoragePayload createInstance(KeyPair payloadOwnerKeys) { - return null; - } - - // TESTCASE: Adding fails when Entry owner is different from sender - @Test - public void addProtectedStorageEntry_payloadOwnerEntryOwnerNotCompatible() throws CryptoException, NoSuchAlgorithmException { - KeyPair notSender = TestUtils.generateKeyPair(); - - ProtectedStorageEntry entryForAdd = buildProtectedMailboxStorageEntry(notSender.getPublic(), receiverKeys.getPublic(), senderKeys.getPrivate(), senderKeys.getPublic(), receiverKeys.getPublic(), 1); - - doProtectedStorageAddAndVerify(entryForAdd, false, false); - } - - // TESTCASE: Adding MailboxStoragePayload when Entry owner is different than sender does not overwrite existing payload - @Test - public void addProtectedStorageEntry_payloadOwnerEntryOwnerNotCompatibleNoSideEffect() throws CryptoException, NoSuchAlgorithmException { - KeyPair notSender = TestUtils.generateKeyPair(); - - doProtectedStorageAddAndVerify(this.getProtectedStorageEntryForAdd(1), true, true); - - ProtectedStorageEntry invalidEntryForAdd = buildProtectedMailboxStorageEntry(notSender.getPublic(), receiverKeys.getPublic(), senderKeys.getPrivate(), senderKeys.getPublic(), receiverKeys.getPublic(), 1); - - doProtectedStorageAddAndVerify(invalidEntryForAdd, false, false); - } - - // TESTCASE: Payload owner and entry owner are not compatible for remove operation - @Test - public void remove_payloadOwnerEntryOwnerNotCompatible() throws NoSuchAlgorithmException, CryptoException { - ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); - doProtectedStorageAddAndVerify(entryForAdd, true, true); - - KeyPair notReceiver = TestUtils.generateKeyPair(); - - ProtectedStorageEntry entryForRemove = buildProtectedMailboxStorageEntry(senderKeys.getPublic(), receiverKeys.getPublic(), notReceiver.getPrivate(), notReceiver.getPublic(), receiverKeys.getPublic(), 1); - - doProtectedStorageRemoveAndVerify(entryForRemove, false, false); - } - - // TESTCASE: Payload owner and entry.receiversPubKey are not compatible for remove operation - @Test - public void remove_payloadOwnerEntryReceiversPubKeyNotCompatible() throws NoSuchAlgorithmException, CryptoException { - ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); - doProtectedStorageAddAndVerify(entryForAdd, true, true); - - KeyPair notSender = TestUtils.generateKeyPair(); - - ProtectedStorageEntry entryForRemove = buildProtectedMailboxStorageEntry(senderKeys.getPublic(), receiverKeys.getPublic(), receiverKeys.getPrivate(), receiverKeys.getPublic(), notSender.getPublic(), 1); - - doProtectedStorageRemoveAndVerify(entryForRemove, false, false); - } - - // TESTCASE: receiversPubKey changed between add and remove - // TODO: Current code does not check receiversPubKey on add() (payload.ownersPubKey == entry.receiversPubKey) - // Can the code just check against payload.ownersPubKey in all cases and deprecate Entry.receiversPubKey? - @Test - public void remove_receiversPubKeyChanged() throws NoSuchAlgorithmException, CryptoException { - KeyPair otherKeys = TestUtils.generateKeyPair(); - - // Add an entry that has an invalid Entry.receiversPubKey. Unfortunately, this succeeds right now. - ProtectedStorageEntry entryForAdd = buildProtectedMailboxStorageEntry(senderKeys.getPublic(), receiverKeys.getPublic(), senderKeys.getPrivate(), senderKeys.getPublic(), otherKeys.getPublic(), 1); - doProtectedStorageAddAndVerify(entryForAdd, true, true); - - doProtectedStorageRemoveAndVerify(this.getProtectedStorageEntryForRemove(2), false, false); - } - - - // XXXBUGXXX: The P2PService calls remove() instead of removeFromMailbox() in the addMailboxData() path. - // This test shows it will always fail even with a valid remove entry. Future work should be able to - // combine the remove paths in the same way the add() paths are combined. This will require deprecating - // the receiversPubKey field which is a duplicate of the ownerPubKey in the MailboxStoragePayload. - // More investigation is needed. - @Test - public void remove_canCallWrongRemoveAndFail() throws CryptoException { - - ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); - ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(1); - - doProtectedStorageAddAndVerify(entryForAdd, true, true); - - SavedTestState beforeState = new SavedTestState(this.testState, entryForRemove); - - // Call remove(ProtectedStorageEntry) instead of removeFromMailbox(ProtectedMailboxStorageEntry) and verify - // it fails - boolean addResult = super.doRemove(entryForRemove); - - if (!this.useMessageHandler) - Assert.assertFalse(addResult); - - // should succeed with expectedStatechange==true when remove paths are combined - verifyProtectedStorageRemove(this.testState, beforeState, entryForRemove, false, this.expectIsDataOwner()); - } - - // TESTCASE: Verify misuse of the API (calling remove() instead of removeFromMailbox correctly errors with - // a payload that is valid for remove of a non-mailbox entry. - @Test - public void remove_canCallWrongRemoveAndFailInvalidPayload() throws CryptoException { - - ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); - - doProtectedStorageAddAndVerify(entryForAdd, true, true); - - SavedTestState beforeState = new SavedTestState(this.testState, entryForAdd); - - // Call remove(ProtectedStorageEntry) instead of removeFromMailbox(ProtectedMailboxStorageEntry) and verify - // it fails with a payload that isn't signed by payload.ownerPubKey - boolean addResult = super.doRemove(entryForAdd); - - if (!this.useMessageHandler) - Assert.assertFalse(addResult); - - verifyProtectedStorageRemove(this.testState, beforeState, entryForAdd, false, this.expectIsDataOwner()); - } - - // TESTCASE: Add after removed when add-once required (greater seq #) - @Override - @Test - public void add_afterRemoveGreaterSeqNr() throws CryptoException { - ProtectedStorageEntry entryForAdd = this.getProtectedStorageEntryForAdd(1); - doProtectedStorageAddAndVerify(entryForAdd, true, true); - - ProtectedStorageEntry entryForRemove = this.getProtectedStorageEntryForRemove(2); - doProtectedStorageRemoveAndVerify(entryForRemove, true, true); - - entryForAdd = this.getProtectedStorageEntryForAdd(3); - doProtectedStorageAddAndVerify(entryForAdd, false, false); - } - } - - public static class BuildEntryAPITests { - private TestState testState; - - @Before - public void setUp() { - this.testState = new TestState(); - - // Deep in the bowels of protobuf we grab the messageID from the version module. This is required to hash the - // full MailboxStoragePayload so make sure it is initialized. - Version.setBaseCryptoNetworkId(1); - } - - // TESTCASE: Adding an entry from the getProtectedStorageEntry API correctly adds the item - @Test - public void getProtectedStorageEntry_NoExist() throws NoSuchAlgorithmException, CryptoException { - KeyPair ownerKeys = TestUtils.generateKeyPair(); - - ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayload(ownerKeys); - ProtectedStorageEntry protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); - - SavedTestState beforeState = new SavedTestState(this.testState, protectedStorageEntry); - Assert.assertTrue(this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, getTestNodeAddress(), null, true)); - - verifyProtectedStorageAdd(this.testState, beforeState, protectedStorageEntry, true, true); - } - - // TESTCASE: Adding an entry from the getProtectedStorageEntry API of an existing item correctly updates the item - @Test - public void getProtectedStorageEntry() throws NoSuchAlgorithmException, CryptoException { - KeyPair ownerKeys = TestUtils.generateKeyPair(); - - ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayload(ownerKeys); - ProtectedStorageEntry protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); - - Assert.assertTrue(this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, getTestNodeAddress(), null, true)); - - SavedTestState beforeState = new SavedTestState(this.testState, protectedStorageEntry); - protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); - this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, getTestNodeAddress(), null, true); - - verifyProtectedStorageAdd(this.testState, beforeState, protectedStorageEntry, true, true); - } - - // TESTCASE: Adding an entry from the getProtectedStorageEntry API of an existing item (added from onMessage path) correctly updates the item - @Test - public void getProtectedStorageEntry_FirstOnMessageSecondAPI() throws NoSuchAlgorithmException, CryptoException { - KeyPair ownerKeys = TestUtils.generateKeyPair(); - - ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayload(ownerKeys); - ProtectedStorageEntry protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); - - Connection mockedConnection = mock(Connection.class); - when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(getTestNodeAddress())); - - this.testState.mockedStorage.onMessage(new AddDataMessage(protectedStorageEntry), mockedConnection); - - SavedTestState beforeState = new SavedTestState(this.testState, protectedStorageEntry); - protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); - Assert.assertTrue(this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, getTestNodeAddress(), null, true)); - - verifyProtectedStorageAdd(this.testState, beforeState, protectedStorageEntry, true, true); - } - - // TESTCASE: Updating an entry from the getRefreshTTLMessage API correctly errors if the item hasn't been seen - @Test - public void getRefreshTTLMessage_NoExists() throws NoSuchAlgorithmException, CryptoException { - KeyPair ownerKeys = TestUtils.generateKeyPair(); - - ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayload(ownerKeys); - - RefreshOfferMessage refreshOfferMessage = this.testState.mockedStorage.getRefreshTTLMessage(protectedStoragePayload, ownerKeys); - - SavedTestState beforeState = new SavedTestState(this.testState, refreshOfferMessage); - Assert.assertFalse(this.testState.mockedStorage.refreshTTL(refreshOfferMessage, getTestNodeAddress(), true)); - - verifyRefreshTTL(this.testState, beforeState, refreshOfferMessage, false, true); - } - - // TESTCASE: Updating an entry from the getRefreshTTLMessage API correctly "refreshes" the item - @Test - public void getRefreshTTLMessage() throws NoSuchAlgorithmException, CryptoException { - KeyPair ownerKeys = TestUtils.generateKeyPair(); - - ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayload(ownerKeys); - ProtectedStorageEntry protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); - this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, getTestNodeAddress(), null, true); - - RefreshOfferMessage refreshOfferMessage = this.testState.mockedStorage.getRefreshTTLMessage(protectedStoragePayload, ownerKeys); - this.testState.mockedStorage.refreshTTL(refreshOfferMessage, getTestNodeAddress(), true); - - refreshOfferMessage = this.testState.mockedStorage.getRefreshTTLMessage(protectedStoragePayload, ownerKeys); - - SavedTestState beforeState = new SavedTestState(this.testState, refreshOfferMessage); - Assert.assertTrue(this.testState.mockedStorage.refreshTTL(refreshOfferMessage, getTestNodeAddress(), true)); - - verifyRefreshTTL(this.testState, beforeState, refreshOfferMessage, true, true); - } - - // TESTCASE: Updating an entry from the getRefreshTTLMessage API correctly "refreshes" the item when it was originally added from onMessage path - @Test - public void getRefreshTTLMessage_FirstOnMessageSecondAPI() throws NoSuchAlgorithmException, CryptoException { - KeyPair ownerKeys = TestUtils.generateKeyPair(); - - ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayload(ownerKeys); - ProtectedStorageEntry protectedStorageEntry = this.testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); - this.testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, getTestNodeAddress(), null, true); - - Connection mockedConnection = mock(Connection.class); - when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(getTestNodeAddress())); - - this.testState.mockedStorage.onMessage(new AddDataMessage(protectedStorageEntry), mockedConnection); - - RefreshOfferMessage refreshOfferMessage = this.testState.mockedStorage.getRefreshTTLMessage(protectedStoragePayload, ownerKeys); - - SavedTestState beforeState = new SavedTestState(this.testState, refreshOfferMessage); - Assert.assertTrue(this.testState.mockedStorage.refreshTTL(refreshOfferMessage, getTestNodeAddress(), true)); - - verifyRefreshTTL(this.testState, beforeState, refreshOfferMessage, true, true); - } - - // TESTCASE: Removing a non-existent mailbox entry from the getMailboxDataWithSignedSeqNr API - @Test - public void getMailboxDataWithSignedSeqNr_RemoveNoExist() throws NoSuchAlgorithmException, CryptoException { - KeyPair receiverKeys = TestUtils.generateKeyPair(); - KeyPair senderKeys = TestUtils.generateKeyPair(); - - MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); - - ProtectedMailboxStorageEntry protectedMailboxStorageEntry = - this.testState.mockedStorage.getMailboxDataWithSignedSeqNr(mailboxStoragePayload, receiverKeys, receiverKeys.getPublic()); - - SavedTestState beforeState = new SavedTestState(this.testState, protectedMailboxStorageEntry); - Assert.assertFalse(this.testState.mockedStorage.removeMailboxData(protectedMailboxStorageEntry, getTestNodeAddress(), true)); - - verifyProtectedStorageRemove(this.testState, beforeState, protectedMailboxStorageEntry, false, true); - } - - // TESTCASE: Adding, then removing a mailbox message from the getMailboxDataWithSignedSeqNr API - @Test - public void getMailboxDataWithSignedSeqNr_AddThenRemove() throws NoSuchAlgorithmException, CryptoException { - KeyPair receiverKeys = TestUtils.generateKeyPair(); - KeyPair senderKeys = TestUtils.generateKeyPair(); - - MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); - - ProtectedMailboxStorageEntry protectedMailboxStorageEntry = - this.testState.mockedStorage.getMailboxDataWithSignedSeqNr(mailboxStoragePayload, senderKeys, receiverKeys.getPublic()); - - Assert.assertTrue(this.testState.mockedStorage.addProtectedStorageEntry(protectedMailboxStorageEntry, getTestNodeAddress(), null, true)); - - protectedMailboxStorageEntry = - this.testState.mockedStorage.getMailboxDataWithSignedSeqNr(mailboxStoragePayload, receiverKeys, receiverKeys.getPublic()); - - SavedTestState beforeState = new SavedTestState(this.testState, protectedMailboxStorageEntry); - Assert.assertTrue(this.testState.mockedStorage.removeMailboxData(protectedMailboxStorageEntry, getTestNodeAddress(), true)); - - verifyProtectedStorageRemove(this.testState, beforeState, protectedMailboxStorageEntry, true, true); - } - - // TESTCASE: Removing a mailbox message that was added from the onMessage handler - @Test - public void getMailboxDataWithSignedSeqNr_ValidRemoveAddFromMessage() throws NoSuchAlgorithmException, CryptoException { - KeyPair receiverKeys = TestUtils.generateKeyPair(); - KeyPair senderKeys = TestUtils.generateKeyPair(); - - ProtectedStorageEntry protectedStorageEntry = - buildProtectedMailboxStorageEntry(senderKeys.getPublic(), receiverKeys.getPublic(), senderKeys.getPrivate(), - senderKeys.getPublic(), receiverKeys.getPublic(), 1); - - Connection mockedConnection = mock(Connection.class); - when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(getTestNodeAddress())); - - this.testState.mockedStorage.onMessage(new AddDataMessage(protectedStorageEntry), mockedConnection); - - MailboxStoragePayload mailboxStoragePayload = (MailboxStoragePayload) protectedStorageEntry.getProtectedStoragePayload(); - - ProtectedMailboxStorageEntry protectedMailboxStorageEntry = - this.testState.mockedStorage.getMailboxDataWithSignedSeqNr(mailboxStoragePayload, receiverKeys, receiverKeys.getPublic()); - - SavedTestState beforeState = new SavedTestState(this.testState, protectedMailboxStorageEntry); - Assert.assertTrue(this.testState.mockedStorage.removeMailboxData(protectedMailboxStorageEntry, getTestNodeAddress(), true)); - - verifyProtectedStorageRemove(this.testState, beforeState, protectedMailboxStorageEntry, true, true); - } - } - - public static class DisconnectTest { - private TestState testState; - private Connection mockedConnection; - - private static ProtectedStorageEntry populateTestState(TestState testState, long ttl) throws CryptoException, NoSuchAlgorithmException { - KeyPair ownerKeys = TestUtils.generateKeyPair(); - ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayload(ownerKeys, ttl); - - ProtectedStorageEntry protectedStorageEntry = testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); - testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, getTestNodeAddress(), null, false); - - return protectedStorageEntry; - } - - private static void verifyStateAfterDisconnect(TestState currentState, SavedTestState beforeState, boolean wasRemoved, boolean wasTTLReduced) { - ProtectedStorageEntry protectedStorageEntry = beforeState.protectedStorageEntryBeforeOp; - - P2PDataStorage.ByteArray hashMapHash = P2PDataStorage.get32ByteHashAsByteArray(protectedStorageEntry.getProtectedStoragePayload()); - - Assert.assertNotEquals(wasRemoved, currentState.mockedStorage.getMap().containsKey(hashMapHash)); - - if (wasRemoved) - verify(currentState.hashMapChangedListener).onRemoved(protectedStorageEntry); - else - verify(currentState.hashMapChangedListener, never()).onRemoved(any(ProtectedStorageEntry.class)); - - if (wasTTLReduced) - Assert.assertTrue(protectedStorageEntry.getCreationTimeStamp() < beforeState.creationTimestampBeforeUpdate); - else - Assert.assertEquals(protectedStorageEntry.getCreationTimeStamp(), beforeState.creationTimestampBeforeUpdate); - } - - @Before - public void setUp() { - this.mockedConnection = mock(Connection.class); - this.testState = new TestState(); - } - - // TESTCASE: Bad peer info - @Test - public void peerConnectionUnknown() { - when(this.mockedConnection.hasPeersNodeAddress()).thenReturn(false); - - this.testState.mockedStorage.onDisconnect(CloseConnectionReason.SOCKET_CLOSED, mockedConnection); - } - - // TESTCASE: Intended disconnects don't trigger expiration - @Test - public void connectionClosedIntended() { - when(this.mockedConnection.hasPeersNodeAddress()).thenReturn(true); - this.testState.mockedStorage.onDisconnect(CloseConnectionReason.CLOSE_REQUESTED_BY_PEER, mockedConnection); - } - - // TESTCASE: Peer NodeAddress unknown - @Test - public void connectionClosedSkipsItemsPeerInfoBadState() throws NoSuchAlgorithmException, CryptoException { - when(this.mockedConnection.hasPeersNodeAddress()).thenReturn(true); - when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.empty()); - - ProtectedStorageEntry protectedStorageEntry = populateTestState(testState, 1); - - SavedTestState beforeState = new SavedTestState(this.testState, protectedStorageEntry); - - this.testState.mockedStorage.onDisconnect(CloseConnectionReason.SOCKET_CLOSED, mockedConnection); - - verifyStateAfterDisconnect(this.testState, beforeState, false, false); - } - - // TESTCASE: Unintended disconnects reduce the TTL for entrys that match disconnected peer - @Test - public void connectionClosedReduceTTL() throws NoSuchAlgorithmException, CryptoException { - when(this.mockedConnection.hasPeersNodeAddress()).thenReturn(true); - when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(getTestNodeAddress())); - - ProtectedStorageEntry protectedStorageEntry = populateTestState(testState, TimeUnit.DAYS.toMillis(90)); - - SavedTestState beforeState = new SavedTestState(this.testState, protectedStorageEntry); - - this.testState.mockedStorage.onDisconnect(CloseConnectionReason.SOCKET_CLOSED, mockedConnection); - - verifyStateAfterDisconnect(this.testState, beforeState, false, true); - } - - // TESTCASE: Unintended disconnects don't reduce TTL for entrys that are not from disconnected peer - @Test - public void connectionClosedSkipsItemsNotFromPeer() throws NoSuchAlgorithmException, CryptoException { - when(this.mockedConnection.hasPeersNodeAddress()).thenReturn(true); - when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(new NodeAddress("notTestNode", 2020))); - - ProtectedStorageEntry protectedStorageEntry = populateTestState(testState, 1); - - SavedTestState beforeState = new SavedTestState(this.testState, protectedStorageEntry); - - this.testState.mockedStorage.onDisconnect(CloseConnectionReason.SOCKET_CLOSED, mockedConnection); - - verifyStateAfterDisconnect(this.testState, beforeState, false, false); - } - - // TESTCASE: Unintended disconnects expire entrys that match disconnected peer and TTL is low enough for expire - @Test - public void connectionClosedReduceTTLAndExpireItemsFromPeer() throws NoSuchAlgorithmException, CryptoException { - when(this.mockedConnection.hasPeersNodeAddress()).thenReturn(true); - when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(getTestNodeAddress())); - - ProtectedStorageEntry protectedStorageEntry = populateTestState(testState, 1); - - SavedTestState beforeState = new SavedTestState(this.testState, protectedStorageEntry); - - this.testState.mockedStorage.onDisconnect(CloseConnectionReason.SOCKET_CLOSED, mockedConnection); - - verifyStateAfterDisconnect(this.testState, beforeState, true, false); - } - } -} diff --git a/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStoreDisconnectTest.java b/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStoreDisconnectTest.java new file mode 100644 index 00000000000..7a54138a928 --- /dev/null +++ b/p2p/src/test/java/bisq/network/p2p/storage/P2PDataStoreDisconnectTest.java @@ -0,0 +1,203 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.network.p2p.storage; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.TestUtils; +import bisq.network.p2p.network.CloseConnectionReason; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.storage.mocks.ExpirableProtectedStoragePayloadStub; +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; +import bisq.network.p2p.storage.payload.ProtectedStoragePayload; + +import bisq.common.crypto.CryptoException; +import bisq.common.proto.persistable.PersistablePayload; + +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import static bisq.network.p2p.storage.TestState.*; + +/** + * Tests of the P2PDataStore ConnectionListener interface. + */ +public class P2PDataStoreDisconnectTest { + private TestState testState; + private Connection mockedConnection; + + private static ProtectedStorageEntry populateTestState(TestState testState, + long ttl) throws CryptoException, NoSuchAlgorithmException { + KeyPair ownerKeys = TestUtils.generateKeyPair(); + ProtectedStoragePayload protectedStoragePayload = new ExpirableProtectedStoragePayloadStub(ownerKeys.getPublic(), ttl); + + ProtectedStorageEntry protectedStorageEntry = testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); + testState.mockedStorage.addProtectedStorageEntry(protectedStorageEntry, TestState.getTestNodeAddress(), null, false); + + return protectedStorageEntry; + } + + private static void verifyStateAfterDisconnect(TestState currentState, + SavedTestState beforeState, + boolean wasRemoved, + boolean wasTTLReduced) { + ProtectedStorageEntry protectedStorageEntry = beforeState.protectedStorageEntryBeforeOp; + + currentState.verifyProtectedStorageRemove(beforeState, protectedStorageEntry, + wasRemoved, false, false, false); + + if (wasTTLReduced) + Assert.assertTrue(protectedStorageEntry.getCreationTimeStamp() < beforeState.creationTimestampBeforeUpdate); + else + Assert.assertEquals(protectedStorageEntry.getCreationTimeStamp(), beforeState.creationTimestampBeforeUpdate); + } + + @Before + public void setUp() { + this.mockedConnection = mock(Connection.class); + this.testState = new TestState(); + } + + // TESTCASE: Bad peer info + @Test + public void peerConnectionUnknown() { + when(this.mockedConnection.hasPeersNodeAddress()).thenReturn(false); + + this.testState.mockedStorage.onDisconnect(CloseConnectionReason.SOCKET_CLOSED, mockedConnection); + } + + // TESTCASE: Intended disconnects don't trigger expiration + @Test + public void connectionClosedIntended() { + when(this.mockedConnection.hasPeersNodeAddress()).thenReturn(true); + this.testState.mockedStorage.onDisconnect(CloseConnectionReason.CLOSE_REQUESTED_BY_PEER, mockedConnection); + } + + // TESTCASE: Peer NodeAddress unknown + @Test + public void connectionClosedSkipsItemsPeerInfoBadState() throws NoSuchAlgorithmException, CryptoException { + when(this.mockedConnection.hasPeersNodeAddress()).thenReturn(true); + when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.empty()); + + ProtectedStorageEntry protectedStorageEntry = populateTestState(testState, 1); + + SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); + + this.testState.mockedStorage.onDisconnect(CloseConnectionReason.SOCKET_CLOSED, mockedConnection); + + verifyStateAfterDisconnect(this.testState, beforeState, false, false); + } + + // TESTCASE: Unintended disconnects reduce the TTL for entrys that match disconnected peer + @Test + public void connectionClosedReduceTTL() throws NoSuchAlgorithmException, CryptoException { + when(this.mockedConnection.hasPeersNodeAddress()).thenReturn(true); + when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(TestState.getTestNodeAddress())); + + ProtectedStorageEntry protectedStorageEntry = populateTestState(testState, TimeUnit.DAYS.toMillis(90)); + + SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); + + this.testState.mockedStorage.onDisconnect(CloseConnectionReason.SOCKET_CLOSED, mockedConnection); + + verifyStateAfterDisconnect(this.testState, beforeState, false, true); + } + + // TESTCASE: Unintended disconnects don't reduce TTL for entrys that are not from disconnected peer + @Test + public void connectionClosedSkipsItemsNotFromPeer() throws NoSuchAlgorithmException, CryptoException { + when(this.mockedConnection.hasPeersNodeAddress()).thenReturn(true); + when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(new NodeAddress("notTestNode", 2020))); + + ProtectedStorageEntry protectedStorageEntry = populateTestState(testState, 1); + + SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); + + this.testState.mockedStorage.onDisconnect(CloseConnectionReason.SOCKET_CLOSED, mockedConnection); + + verifyStateAfterDisconnect(this.testState, beforeState, false, false); + } + + // TESTCASE: Unintended disconnects expire entrys that match disconnected peer and TTL is low enough for expire + @Test + public void connectionClosedReduceTTLAndExpireItemsFromPeer() throws NoSuchAlgorithmException, CryptoException { + when(this.mockedConnection.hasPeersNodeAddress()).thenReturn(true); + when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(TestState.getTestNodeAddress())); + + ProtectedStorageEntry protectedStorageEntry = populateTestState(testState, 1); + + SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); + + // Increment the time by 1 hour which will put the protectedStorageState outside TTL + this.testState.incrementClock(); + + this.testState.mockedStorage.onDisconnect(CloseConnectionReason.SOCKET_CLOSED, mockedConnection); + + verifyStateAfterDisconnect(this.testState, beforeState, true, false); + } + + // TESTCASE: ProtectedStoragePayloads implementing the PersistablePayload interface are correctly removed + // from the persistent store during the onDisconnect path. + @Test + public void connectionClosedReduceTTLAndExpireItemsFromPeerPersistable() + throws NoSuchAlgorithmException, CryptoException { + + class ExpirablePersistentProtectedStoragePayloadStub + extends ExpirableProtectedStoragePayloadStub implements PersistablePayload { + + public ExpirablePersistentProtectedStoragePayloadStub(PublicKey ownerPubKey) { + super(ownerPubKey, 0); + } + } + + when(this.mockedConnection.hasPeersNodeAddress()).thenReturn(true); + when(mockedConnection.getPeersNodeAddressOptional()).thenReturn(Optional.of(TestState.getTestNodeAddress())); + + KeyPair ownerKeys = TestUtils.generateKeyPair(); + ProtectedStoragePayload protectedStoragePayload = + new ExpirablePersistentProtectedStoragePayloadStub(ownerKeys.getPublic()); + + ProtectedStorageEntry protectedStorageEntry = + testState.mockedStorage.getProtectedStorageEntry(protectedStoragePayload, ownerKeys); + + testState.mockedStorage.addProtectedStorageEntry( + protectedStorageEntry, TestState.getTestNodeAddress(), null, false); + + SavedTestState beforeState = this.testState.saveTestState(protectedStorageEntry); + + // Increment the time by 1 hour which will put the protectedStorageState outside TTL + this.testState.incrementClock(); + + this.testState.mockedStorage.onDisconnect(CloseConnectionReason.SOCKET_CLOSED, mockedConnection); + + verifyStateAfterDisconnect(this.testState, beforeState, true, false); + } +} diff --git a/p2p/src/test/java/bisq/network/p2p/storage/ProtectedDataStorageTest.java b/p2p/src/test/java/bisq/network/p2p/storage/ProtectedDataStorageTest.java deleted file mode 100644 index f04dd02bf76..00000000000 --- a/p2p/src/test/java/bisq/network/p2p/storage/ProtectedDataStorageTest.java +++ /dev/null @@ -1,229 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.network.p2p.storage; - -import bisq.network.crypto.EncryptionService; -import bisq.network.p2p.NodeAddress; -import bisq.network.p2p.P2PService; -import bisq.network.p2p.TestUtils; -import bisq.network.p2p.network.NetworkNode; -import bisq.network.p2p.peers.PeerManager; -import bisq.network.p2p.storage.messages.RefreshOfferMessage; -import bisq.network.p2p.storage.mocks.MockData; -import bisq.network.p2p.storage.payload.ProtectedStorageEntry; - -import bisq.common.UserThread; -import bisq.common.crypto.CryptoException; -import bisq.common.crypto.KeyRing; -import bisq.common.crypto.KeyStorage; -import bisq.common.crypto.Sig; -import bisq.common.storage.FileUtil; - -import java.security.InvalidKeyException; -import java.security.KeyPair; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.SignatureException; -import java.security.cert.CertificateException; - -import java.nio.file.Path; -import java.nio.file.Paths; - -import java.io.File; -import java.io.IOException; - -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Executors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; - -@Ignore -public class ProtectedDataStorageTest { - private static final Logger log = LoggerFactory.getLogger(ProtectedDataStorageTest.class); - - final boolean useClearNet = true; - private final Set seedNodes = new HashSet<>(); - private NetworkNode networkNode1; - private PeerManager peerManager1; - private EncryptionService encryptionService1, encryptionService2; - private P2PDataStorage dataStorage1; - private KeyPair storageSignatureKeyPair1, storageSignatureKeyPair2; - private KeyRing keyRing1, keyRing2; - private MockData mockData; - private final int sleepTime = 100; - private File dir1; - private File dir2; - - @Before - public void setup() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException { - - dir1 = File.createTempFile("temp_tests1", ""); - //noinspection ResultOfMethodCallIgnored,ResultOfMethodCallIgnored - dir1.delete(); - //noinspection ResultOfMethodCallIgnored,ResultOfMethodCallIgnored - dir1.mkdir(); - dir2 = File.createTempFile("temp_tests2", ""); - //noinspection ResultOfMethodCallIgnored,ResultOfMethodCallIgnored - dir2.delete(); - //noinspection ResultOfMethodCallIgnored,ResultOfMethodCallIgnored - dir2.mkdir(); - - UserThread.setExecutor(Executors.newSingleThreadExecutor()); - P2PDataStorage.CHECK_TTL_INTERVAL_SEC = 500; - - keyRing1 = new KeyRing(new KeyStorage(dir1)); - - storageSignatureKeyPair1 = keyRing1.getSignatureKeyPair(); - encryptionService1 = new EncryptionService(keyRing1, TestUtils.getNetworkProtoResolver()); - P2PService p2PService = TestUtils.getAndStartSeedNode(8001, useClearNet, seedNodes).getSeedNodeP2PService(); - networkNode1 = p2PService.getNetworkNode(); - peerManager1 = p2PService.getPeerManager(); - dataStorage1 = p2PService.getP2PDataStorage(); - - // for mailbox - keyRing2 = new KeyRing(new KeyStorage(dir2)); - storageSignatureKeyPair2 = keyRing2.getSignatureKeyPair(); - encryptionService2 = new EncryptionService(keyRing2, TestUtils.getNetworkProtoResolver()); - - mockData = new MockData("mockData", keyRing1.getSignatureKeyPair().getPublic()); - Thread.sleep(sleepTime); - } - - @After - public void tearDown() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException { - Thread.sleep(sleepTime); - if (dataStorage1 != null) dataStorage1.shutDown(); - if (peerManager1 != null) peerManager1.shutDown(); - - if (networkNode1 != null) { - CountDownLatch shutDownLatch = new CountDownLatch(1); - networkNode1.shutDown(shutDownLatch::countDown); - shutDownLatch.await(); - } - - Path path = Paths.get(TestUtils.test_dummy_dir); - File dir = path.toFile(); - FileUtil.deleteDirectory(dir); - FileUtil.deleteDirectory(dir1); - FileUtil.deleteDirectory(dir2); - } - - //@Test - public void testAddAndRemove() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException, NoSuchProviderException { - ProtectedStorageEntry data = dataStorage1.getProtectedStorageEntry(mockData, storageSignatureKeyPair1); - Assert.assertTrue(dataStorage1.addProtectedStorageEntry(data, null, null, true)); - Assert.assertEquals(1, dataStorage1.getMap().size()); - - int newSequenceNumber = data.getSequenceNumber() + 1; - byte[] hashOfDataAndSeqNr = P2PDataStorage.get32ByteHash(new P2PDataStorage.DataAndSeqNrPair(data.getProtectedStoragePayload(), newSequenceNumber)); - byte[] signature = Sig.sign(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr); - ProtectedStorageEntry dataToRemove = new ProtectedStorageEntry(data.getProtectedStoragePayload(), data.getOwnerPubKey(), newSequenceNumber, signature); - Assert.assertTrue(dataStorage1.remove(dataToRemove, null, true)); - Assert.assertEquals(0, dataStorage1.getMap().size()); - } - - // @Test - public void testTTL() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException, NoSuchProviderException { - mockData.setTtl((int) (P2PDataStorage.CHECK_TTL_INTERVAL_SEC * 1.5)); - ProtectedStorageEntry data = dataStorage1.getProtectedStorageEntry(mockData, storageSignatureKeyPair1); - log.debug("data.date " + data.getCreationTimeStamp()); - Assert.assertTrue(dataStorage1.addProtectedStorageEntry(data, null, null, true)); - log.debug("test 1"); - Assert.assertEquals(1, dataStorage1.getMap().size()); - - Thread.sleep(P2PDataStorage.CHECK_TTL_INTERVAL_SEC); - log.debug("test 2"); - Assert.assertEquals(1, dataStorage1.getMap().size()); - - Thread.sleep(P2PDataStorage.CHECK_TTL_INTERVAL_SEC * 2); - log.debug("test 3 removed"); - Assert.assertEquals(0, dataStorage1.getMap().size()); - } - - /* //@Test - public void testRePublish() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException, NoSuchProviderException { - mockData.ttl = (int) (P2PDataStorage.CHECK_TTL_INTERVAL_MILLIS * 1.5); - ProtectedStorageEntry data = dataStorage1.getDataWithSignedSeqNr(mockData, storageSignatureKeyPair1); - Assert.assertTrue(dataStorage1.add(data, null)); - Assert.assertEquals(1, dataStorage1.getMap().size()); - Thread.sleep(P2PDataStorage.CHECK_TTL_INTERVAL_MILLIS); - log.debug("test 1"); - Assert.assertEquals(1, dataStorage1.getMap().size()); - - data = dataStorage1.getDataWithSignedSeqNr(mockData, storageSignatureKeyPair1); - Assert.assertTrue(dataStorage1.rePublish(data, null)); - Thread.sleep(P2PDataStorage.CHECK_TTL_INTERVAL_MILLIS); - log.debug("test 2"); - Assert.assertEquals(1, dataStorage1.getMap().size()); - - data = dataStorage1.getDataWithSignedSeqNr(mockData, storageSignatureKeyPair1); - Assert.assertTrue(dataStorage1.rePublish(data, null)); - Thread.sleep(P2PDataStorage.CHECK_TTL_INTERVAL_MILLIS); - log.debug("test 3"); - Assert.assertEquals(1, dataStorage1.getMap().size()); - - Thread.sleep(P2PDataStorage.CHECK_TTL_INTERVAL_MILLIS); - log.debug("test 4"); - Assert.assertEquals(1, dataStorage1.getMap().size()); - - Thread.sleep(P2PDataStorage.CHECK_TTL_INTERVAL_MILLIS * 2); - log.debug("test 5 removed"); - Assert.assertEquals(0, dataStorage1.getMap().size()); - } - */ - @Test - public void testRefreshTTL() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException, NoSuchProviderException { - mockData.setTtl((int) (P2PDataStorage.CHECK_TTL_INTERVAL_SEC * 1.5)); - ProtectedStorageEntry data = dataStorage1.getProtectedStorageEntry(mockData, storageSignatureKeyPair1); - Assert.assertTrue(dataStorage1.addProtectedStorageEntry(data, null, null, true)); - Assert.assertEquals(1, dataStorage1.getMap().size()); - Thread.sleep(P2PDataStorage.CHECK_TTL_INTERVAL_SEC); - log.debug("test 1"); - Assert.assertEquals(1, dataStorage1.getMap().size()); - - RefreshOfferMessage refreshTTLMessage = dataStorage1.getRefreshTTLMessage(mockData, storageSignatureKeyPair1); - Assert.assertTrue(dataStorage1.refreshTTL(refreshTTLMessage, null, true)); - Thread.sleep(P2PDataStorage.CHECK_TTL_INTERVAL_SEC); - log.debug("test 2"); - Assert.assertEquals(1, dataStorage1.getMap().size()); - - refreshTTLMessage = dataStorage1.getRefreshTTLMessage(mockData, storageSignatureKeyPair1); - Assert.assertTrue(dataStorage1.refreshTTL(refreshTTLMessage, null, true)); - Thread.sleep(P2PDataStorage.CHECK_TTL_INTERVAL_SEC); - log.debug("test 3"); - Assert.assertEquals(1, dataStorage1.getMap().size()); - - Thread.sleep(P2PDataStorage.CHECK_TTL_INTERVAL_SEC); - log.debug("test 4"); - Assert.assertEquals(1, dataStorage1.getMap().size()); - - Thread.sleep(P2PDataStorage.CHECK_TTL_INTERVAL_SEC * 2); - log.debug("test 5 removed"); - Assert.assertEquals(0, dataStorage1.getMap().size()); - } -} diff --git a/p2p/src/test/java/bisq/network/p2p/storage/TestState.java b/p2p/src/test/java/bisq/network/p2p/storage/TestState.java new file mode 100644 index 00000000000..b13ec2a97a4 --- /dev/null +++ b/p2p/src/test/java/bisq/network/p2p/storage/TestState.java @@ -0,0 +1,373 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.network.p2p.storage; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.BroadcastHandler; +import bisq.network.p2p.peers.Broadcaster; +import bisq.network.p2p.storage.messages.AddDataMessage; +import bisq.network.p2p.storage.messages.AddPersistableNetworkPayloadMessage; +import bisq.network.p2p.storage.messages.BroadcastMessage; +import bisq.network.p2p.storage.messages.RefreshOfferMessage; +import bisq.network.p2p.storage.messages.RemoveDataMessage; +import bisq.network.p2p.storage.messages.RemoveMailboxDataMessage; +import bisq.network.p2p.storage.mocks.AppendOnlyDataStoreServiceFake; +import bisq.network.p2p.storage.mocks.ClockFake; +import bisq.network.p2p.storage.mocks.ProtectedDataStoreServiceFake; +import bisq.network.p2p.storage.payload.MailboxStoragePayload; +import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +import bisq.network.p2p.storage.payload.ProtectedMailboxStorageEntry; +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; +import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreListener; +import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreService; +import bisq.network.p2p.storage.persistence.ProtectedDataStoreListener; +import bisq.network.p2p.storage.persistence.ProtectedDataStoreService; +import bisq.network.p2p.storage.persistence.ResourceDataStoreService; +import bisq.network.p2p.storage.persistence.SequenceNumberMap; + +import bisq.common.crypto.Sig; +import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.storage.Storage; + +import java.security.PublicKey; + +import java.time.Clock; + +import java.util.concurrent.TimeUnit; + +import org.junit.Assert; + +import org.mockito.ArgumentCaptor; + +import static org.mockito.Mockito.*; + +/** + * Test object that stores a P2PDataStore instance as well as the mock objects necessary for state validation. + * + * Used in the P2PDataStorage*Test(s) in order to leverage common test set up and validation. + */ +public class TestState { + final P2PDataStorage mockedStorage; + final Broadcaster mockBroadcaster; + + final AppendOnlyDataStoreListener appendOnlyDataStoreListener; + private final ProtectedDataStoreListener protectedDataStoreListener; + final HashMapChangedListener hashMapChangedListener; + private final Storage mockSeqNrStorage; + final ClockFake clockFake; + + /** + * Subclass of P2PDataStorage that allows for easier testing, but keeps all functionality + */ + static class P2PDataStorageForTest extends P2PDataStorage { + + P2PDataStorageForTest(NetworkNode networkNode, + Broadcaster broadcaster, + AppendOnlyDataStoreService appendOnlyDataStoreService, + ProtectedDataStoreService protectedDataStoreService, + ResourceDataStoreService resourceDataStoreService, + Storage sequenceNumberMapStorage, + Clock clock) { + super(networkNode, broadcaster, appendOnlyDataStoreService, protectedDataStoreService, resourceDataStoreService, sequenceNumberMapStorage, clock); + + this.maxSequenceNumberMapSizeBeforePurge = 5; + } + } + + TestState() { + this.mockBroadcaster = mock(Broadcaster.class); + this.mockSeqNrStorage = mock(Storage.class); + this.clockFake = new ClockFake(); + + this.mockedStorage = new P2PDataStorageForTest(mock(NetworkNode.class), + this.mockBroadcaster, + new AppendOnlyDataStoreServiceFake(), + new ProtectedDataStoreServiceFake(), mock(ResourceDataStoreService.class), + this.mockSeqNrStorage, this.clockFake); + + this.appendOnlyDataStoreListener = mock(AppendOnlyDataStoreListener.class); + this.protectedDataStoreListener = mock(ProtectedDataStoreListener.class); + this.hashMapChangedListener = mock(HashMapChangedListener.class); + + this.mockedStorage.addHashMapChangedListener(this.hashMapChangedListener); + this.mockedStorage.addAppendOnlyDataStoreListener(this.appendOnlyDataStoreListener); + this.mockedStorage.addProtectedDataStoreListener(this.protectedDataStoreListener); + } + + private void resetState() { + reset(this.mockBroadcaster); + reset(this.appendOnlyDataStoreListener); + reset(this.protectedDataStoreListener); + reset(this.hashMapChangedListener); + reset(this.mockSeqNrStorage); + } + + void incrementClock() { + this.clockFake.increment(TimeUnit.HOURS.toMillis(1)); + } + + public static NodeAddress getTestNodeAddress() { + return new NodeAddress("address", 8080); + } + + /** + * Common test helpers that verify the correct events were signaled based on the test expectation and before/after states. + */ + private void verifySequenceNumberMapWriteContains(P2PDataStorage.ByteArray payloadHash, + int sequenceNumber) { + final ArgumentCaptor captor = ArgumentCaptor.forClass(SequenceNumberMap.class); + verify(this.mockSeqNrStorage).queueUpForSave(captor.capture(), anyLong()); + + SequenceNumberMap savedMap = captor.getValue(); + Assert.assertEquals(sequenceNumber, savedMap.get(payloadHash).sequenceNr); + } + + void verifyPersistableAdd(SavedTestState beforeState, + PersistableNetworkPayload persistableNetworkPayload, + boolean expectedStateChange, + boolean expectedBroadcastAndListenersSignaled, + boolean expectedIsDataOwner) { + P2PDataStorage.ByteArray hash = new P2PDataStorage.ByteArray(persistableNetworkPayload.getHash()); + + if (expectedStateChange) { + // Payload is accessible from get() + Assert.assertEquals(persistableNetworkPayload, this.mockedStorage.getAppendOnlyDataStoreMap().get(hash)); + } else { + // On failure, just ensure the state remained the same as before the add + if (beforeState.persistableNetworkPayloadBeforeOp != null) + Assert.assertEquals(beforeState.persistableNetworkPayloadBeforeOp, this.mockedStorage.getAppendOnlyDataStoreMap().get(hash)); + else + Assert.assertNull(this.mockedStorage.getAppendOnlyDataStoreMap().get(hash)); + } + + if (expectedStateChange && expectedBroadcastAndListenersSignaled) { + // Broadcast Called + verify(this.mockBroadcaster).broadcast(any(AddPersistableNetworkPayloadMessage.class), any(NodeAddress.class), + eq(null), eq(expectedIsDataOwner)); + + // Verify the listeners were updated once + verify(this.appendOnlyDataStoreListener).onAdded(persistableNetworkPayload); + + } else { + verify(this.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class), any(BroadcastHandler.Listener.class), anyBoolean()); + + // Verify the listeners were never updated + verify(this.appendOnlyDataStoreListener, never()).onAdded(persistableNetworkPayload); + } + } + + void verifyProtectedStorageAdd(SavedTestState beforeState, + ProtectedStorageEntry protectedStorageEntry, + boolean expectedStateChange, + boolean expectedIsDataOwner) { + P2PDataStorage.ByteArray hashMapHash = P2PDataStorage.get32ByteHashAsByteArray(protectedStorageEntry.getProtectedStoragePayload()); + P2PDataStorage.ByteArray storageHash = P2PDataStorage.getCompactHashAsByteArray(protectedStorageEntry.getProtectedStoragePayload()); + + if (expectedStateChange) { + Assert.assertEquals(protectedStorageEntry, this.mockedStorage.getMap().get(hashMapHash)); + + // PersistablePayload payloads need to be written to disk and listeners signaled... unless the hash already exists in the protectedDataStore. + // Note: this behavior is different from the HashMap listeners that are signaled on an increase in seq #, even if the hash already exists. + // TODO: Should the behavior be identical between this and the HashMap listeners? + // TODO: Do we want ot overwrite stale values in order to persist updated sequence numbers and timestamps? + if (protectedStorageEntry.getProtectedStoragePayload() instanceof PersistablePayload && beforeState.protectedStorageEntryBeforeOpDataStoreMap == null) { + Assert.assertEquals(protectedStorageEntry, this.mockedStorage.getProtectedDataStoreMap().get(storageHash)); + verify(this.protectedDataStoreListener).onAdded(protectedStorageEntry); + } else { + Assert.assertEquals(beforeState.protectedStorageEntryBeforeOpDataStoreMap, this.mockedStorage.getProtectedDataStoreMap().get(storageHash)); + verify(this.protectedDataStoreListener, never()).onAdded(protectedStorageEntry); + } + + verify(this.hashMapChangedListener).onAdded(protectedStorageEntry); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BroadcastMessage.class); + verify(this.mockBroadcaster).broadcast(captor.capture(), any(NodeAddress.class), + eq(null), eq(expectedIsDataOwner)); + + BroadcastMessage broadcastMessage = captor.getValue(); + Assert.assertTrue(broadcastMessage instanceof AddDataMessage); + Assert.assertEquals(protectedStorageEntry, ((AddDataMessage) broadcastMessage).getProtectedStorageEntry()); + + this.verifySequenceNumberMapWriteContains(P2PDataStorage.get32ByteHashAsByteArray(protectedStorageEntry.getProtectedStoragePayload()), protectedStorageEntry.getSequenceNumber()); + } else { + Assert.assertEquals(beforeState.protectedStorageEntryBeforeOp, this.mockedStorage.getMap().get(hashMapHash)); + Assert.assertEquals(beforeState.protectedStorageEntryBeforeOpDataStoreMap, this.mockedStorage.getProtectedDataStoreMap().get(storageHash)); + + verify(this.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class), any(BroadcastHandler.Listener.class), anyBoolean()); + + // Internal state didn't change... nothing should be notified + verify(this.hashMapChangedListener, never()).onAdded(protectedStorageEntry); + verify(this.protectedDataStoreListener, never()).onAdded(protectedStorageEntry); + verify(this.mockSeqNrStorage, never()).queueUpForSave(any(SequenceNumberMap.class), anyLong()); + } + } + + void verifyProtectedStorageRemove(SavedTestState beforeState, + ProtectedStorageEntry protectedStorageEntry, + boolean expectedStateChange, + boolean expectedBroadcastOnStateChange, + boolean expectedSeqNrWriteOnStateChange, + boolean expectedIsDataOwner) { + P2PDataStorage.ByteArray hashMapHash = P2PDataStorage.get32ByteHashAsByteArray(protectedStorageEntry.getProtectedStoragePayload()); + P2PDataStorage.ByteArray storageHash = P2PDataStorage.getCompactHashAsByteArray(protectedStorageEntry.getProtectedStoragePayload()); + + if (expectedStateChange) { + Assert.assertNull(this.mockedStorage.getMap().get(hashMapHash)); + + if (protectedStorageEntry.getProtectedStoragePayload() instanceof PersistablePayload) { + Assert.assertNull(this.mockedStorage.getProtectedDataStoreMap().get(storageHash)); + + verify(this.protectedDataStoreListener).onRemoved(protectedStorageEntry); + } + + verify(this.hashMapChangedListener).onRemoved(protectedStorageEntry); + + if (expectedSeqNrWriteOnStateChange) + this.verifySequenceNumberMapWriteContains(P2PDataStorage.get32ByteHashAsByteArray(protectedStorageEntry.getProtectedStoragePayload()), protectedStorageEntry.getSequenceNumber()); + + if (expectedBroadcastOnStateChange) { + if (protectedStorageEntry instanceof ProtectedMailboxStorageEntry) + verify(this.mockBroadcaster).broadcast(any(RemoveMailboxDataMessage.class), any(NodeAddress.class), eq(null), eq(expectedIsDataOwner)); + else + verify(this.mockBroadcaster).broadcast(any(RemoveDataMessage.class), any(NodeAddress.class), eq(null), eq(expectedIsDataOwner)); + } + + } else { + Assert.assertEquals(beforeState.protectedStorageEntryBeforeOp, this.mockedStorage.getMap().get(hashMapHash)); + + verify(this.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class), any(BroadcastHandler.Listener.class), anyBoolean()); + verify(this.hashMapChangedListener, never()).onAdded(protectedStorageEntry); + verify(this.protectedDataStoreListener, never()).onAdded(protectedStorageEntry); + verify(this.mockSeqNrStorage, never()).queueUpForSave(any(SequenceNumberMap.class), anyLong()); + } + } + + void verifyRefreshTTL(SavedTestState beforeState, + RefreshOfferMessage refreshOfferMessage, + boolean expectedStateChange, + boolean expectedIsDataOwner) { + P2PDataStorage.ByteArray payloadHash = new P2PDataStorage.ByteArray(refreshOfferMessage.getHashOfPayload()); + + ProtectedStorageEntry entryAfterRefresh = this.mockedStorage.getMap().get(payloadHash); + + if (expectedStateChange) { + Assert.assertNotNull(entryAfterRefresh); + Assert.assertEquals(refreshOfferMessage.getSequenceNumber(), entryAfterRefresh.getSequenceNumber()); + Assert.assertEquals(refreshOfferMessage.getSignature(), entryAfterRefresh.getSignature()); + Assert.assertTrue(entryAfterRefresh.getCreationTimeStamp() > beforeState.creationTimestampBeforeUpdate); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BroadcastMessage.class); + verify(this.mockBroadcaster).broadcast(captor.capture(), any(NodeAddress.class), + eq(null), eq(expectedIsDataOwner)); + + BroadcastMessage broadcastMessage = captor.getValue(); + Assert.assertTrue(broadcastMessage instanceof RefreshOfferMessage); + Assert.assertEquals(refreshOfferMessage, broadcastMessage); + + this.verifySequenceNumberMapWriteContains(payloadHash, refreshOfferMessage.getSequenceNumber()); + } else { + + // Verify the existing entry is unchanged + if (beforeState.protectedStorageEntryBeforeOp != null) { + Assert.assertEquals(entryAfterRefresh, beforeState.protectedStorageEntryBeforeOp); + Assert.assertEquals(beforeState.protectedStorageEntryBeforeOp.getSequenceNumber(), entryAfterRefresh.getSequenceNumber()); + Assert.assertEquals(beforeState.protectedStorageEntryBeforeOp.getSignature(), entryAfterRefresh.getSignature()); + Assert.assertEquals(beforeState.creationTimestampBeforeUpdate, entryAfterRefresh.getCreationTimeStamp()); + } + + verify(this.mockBroadcaster, never()).broadcast(any(BroadcastMessage.class), any(NodeAddress.class), any(BroadcastHandler.Listener.class), anyBoolean()); + verify(this.mockSeqNrStorage, never()).queueUpForSave(any(SequenceNumberMap.class), anyLong()); + } + } + + static MailboxStoragePayload buildMailboxStoragePayload(PublicKey senderKey, PublicKey receiverKey) { + // Need to be able to take the hash which leverages protobuf Messages + protobuf.StoragePayload messageMock = mock(protobuf.StoragePayload.class); + when(messageMock.toByteArray()).thenReturn(Sig.getPublicKeyBytes(receiverKey)); + + MailboxStoragePayload payloadMock = mock(MailboxStoragePayload.class); + when(payloadMock.getOwnerPubKey()).thenReturn(receiverKey); + when(payloadMock.getSenderPubKeyForAddOperation()).thenReturn(senderKey); + when(payloadMock.toProtoMessage()).thenReturn(messageMock); + + return payloadMock; + } + + SavedTestState saveTestState(PersistableNetworkPayload persistableNetworkPayload) { + return new SavedTestState(this, persistableNetworkPayload); + } + + SavedTestState saveTestState(ProtectedStorageEntry protectedStorageEntry) { + return new SavedTestState(this, protectedStorageEntry); + } + + SavedTestState saveTestState(RefreshOfferMessage refreshOfferMessage) { + return new SavedTestState(this, refreshOfferMessage); + } + + /** + * Wrapper object for TestState state that needs to be saved for future validation. Used in multiple tests + * to verify that the state before and after an operation matched the expectation. + */ + static class SavedTestState { + final TestState state; + + // Used in PersistableNetworkPayload tests + PersistableNetworkPayload persistableNetworkPayloadBeforeOp; + + // Used in ProtectedStorageEntry tests + ProtectedStorageEntry protectedStorageEntryBeforeOp; + ProtectedStorageEntry protectedStorageEntryBeforeOpDataStoreMap; + + long creationTimestampBeforeUpdate; + + private SavedTestState(TestState state) { + this.state = state; + this.creationTimestampBeforeUpdate = 0; + this.state.resetState(); + } + + private SavedTestState(TestState testState, PersistableNetworkPayload persistableNetworkPayload) { + this(testState); + P2PDataStorage.ByteArray hash = new P2PDataStorage.ByteArray(persistableNetworkPayload.getHash()); + this.persistableNetworkPayloadBeforeOp = testState.mockedStorage.getAppendOnlyDataStoreMap().get(hash); + } + + private SavedTestState(TestState testState, ProtectedStorageEntry protectedStorageEntry) { + this(testState); + + P2PDataStorage.ByteArray storageHash = P2PDataStorage.getCompactHashAsByteArray(protectedStorageEntry.getProtectedStoragePayload()); + this.protectedStorageEntryBeforeOpDataStoreMap = testState.mockedStorage.getProtectedDataStoreMap().get(storageHash); + + P2PDataStorage.ByteArray hashMapHash = P2PDataStorage.get32ByteHashAsByteArray(protectedStorageEntry.getProtectedStoragePayload()); + this.protectedStorageEntryBeforeOp = testState.mockedStorage.getMap().get(hashMapHash); + + this.creationTimestampBeforeUpdate = (this.protectedStorageEntryBeforeOp != null) ? this.protectedStorageEntryBeforeOp.getCreationTimeStamp() : 0; + } + + private SavedTestState(TestState testState, RefreshOfferMessage refreshOfferMessage) { + this(testState); + + P2PDataStorage.ByteArray hashMapHash = new P2PDataStorage.ByteArray(refreshOfferMessage.getHashOfPayload()); + this.protectedStorageEntryBeforeOp = testState.mockedStorage.getMap().get(hashMapHash); + + this.creationTimestampBeforeUpdate = (this.protectedStorageEntryBeforeOp != null) ? this.protectedStorageEntryBeforeOp.getCreationTimeStamp() : 0; + } + } +} diff --git a/p2p/src/test/java/bisq/network/p2p/storage/messages/AddDataMessageTest.java b/p2p/src/test/java/bisq/network/p2p/storage/messages/AddDataMessageTest.java index 43ac2b5655b..3c70ff6b7eb 100644 --- a/p2p/src/test/java/bisq/network/p2p/storage/messages/AddDataMessageTest.java +++ b/p2p/src/test/java/bisq/network/p2p/storage/messages/AddDataMessageTest.java @@ -36,6 +36,8 @@ import java.security.SignatureException; import java.security.cert.CertificateException; +import java.time.Clock; + import java.io.File; import java.io.IOException; @@ -72,7 +74,7 @@ public void toProtoBuf() throws Exception { MailboxStoragePayload mailboxStoragePayload = new MailboxStoragePayload(prefixedSealedAndSignedMessage, keyRing1.getPubKeyRing().getSignaturePubKey(), keyRing1.getPubKeyRing().getSignaturePubKey()); ProtectedStorageEntry protectedStorageEntry = new ProtectedMailboxStorageEntry(mailboxStoragePayload, - keyRing1.getSignatureKeyPair().getPublic(), 1, RandomUtils.nextBytes(10), keyRing1.getPubKeyRing().getSignaturePubKey()); + keyRing1.getSignatureKeyPair().getPublic(), 1, RandomUtils.nextBytes(10), keyRing1.getPubKeyRing().getSignaturePubKey(), Clock.systemDefaultZone()); AddDataMessage dataMessage1 = new AddDataMessage(protectedStorageEntry); protobuf.NetworkEnvelope envelope = dataMessage1.toProtoNetworkEnvelope(); diff --git a/p2p/src/test/java/bisq/network/p2p/storage/mocks/AppendOnlyDataStoreServiceFake.java b/p2p/src/test/java/bisq/network/p2p/storage/mocks/AppendOnlyDataStoreServiceFake.java index e9e38cc0148..4119ff1426c 100644 --- a/p2p/src/test/java/bisq/network/p2p/storage/mocks/AppendOnlyDataStoreServiceFake.java +++ b/p2p/src/test/java/bisq/network/p2p/storage/mocks/AppendOnlyDataStoreServiceFake.java @@ -24,6 +24,12 @@ import java.util.HashMap; import java.util.Map; +/** + * Implementation of an in-memory AppendOnlyDataStoreService that can be used in tests. Removes overhead + * involving files, resources, and services for tests that don't need it. + * + * @see Reference + */ public class AppendOnlyDataStoreServiceFake extends AppendOnlyDataStoreService { private final Map map; diff --git a/p2p/src/test/java/bisq/network/p2p/storage/mocks/ClockFake.java b/p2p/src/test/java/bisq/network/p2p/storage/mocks/ClockFake.java new file mode 100644 index 00000000000..6f5e42245a6 --- /dev/null +++ b/p2p/src/test/java/bisq/network/p2p/storage/mocks/ClockFake.java @@ -0,0 +1,54 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.network.p2p.storage.mocks; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; + +/** + * Fake implementation of the Clock object that can be used in tests that need finer control over the current time. + * + * @see Reference + */ +public class ClockFake extends Clock { + private Instant currentInstant; + + public ClockFake() { + this.currentInstant = Instant.now(); + } + + @Override + public ZoneId getZone() { + throw new UnsupportedOperationException("ClockFake does not support getZone"); + } + + @Override + public Clock withZone(ZoneId zoneId) { + throw new UnsupportedOperationException("ClockFake does not support withZone"); + } + + @Override + public Instant instant() { + return this.currentInstant; + } + + public void increment(long milliseconds) { + this.currentInstant = this.currentInstant.plusMillis(milliseconds); + } +} diff --git a/p2p/src/test/java/bisq/network/p2p/storage/mocks/DateTolerantPayloadStub.java b/p2p/src/test/java/bisq/network/p2p/storage/mocks/DateTolerantPayloadStub.java index 327237bffcf..ce709c2e2b8 100644 --- a/p2p/src/test/java/bisq/network/p2p/storage/mocks/DateTolerantPayloadStub.java +++ b/p2p/src/test/java/bisq/network/p2p/storage/mocks/DateTolerantPayloadStub.java @@ -21,6 +21,13 @@ import java.time.Clock; +/** + * Stub implementation of a ProtectedStoragePayload implementing the DateTolerantPayload marker interface + * that can be used in tests to provide canned answers to calls. Useful if the tests don't care about the implementation + * details of the ProtectedStoragePayload. + * + * @see Reference + */ public class DateTolerantPayloadStub implements DateTolerantPayload { private final boolean dateInTolerance; diff --git a/p2p/src/test/java/bisq/network/p2p/storage/mocks/ExpirableProtectedStoragePayloadStub.java b/p2p/src/test/java/bisq/network/p2p/storage/mocks/ExpirableProtectedStoragePayloadStub.java new file mode 100644 index 00000000000..16c70fbd27a --- /dev/null +++ b/p2p/src/test/java/bisq/network/p2p/storage/mocks/ExpirableProtectedStoragePayloadStub.java @@ -0,0 +1,59 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.network.p2p.storage.mocks; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.storage.TestState; +import bisq.network.p2p.storage.payload.ExpirablePayload; +import bisq.network.p2p.storage.payload.RequiresOwnerIsOnlinePayload; + +import java.security.PublicKey; + +import java.util.concurrent.TimeUnit; + +/** + * Stub implementation of a ProtectedStoragePayloadStub implementing the ExpirablePayload & RequiresOwnerIsOnlinePayload + * marker interfaces that can be used in tests to provide canned answers to calls. Useful if the tests don't care about + * the implementation details of the ProtectedStoragePayload. + * + * @see Reference + */ +public class ExpirableProtectedStoragePayloadStub extends ProtectedStoragePayloadStub + implements ExpirablePayload, RequiresOwnerIsOnlinePayload { + private long ttl; + + public ExpirableProtectedStoragePayloadStub(PublicKey ownerPubKey) { + super(ownerPubKey); + ttl = TimeUnit.DAYS.toMillis(90); + } + + public ExpirableProtectedStoragePayloadStub(PublicKey ownerPubKey, long ttl) { + this(ownerPubKey); + this.ttl = ttl; + } + + @Override + public NodeAddress getOwnerNodeAddress() { + return TestState.getTestNodeAddress(); + } + + @Override + public long getTTL() { + return this.ttl; + } +} diff --git a/p2p/src/test/java/bisq/network/p2p/storage/mocks/PersistableExpirableProtectedStoragePayloadStub.java b/p2p/src/test/java/bisq/network/p2p/storage/mocks/PersistableExpirableProtectedStoragePayloadStub.java new file mode 100644 index 00000000000..b1e83b78cf4 --- /dev/null +++ b/p2p/src/test/java/bisq/network/p2p/storage/mocks/PersistableExpirableProtectedStoragePayloadStub.java @@ -0,0 +1,40 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.network.p2p.storage.mocks; + +import bisq.common.proto.persistable.PersistablePayload; + +import java.security.PublicKey; +/** + * Stub implementation of a ProtectedStoragePayloadStub implementing the ExpirablePayload & RequiresOwnerIsOnlinePayload + * & PersistablePayload marker interfaces that can be used in tests to provide canned answers to calls. Useful if the + * tests don't care about the implementation details of the ProtectedStoragePayload. + * + * @see Reference + */ +public class PersistableExpirableProtectedStoragePayloadStub extends ExpirableProtectedStoragePayloadStub + implements PersistablePayload { + + public PersistableExpirableProtectedStoragePayloadStub(PublicKey ownerPubKey) { + super(ownerPubKey); + } + + public PersistableExpirableProtectedStoragePayloadStub(PublicKey ownerPubKey, long ttl) { + super(ownerPubKey, ttl); + } +} diff --git a/p2p/src/test/java/bisq/network/p2p/storage/mocks/PersistableNetworkPayloadStub.java b/p2p/src/test/java/bisq/network/p2p/storage/mocks/PersistableNetworkPayloadStub.java index 1d2a8e1fbe7..6ab73155480 100644 --- a/p2p/src/test/java/bisq/network/p2p/storage/mocks/PersistableNetworkPayloadStub.java +++ b/p2p/src/test/java/bisq/network/p2p/storage/mocks/PersistableNetworkPayloadStub.java @@ -19,6 +19,13 @@ import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +/** + * Stub implementation of a PersistableNetworkPayload that can be used in tests + * to provide canned answers to calls. Useful if the tests don't care about the implementation + * * details of the PersistableNetworkPayload. + * + * @see Reference + */ public class PersistableNetworkPayloadStub implements PersistableNetworkPayload { private final boolean hashSizeValid; diff --git a/p2p/src/test/java/bisq/network/p2p/storage/mocks/ProtectedDataStoreServiceFake.java b/p2p/src/test/java/bisq/network/p2p/storage/mocks/ProtectedDataStoreServiceFake.java index 0ad22d88466..717a3b806b4 100644 --- a/p2p/src/test/java/bisq/network/p2p/storage/mocks/ProtectedDataStoreServiceFake.java +++ b/p2p/src/test/java/bisq/network/p2p/storage/mocks/ProtectedDataStoreServiceFake.java @@ -24,6 +24,12 @@ import java.util.HashMap; import java.util.Map; +/** + * Implementation of an in-memory ProtectedDataStoreService that can be used in tests. Removes overhead + * involving files, resources, and services for tests that don't need it. + * + * @see Reference + */ public class ProtectedDataStoreServiceFake extends ProtectedDataStoreService { private final Map map; diff --git a/p2p/src/test/java/bisq/network/p2p/storage/mocks/ProtectedStoragePayloadStub.java b/p2p/src/test/java/bisq/network/p2p/storage/mocks/ProtectedStoragePayloadStub.java index 5a176fca302..d6be958138f 100644 --- a/p2p/src/test/java/bisq/network/p2p/storage/mocks/ProtectedStoragePayloadStub.java +++ b/p2p/src/test/java/bisq/network/p2p/storage/mocks/ProtectedStoragePayloadStub.java @@ -34,14 +34,18 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -/* - * Stub ProtectedStoragePayload whose hash is equal to the ownerPubKey +/** + * Stub implementation of a ProtectedStoragePayload that can be used in tests + * to provide canned answers to calls. Useful if the tests don't care about the implementation + * details of the ProtectedStoragePayload. + * + * @see Reference */ public class ProtectedStoragePayloadStub implements ProtectedStoragePayload { @Getter private PublicKey ownerPubKey; - private Message messageMock; + protected Message messageMock; public ProtectedStoragePayloadStub(PublicKey ownerPubKey) { this.ownerPubKey = ownerPubKey; diff --git a/p2p/src/test/java/bisq/network/p2p/storage/payload/ProtectedMailboxStorageEntryTest.java b/p2p/src/test/java/bisq/network/p2p/storage/payload/ProtectedMailboxStorageEntryTest.java new file mode 100644 index 00000000000..74d466aa148 --- /dev/null +++ b/p2p/src/test/java/bisq/network/p2p/storage/payload/ProtectedMailboxStorageEntryTest.java @@ -0,0 +1,199 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.network.p2p.storage.payload; + +import bisq.network.p2p.PrefixedSealedAndSignedMessage; +import bisq.network.p2p.TestUtils; +import bisq.network.p2p.storage.P2PDataStorage; + +import bisq.common.app.Version; +import bisq.common.crypto.CryptoException; +import bisq.common.crypto.Sig; + +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; + +import java.time.Clock; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.Mockito.*; + +public class ProtectedMailboxStorageEntryTest { + + private static MailboxStoragePayload buildMailboxStoragePayload(PublicKey payloadSenderPubKeyForAddOperation, + PublicKey payloadOwnerPubKey) { + + // Mock out the PrefixedSealedAndSignedMessage with a version that just serializes to the DEFAULT_INSTANCE + // in protobuf. This object is never validated in the test, but needs to be hashed as part of the testing path. + PrefixedSealedAndSignedMessage prefixedSealedAndSignedMessageMock = mock(PrefixedSealedAndSignedMessage.class); + protobuf.NetworkEnvelope networkEnvelopeMock = mock(protobuf.NetworkEnvelope.class); + when(networkEnvelopeMock.getPrefixedSealedAndSignedMessage()).thenReturn( + protobuf.PrefixedSealedAndSignedMessage.getDefaultInstance()); + when(prefixedSealedAndSignedMessageMock.toProtoNetworkEnvelope()).thenReturn(networkEnvelopeMock); + + return new MailboxStoragePayload( + prefixedSealedAndSignedMessageMock, payloadSenderPubKeyForAddOperation, payloadOwnerPubKey); + } + + private static ProtectedMailboxStorageEntry buildProtectedMailboxStorageEntry( + MailboxStoragePayload mailboxStoragePayload, KeyPair ownerKey, PublicKey receiverKey, int sequenceNumber) throws CryptoException { + byte[] hashOfDataAndSeqNr = P2PDataStorage.get32ByteHash(new P2PDataStorage.DataAndSeqNrPair(mailboxStoragePayload, sequenceNumber)); + byte[] signature = Sig.sign(ownerKey.getPrivate(), hashOfDataAndSeqNr); + + return new ProtectedMailboxStorageEntry(mailboxStoragePayload, ownerKey.getPublic(), sequenceNumber, signature, receiverKey, Clock.systemDefaultZone()); + } + + @Before + public void SetUp() { + // Deep in the bowels of protobuf we grab the messageID from the version module. This is required to hash the + // full MailboxStoragePayload so make sure it is initialized. + Version.setBaseCryptoNetworkId(1); + } + + // TESTCASE: validForAddOperation() should return true if the Entry owner and sender key specified in payload match + @Test + public void isValidForAddOperation() throws NoSuchAlgorithmException, CryptoException { + KeyPair senderKeys = TestUtils.generateKeyPair(); + KeyPair receiverKeys = TestUtils.generateKeyPair(); + + MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); + ProtectedStorageEntry protectedStorageEntry = buildProtectedMailboxStorageEntry(mailboxStoragePayload, senderKeys, receiverKeys.getPublic(), 1); + + Assert.assertTrue(protectedStorageEntry.isValidForAddOperation()); + } + + // TESTCASE: validForAddOperation() should return false if the Entry owner and sender key specified in payload don't match + @Test + public void isValidForAddOperation_EntryOwnerPayloadReceiverMismatch() throws NoSuchAlgorithmException, CryptoException { + KeyPair senderKeys = TestUtils.generateKeyPair(); + KeyPair receiverKeys = TestUtils.generateKeyPair(); + + MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); + ProtectedStorageEntry protectedStorageEntry = buildProtectedMailboxStorageEntry(mailboxStoragePayload, receiverKeys, receiverKeys.getPublic(), 1); + + Assert.assertFalse(protectedStorageEntry.isValidForAddOperation()); + } + + // TESTCASE: validForAddOperation() should fail if Entry.receiversPubKey and Payload.ownerPubKey don't match + @Test + public void isValidForAddOperation_EntryReceiverPayloadReceiverMismatch() throws NoSuchAlgorithmException, CryptoException { + KeyPair senderKeys = TestUtils.generateKeyPair(); + KeyPair receiverKeys = TestUtils.generateKeyPair(); + + MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); + ProtectedStorageEntry protectedStorageEntry = buildProtectedMailboxStorageEntry(mailboxStoragePayload, senderKeys, senderKeys.getPublic(), 1); + + Assert.assertFalse(protectedStorageEntry.isValidForAddOperation()); + } + + // TESTCASE: validForAddOperation() should fail if the signature isn't valid + @Test + public void isValidForAddOperation_BadSignature() throws NoSuchAlgorithmException, CryptoException { + KeyPair senderKeys = TestUtils.generateKeyPair(); + KeyPair receiverKeys = TestUtils.generateKeyPair(); + + MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); + ProtectedStorageEntry protectedStorageEntry = buildProtectedMailboxStorageEntry(mailboxStoragePayload, senderKeys, receiverKeys.getPublic(), 1); + + protectedStorageEntry.updateSignature( new byte[] { 0 }); + + Assert.assertFalse(protectedStorageEntry.isValidForAddOperation()); + } + + // TESTCASE: validForRemoveOperation() should return true if the Entry owner and payload owner match + @Test + public void validForRemove() throws NoSuchAlgorithmException, CryptoException { + KeyPair senderKeys = TestUtils.generateKeyPair(); + KeyPair receiverKeys = TestUtils.generateKeyPair(); + + MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); + ProtectedStorageEntry protectedStorageEntry = buildProtectedMailboxStorageEntry(mailboxStoragePayload, receiverKeys, receiverKeys.getPublic(), 1); + + Assert.assertTrue(protectedStorageEntry.isValidForRemoveOperation()); + } + + // TESTCASE: validForRemoveOperation() should return false if the Entry owner and payload owner don't match + @Test + public void validForRemoveEntryOwnerPayloadOwnerMismatch() throws NoSuchAlgorithmException, CryptoException { + KeyPair senderKeys = TestUtils.generateKeyPair(); + KeyPair receiverKeys = TestUtils.generateKeyPair(); + + MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); + ProtectedStorageEntry protectedStorageEntry = buildProtectedMailboxStorageEntry(mailboxStoragePayload, senderKeys, receiverKeys.getPublic(), 1); + + Assert.assertFalse(protectedStorageEntry.isValidForRemoveOperation()); + } + + // TESTCASE: isValidForRemoveOperation() should fail if the signature is bad + @Test + public void isValidForRemoveOperation_BadSignature() throws NoSuchAlgorithmException, CryptoException { + KeyPair senderKeys = TestUtils.generateKeyPair(); + KeyPair receiverKeys = TestUtils.generateKeyPair(); + + MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); + ProtectedStorageEntry protectedStorageEntry = buildProtectedMailboxStorageEntry(mailboxStoragePayload, receiverKeys, receiverKeys.getPublic(), 1); + + protectedStorageEntry.updateSignature(new byte[] { 0 }); + + Assert.assertFalse(protectedStorageEntry.isValidForRemoveOperation()); + } + + // TESTCASE: isValidForRemoveOperation() should fail if the receiversPubKey does not match the Entry owner + @Test + public void isValidForRemoveOperation_ReceiversPubKeyMismatch() throws NoSuchAlgorithmException, CryptoException { + KeyPair senderKeys = TestUtils.generateKeyPair(); + KeyPair receiverKeys = TestUtils.generateKeyPair(); + + MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); + ProtectedStorageEntry protectedStorageEntry = buildProtectedMailboxStorageEntry(mailboxStoragePayload, receiverKeys, senderKeys.getPublic(), 1); + + Assert.assertFalse(protectedStorageEntry.isValidForRemoveOperation()); + } + + // TESTCASE: isMetadataEquals() should succeed if the sequence number changes + @Test + public void isMetadataEquals() throws NoSuchAlgorithmException, CryptoException { + KeyPair senderKeys = TestUtils.generateKeyPair(); + KeyPair receiverKeys = TestUtils.generateKeyPair(); + + MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); + ProtectedStorageEntry seqNrOne = buildProtectedMailboxStorageEntry(mailboxStoragePayload, senderKeys, receiverKeys.getPublic(), 1); + + ProtectedStorageEntry seqNrTwo = buildProtectedMailboxStorageEntry(mailboxStoragePayload, senderKeys, receiverKeys.getPublic(), 2); + + Assert.assertTrue(seqNrOne.matchesRelevantPubKey(seqNrTwo)); + } + + // TESTCASE: isMetadataEquals() should fail if the receiversPubKey changes + @Test + public void isMetadataEquals_receiverPubKeyChanged() throws NoSuchAlgorithmException, CryptoException { + KeyPair senderKeys = TestUtils.generateKeyPair(); + KeyPair receiverKeys = TestUtils.generateKeyPair(); + + MailboxStoragePayload mailboxStoragePayload = buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()); + ProtectedStorageEntry seqNrOne = buildProtectedMailboxStorageEntry(mailboxStoragePayload, senderKeys, receiverKeys.getPublic(), 1); + + ProtectedStorageEntry seqNrTwo = buildProtectedMailboxStorageEntry(mailboxStoragePayload, senderKeys, senderKeys.getPublic(), 1); + + Assert.assertFalse(seqNrOne.matchesRelevantPubKey(seqNrTwo)); + } +} diff --git a/p2p/src/test/java/bisq/network/p2p/storage/payload/ProtectedStorageEntryTest.java b/p2p/src/test/java/bisq/network/p2p/storage/payload/ProtectedStorageEntryTest.java new file mode 100644 index 00000000000..1b5313977e7 --- /dev/null +++ b/p2p/src/test/java/bisq/network/p2p/storage/payload/ProtectedStorageEntryTest.java @@ -0,0 +1,252 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.network.p2p.storage.payload; + +import bisq.network.p2p.PrefixedSealedAndSignedMessage; +import bisq.network.p2p.TestUtils; +import bisq.network.p2p.storage.P2PDataStorage; +import bisq.network.p2p.storage.mocks.ProtectedStoragePayloadStub; + +import bisq.common.app.Version; +import bisq.common.crypto.CryptoException; +import bisq.common.crypto.Sig; + +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; + +import java.time.Clock; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ProtectedStorageEntryTest { + private static ProtectedStorageEntry buildProtectedStorageEntry(KeyPair payloadOwner, KeyPair entryOwner, int sequenceNumber) throws CryptoException { + return buildProtectedStorageEntry(new ProtectedStoragePayloadStub(payloadOwner.getPublic()), entryOwner, sequenceNumber); + } + + private static ProtectedStorageEntry buildProtectedStorageEntry(ProtectedStoragePayload protectedStoragePayload, + KeyPair entryOwner, int sequenceNumber) throws CryptoException { + + byte[] hashOfDataAndSeqNr = P2PDataStorage.get32ByteHash(new P2PDataStorage.DataAndSeqNrPair(protectedStoragePayload, sequenceNumber)); + byte[] signature = Sig.sign(entryOwner.getPrivate(), hashOfDataAndSeqNr); + + return new ProtectedStorageEntry(protectedStoragePayload, entryOwner.getPublic(), sequenceNumber, + signature, Clock.systemDefaultZone()); + } + + private static MailboxStoragePayload buildMailboxStoragePayload(PublicKey payloadSenderPubKeyForAddOperation, + PublicKey payloadOwnerPubKey) { + + // Mock out the PrefixedSealedAndSignedMessage with a version that just serializes to the DEFAULT_INSTANCE + // in protobuf. This object is never validated in the test, but needs to be hashed as part of the testing path. + PrefixedSealedAndSignedMessage prefixedSealedAndSignedMessageMock = mock(PrefixedSealedAndSignedMessage.class); + protobuf.NetworkEnvelope networkEnvelopeMock = mock(protobuf.NetworkEnvelope.class); + when(networkEnvelopeMock.getPrefixedSealedAndSignedMessage()).thenReturn( + protobuf.PrefixedSealedAndSignedMessage.getDefaultInstance()); + when(prefixedSealedAndSignedMessageMock.toProtoNetworkEnvelope()).thenReturn(networkEnvelopeMock); + + return new MailboxStoragePayload( + prefixedSealedAndSignedMessageMock, payloadSenderPubKeyForAddOperation, payloadOwnerPubKey); + } + + @Before + public void SetUp() { + // Deep in the bowels of protobuf we grab the messageID from the version module. This is required to hash the + // full MailboxStoragePayload so make sure it is initialized. + Version.setBaseCryptoNetworkId(1); + } + + // TESTCASE: validForAddOperation() should return true if the Entry owner and payload owner match + @Test + public void isValidForAddOperation() throws NoSuchAlgorithmException, CryptoException { + KeyPair ownerKeys = TestUtils.generateKeyPair(); + ProtectedStorageEntry protectedStorageEntry = buildProtectedStorageEntry(ownerKeys, ownerKeys, 1); + + Assert.assertTrue(protectedStorageEntry.isValidForAddOperation()); + } + + // TESTCASE: validForAddOperation() should return false if the Entry owner and payload owner don't match + @Test + public void isValidForAddOperation_Mismatch() throws NoSuchAlgorithmException, CryptoException { + KeyPair ownerKeys = TestUtils.generateKeyPair(); + KeyPair notOwnerKeys = TestUtils.generateKeyPair(); + ProtectedStorageEntry protectedStorageEntry = buildProtectedStorageEntry(ownerKeys, notOwnerKeys, 1); + + Assert.assertFalse(protectedStorageEntry.isValidForAddOperation()); + } + + // TESTCASE: validForAddOperation() should fail if the entry is a MailboxStoragePayload wrapped in a + // ProtectedStorageEntry and the Entry is owned by the sender + // XXXBUGXXX: Currently, a mis-wrapped MailboxStorageEntry will circumvent the senderPubKeyForAddOperation checks + @Test + public void isValidForAddOperation_invalidMailboxPayloadSender() throws NoSuchAlgorithmException, CryptoException { + KeyPair senderKeys = TestUtils.generateKeyPair(); + KeyPair receiverKeys = TestUtils.generateKeyPair(); + + ProtectedStorageEntry protectedStorageEntry = buildProtectedStorageEntry( + buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()), senderKeys, 1); + + // should be assertFalse + Assert.assertTrue(protectedStorageEntry.isValidForAddOperation()); + } + + // TESTCASE: validForAddOperation() should fail if the entry is a MailboxStoragePayload wrapped in a + // ProtectedStorageEntry and the Entry is owned by the receiver + @Test + public void isValidForAddOperation_invalidMailboxPayloadReceiver() throws NoSuchAlgorithmException, CryptoException { + KeyPair senderKeys = TestUtils.generateKeyPair(); + KeyPair receiverKeys = TestUtils.generateKeyPair(); + + ProtectedStorageEntry protectedStorageEntry = buildProtectedStorageEntry( + buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()), receiverKeys, 1); + + Assert.assertFalse(protectedStorageEntry.isValidForAddOperation()); + } + + // TESTCASE: validForAddOperation() should fail if the signature isn't valid + @Test + public void isValidForAddOperation_BadSignature() throws NoSuchAlgorithmException, CryptoException { + KeyPair ownerKeys = TestUtils.generateKeyPair(); + ProtectedStorageEntry protectedStorageEntry = buildProtectedStorageEntry(ownerKeys, ownerKeys, 1); + + protectedStorageEntry.updateSignature( new byte[] { 0 }); + + Assert.assertFalse(protectedStorageEntry.isValidForAddOperation()); + } + + // TESTCASE: validForRemoveOperation() should return true if the Entry owner and payload owner match + @Test + public void isValidForRemoveOperation() throws NoSuchAlgorithmException, CryptoException { + KeyPair ownerKeys = TestUtils.generateKeyPair(); + ProtectedStorageEntry protectedStorageEntry = buildProtectedStorageEntry(ownerKeys, ownerKeys, 1); + + Assert.assertTrue(protectedStorageEntry.isValidForRemoveOperation()); + } + + // TESTCASE: validForRemoveOperation() should return false if the Entry owner and payload owner don't match + @Test + public void isValidForRemoveOperation_Mismatch() throws NoSuchAlgorithmException, CryptoException { + KeyPair ownerKeys = TestUtils.generateKeyPair(); + KeyPair notOwnerKeys = TestUtils.generateKeyPair(); + ProtectedStorageEntry protectedStorageEntry = buildProtectedStorageEntry(ownerKeys, notOwnerKeys, 1); + + Assert.assertFalse(protectedStorageEntry.isValidForRemoveOperation()); + } + + // TESTCASE: validForRemoveOperation() should fail if the entry is a MailboxStoragePayload wrapped in a + // ProtectedStorageEntry and the Entry is owned by the sender + // XXXBUGXXX: Currently, a mis-wrapped MailboxStoragePayload will succeed + @Test + public void isValidForRemoveOperation_invalidMailboxPayloadSender() throws NoSuchAlgorithmException, CryptoException { + KeyPair senderKeys = TestUtils.generateKeyPair(); + KeyPair receiverKeys = TestUtils.generateKeyPair(); + + ProtectedStorageEntry protectedStorageEntry = buildProtectedStorageEntry( + buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()), senderKeys, 1); + + // should be assertFalse + Assert.assertTrue(protectedStorageEntry.isValidForRemoveOperation()); + } + + @Test + public void isValidForRemoveOperation_invalidMailboxPayloadReceiver() throws NoSuchAlgorithmException, CryptoException { + KeyPair senderKeys = TestUtils.generateKeyPair(); + KeyPair receiverKeys = TestUtils.generateKeyPair(); + + ProtectedStorageEntry protectedStorageEntry = buildProtectedStorageEntry( + buildMailboxStoragePayload(senderKeys.getPublic(), receiverKeys.getPublic()), receiverKeys, 1); + + Assert.assertFalse(protectedStorageEntry.isValidForRemoveOperation()); + } + + // TESTCASE: isValidForRemoveOperation() should fail if the signature is bad + @Test + public void isValidForRemoveOperation_BadSignature() throws NoSuchAlgorithmException, CryptoException { + KeyPair ownerKeys = TestUtils.generateKeyPair(); + ProtectedStorageEntry protectedStorageEntry = buildProtectedStorageEntry(ownerKeys, ownerKeys, 1); + + protectedStorageEntry.updateSignature(new byte[] { 0 }); + + Assert.assertFalse(protectedStorageEntry.isValidForRemoveOperation()); + } + + // TESTCASE: isMetadataEquals() should succeed if the sequence number changes + @Test + public void isMetadataEquals() throws NoSuchAlgorithmException, CryptoException { + KeyPair ownerKeys = TestUtils.generateKeyPair(); + ProtectedStorageEntry seqNrOne = buildProtectedStorageEntry(ownerKeys, ownerKeys, 1); + + ProtectedStorageEntry seqNrTwo = buildProtectedStorageEntry(ownerKeys, ownerKeys, 2); + + Assert.assertTrue(seqNrOne.matchesRelevantPubKey(seqNrTwo)); + } + + // TESTCASE: isMetadataEquals() should fail if the OwnerPubKey changes + @Test + public void isMetadataEquals_OwnerPubKeyChanged() throws NoSuchAlgorithmException, CryptoException { + KeyPair ownerKeys = TestUtils.generateKeyPair(); + KeyPair notOwner = TestUtils.generateKeyPair(); + + ProtectedStorageEntry protectedStorageEntryOne = buildProtectedStorageEntry(ownerKeys, ownerKeys, 1); + + ProtectedStorageEntry protectedStorageEntryTwo = buildProtectedStorageEntry(ownerKeys, notOwner, 1); + + Assert.assertFalse(protectedStorageEntryOne.matchesRelevantPubKey(protectedStorageEntryTwo)); + } + + // TESTCASE: Payload implementing ProtectedStoragePayload & PersistableNetworkPayload is invalid + // We rely on the fact that a payload is either a ProtectedStoragePayload OR PersistableNetworkPayload, but Java + // does not have a clean way to specify mutually exclusive interfaces. + // + // We also want to guarantee that ONLY ProtectedStoragePayload objects are valid as payloads in + // ProtectedStorageEntrys. This test will give a defense in case future development work breaks that expectation. + @Test(expected = IllegalArgumentException.class) + public void ProtectedStoragePayload_PersistableNetworkPayload_incompatible() throws NoSuchAlgorithmException { + class IncompatiblePayload extends ProtectedStoragePayloadStub implements PersistableNetworkPayload { + + private IncompatiblePayload(PublicKey ownerPubKey) { + super(ownerPubKey); + } + + @Override + public byte[] getHash() { + return new byte[0]; + } + + @Override + public boolean verifyHashSize() { + return true; + } + + @Override + public protobuf.PersistableNetworkPayload toProtoMessage() { + return (protobuf.PersistableNetworkPayload) this.messageMock; + } + } + + KeyPair ownerKeys = TestUtils.generateKeyPair(); + IncompatiblePayload incompatiblePayload = new IncompatiblePayload(ownerKeys.getPublic()); + new ProtectedStorageEntry(incompatiblePayload,ownerKeys.getPublic(), 1, + new byte[] { 0 }, Clock.systemDefaultZone()); + } +}