/*
* This file is part of Bitsquare.
*
* Bitsquare 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.
*
* Bitsquare 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 Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bitsquare.arbitration;
import com.google.inject.Inject;
import io.bitsquare.common.Timer;
import io.bitsquare.common.UserThread;
import io.bitsquare.common.crypto.KeyRing;
import io.bitsquare.common.handlers.ErrorMessageHandler;
import io.bitsquare.common.handlers.ResultHandler;
import io.bitsquare.p2p.BootstrapListener;
import io.bitsquare.p2p.NodeAddress;
import io.bitsquare.p2p.P2PService;
import io.bitsquare.p2p.storage.HashMapChangedListener;
import io.bitsquare.p2p.storage.storageentry.ProtectedStorageEntry;
import io.bitsquare.user.Preferences;
import io.bitsquare.user.User;
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.math.BigInteger;
import java.security.PublicKey;
import java.security.SignatureException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.bitcoinj.core.Utils.HEX;
public class ArbitratorManager {
private static final Logger log = LoggerFactory.getLogger(ArbitratorManager.class);
///////////////////////////////////////////////////////////////////////////////////////////
// Static
///////////////////////////////////////////////////////////////////////////////////////////
private static final long REPUBLISH_MILLIS = Arbitrator.TTL / 2;
private static final long RETRY_REPUBLISH_SEC = 5;
private static final long REPEATED_REPUBLISH_AT_STARTUP_SEC = 60;
// Keys for invited arbitrators in bootstrapping phase (before registration is open to anyone and security payment is implemented)
// For developers we add here 2 test keys so one can setup an arbitrator by adding that test pubKey
// to the publicKeys list and use the test PrivKey for arbitrator registration.
// PrivKey for dev testing: 6ac43ea1df2a290c1c8391736aa42e4339c5cb4f110ff0257a13b63211977b7a
// Matching pubKey for dev testing: 027a381b5333a56e1cc3d90d3a7d07f26509adf7029ed06fc997c656621f8da1ee
private static final List<String> publicKeys = new ArrayList<>(Arrays.asList(
"03697a499d24f497b3c46bf716318231e46c4e6a685a4e122d8e2a2b229fa1f4b8",
"0365c6af94681dbee69de1851f98d4684063bf5c2d64b1c73ed5d90434f375a054",
"031c502a60f9dbdb5ae5e438a79819e4e1f417211dd537ac12c9bc23246534c4bd",
"02c1e5a242387b6d5319ce27246cea6edaaf51c3550591b528d2578a4753c56c2c",
"025c319faf7067d9299590dd6c97fe7e56cd4dac61205ccee1cd1fc390142390a2",
"038f6e24c2bfe5d51d0a290f20a9a657c270b94ef2b9c12cd15ca3725fa798fc55",
"0255256ff7fb615278c4544a9bbd3f5298b903b8a011cd7889be19b6b1c45cbefe",
"024a3a37289f08c910fbd925ebc72b946f33feaeff451a4738ee82037b4cda2e95",
"02a88b75e9f0f8afba1467ab26799dcc38fd7a6468fb2795444b425eb43e2c10bd",
"02349a51512c1c04c67118386f4d27d768c5195a83247c150a4b722d161722ba81",
"03f718a2e0dc672c7cdec0113e72c3322efc70412bb95870750d25c32cd98de17d",
"028ff47ee2c56e66313928975c58fa4f1b19a0f81f3a96c4e9c9c3c6768075509e",
"02b517c0cbc3a49548f448ddf004ed695c5a1c52ec110be1bfd65fa0ca0761c94b",
"03df837a3a0f3d858e82f3356b71d1285327f101f7c10b404abed2abc1c94e7169",
"0203a90fb2ab698e524a5286f317a183a84327b8f8c3f7fa4a98fec9e1cefd6b72",
"023c99cc073b851c892d8c43329ca3beb5d2213ee87111af49884e3ce66cbd5ba5"
));
///////////////////////////////////////////////////////////////////////////////////////////
// Instance fields
///////////////////////////////////////////////////////////////////////////////////////////
private final KeyRing keyRing;
private final ArbitratorService arbitratorService;
private final User user;
private Preferences preferences;
private final ObservableMap<NodeAddress, Arbitrator> arbitratorsObservableMap = FXCollections.observableHashMap();
private final List<Arbitrator> persistedAcceptedArbitrators;
private Timer republishArbitratorTimer, retryRepublishArbitratorTimer;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public ArbitratorManager(KeyRing keyRing, ArbitratorService arbitratorService, User user, Preferences preferences) {
this.keyRing = keyRing;
this.arbitratorService = arbitratorService;
this.user = user;
this.preferences = preferences;
persistedAcceptedArbitrators = new ArrayList<>(user.getAcceptedArbitrators());
user.clearAcceptedArbitrators();
arbitratorService.addHashSetChangedListener(new HashMapChangedListener() {
@Override
public void onAdded(ProtectedStorageEntry data) {
if (data.getStoragePayload() instanceof Arbitrator)
updateArbitratorMap();
}
@Override
public void onRemoved(ProtectedStorageEntry data) {
if (data.getStoragePayload() instanceof Arbitrator)
updateArbitratorMap();
}
});
}
public void shutDown() {
stopRepublishArbitratorTimer();
stopRetryRepublishArbitratorTimer();
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void onAllServicesInitialized() {
if (user.getRegisteredArbitrator() != null) {
P2PService p2PService = arbitratorService.getP2PService();
if (p2PService.isBootstrapped())
isBootstrapped();
else
p2PService.addP2PServiceListener(new BootstrapListener() {
@Override
public void onBootstrapComplete() {
isBootstrapped();
}
});
}
updateArbitratorMap();
}
private void isBootstrapped() {
if (republishArbitratorTimer == null) {
republishArbitratorTimer = UserThread.runPeriodically(this::republishArbitrator, REPUBLISH_MILLIS, TimeUnit.MILLISECONDS);
UserThread.runAfter(this::republishArbitrator, REPEATED_REPUBLISH_AT_STARTUP_SEC);
republishArbitrator();
}
}
public void updateArbitratorMap() {
Map<NodeAddress, Arbitrator> map = arbitratorService.getArbitrators();
arbitratorsObservableMap.clear();
Map<NodeAddress, Arbitrator> filtered = map.values().stream()
.filter(e -> isPublicKeyInList(Utils.HEX.encode(e.getRegistrationPubKey()))
&& verifySignature(e.getPubKeyRing().getSignaturePubKey(), e.getRegistrationPubKey(), e.getRegistrationSignature()))
.collect(Collectors.toMap(Arbitrator::getArbitratorNodeAddress, Function.identity()));
arbitratorsObservableMap.putAll(filtered);
arbitratorsObservableMap.values().stream()
.filter(arbitrator -> persistedAcceptedArbitrators.contains(arbitrator))
.forEach(user::addAcceptedArbitrator);
if (preferences.getAutoSelectArbitrators()) {
arbitratorsObservableMap.values().stream()
.filter(user::hasMatchingLanguage)
.forEach(user::addAcceptedArbitrator);
} else {
// if we don't have any arbitrator we set all matching
// we use a delay as we might get our matching arbitrator a bit delayed (first we get one we did not selected
// then we get our selected one - we don't want to activate the first in that case)
UserThread.runAfter(() -> {
if (user.getAcceptedArbitrators().isEmpty()) {
arbitratorsObservableMap.values().stream()
.filter(user::hasMatchingLanguage)
.forEach(user::addAcceptedArbitrator);
}
}, 100, TimeUnit.MILLISECONDS);
}
}
public void addArbitrator(Arbitrator arbitrator, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
user.setRegisteredArbitrator(arbitrator);
arbitratorsObservableMap.put(arbitrator.getArbitratorNodeAddress(), arbitrator);
arbitratorService.addArbitrator(arbitrator,
() -> {
log.debug("Arbitrator successfully saved in P2P network");
resultHandler.handleResult();
if (arbitratorsObservableMap.size() > 0)
UserThread.runAfter(this::updateArbitratorMap, 100, TimeUnit.MILLISECONDS);
},
errorMessageHandler::handleErrorMessage);
}
public void removeArbitrator(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
Arbitrator registeredArbitrator = user.getRegisteredArbitrator();
if (registeredArbitrator != null) {
user.setRegisteredArbitrator(null);
arbitratorsObservableMap.remove(registeredArbitrator.getArbitratorNodeAddress());
arbitratorService.removeArbitrator(registeredArbitrator,
() -> {
log.debug("Arbitrator successfully removed from P2P network");
resultHandler.handleResult();
},
errorMessageHandler::handleErrorMessage);
}
}
public ObservableMap<NodeAddress, Arbitrator> getArbitratorsObservableMap() {
return arbitratorsObservableMap;
}
// A private key is handed over to selected arbitrators for registration.
// An invited arbitrator will sign at registration his storageSignaturePubKey with that private key and attach the signature and pubKey to his data.
// Other users will check the signature with the list of public keys hardcoded in the app.
public String signStorageSignaturePubKey(ECKey key) {
String keyToSignAsHex = Utils.HEX.encode(keyRing.getPubKeyRing().getSignaturePubKey().getEncoded());
return key.signMessage(keyToSignAsHex);
}
@Nullable
public ECKey getRegistrationKey(String privKeyBigIntString) {
try {
return ECKey.fromPrivate(new BigInteger(1, HEX.decode(privKeyBigIntString)));
} catch (Throwable t) {
return null;
}
}
public boolean isPublicKeyInList(String pubKeyAsHex) {
return publicKeys.contains(pubKeyAsHex);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void republishArbitrator() {
Arbitrator registeredArbitrator = user.getRegisteredArbitrator();
if (registeredArbitrator != null) {
addArbitrator(registeredArbitrator,
this::updateArbitratorMap,
errorMessage -> {
if (retryRepublishArbitratorTimer == null)
retryRepublishArbitratorTimer = UserThread.runPeriodically(() -> {
stopRetryRepublishArbitratorTimer();
republishArbitrator();
}, RETRY_REPUBLISH_SEC);
}
);
}
}
private boolean verifySignature(PublicKey storageSignaturePubKey, byte[] registrationPubKey, String signature) {
String keyToSignAsHex = Utils.HEX.encode(storageSignaturePubKey.getEncoded());
try {
ECKey key = ECKey.fromPublicOnly(registrationPubKey);
key.verifyMessage(keyToSignAsHex, signature);
return true;
} catch (SignatureException e) {
log.warn("verifySignature failed");
return false;
}
}
private void stopRetryRepublishArbitratorTimer() {
if (retryRepublishArbitratorTimer != null) {
retryRepublishArbitratorTimer.stop();
retryRepublishArbitratorTimer = null;
}
}
private void stopRepublishArbitratorTimer() {
if (republishArbitratorTimer != null) {
republishArbitratorTimer.stop();
republishArbitratorTimer = null;
}
}
}