diff --git a/common/src/main/java/bisq/common/util/Utilities.java b/common/src/main/java/bisq/common/util/Utilities.java index 7947157bc7..6cdd3bf342 100644 --- a/common/src/main/java/bisq/common/util/Utilities.java +++ b/common/src/main/java/bisq/common/util/Utilities.java @@ -42,10 +42,6 @@ import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; -import javax.crypto.Cipher; - -import java.security.NoSuchAlgorithmException; - import java.net.URI; import java.net.URISyntaxException; @@ -56,15 +52,19 @@ import java.util.Date; import java.util.GregorianCalendar; import java.util.HashSet; import java.util.Locale; +import java.util.Map; import java.util.Random; import java.util.Set; import java.util.TimeZone; import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @@ -330,6 +330,10 @@ public class Utilities { return new KeyCodeCombination(keyCode, KeyCombination.ALT_DOWN).match(keyEvent); } + public static boolean isCtrlShiftPressed(KeyCode keyCode, KeyEvent keyEvent) { + return new KeyCodeCombination(keyCode, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN).match(keyEvent); + } + public static byte[] concatenateByteArrays(byte[] array1, byte[] array2) { return ArrayUtils.addAll(array1, array2); } @@ -452,4 +456,10 @@ public class Utilities { } return result; } + + // Helper to filter unique elements by key + public static Predicate distinctByKey(Function keyExtractor) { + Map map = new ConcurrentHashMap<>(); + return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; + } } diff --git a/core/src/main/java/bisq/core/account/sign/SignedWitness.java b/core/src/main/java/bisq/core/account/sign/SignedWitness.java index 975563623a..94c1cb11a3 100644 --- a/core/src/main/java/bisq/core/account/sign/SignedWitness.java +++ b/core/src/main/java/bisq/core/account/sign/SignedWitness.java @@ -168,7 +168,7 @@ public class SignedWitness implements ProcessOncePersistableNetworkPayload, Pers // Getters /////////////////////////////////////////////////////////////////////////////////////////// - P2PDataStorage.ByteArray getHashAsByteArray() { + public P2PDataStorage.ByteArray getHashAsByteArray() { return new P2PDataStorage.ByteArray(hash); } diff --git a/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java b/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java index d00df0fa2e..e3a22f0898 100644 --- a/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java +++ b/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java @@ -29,6 +29,7 @@ import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreService; import bisq.common.UserThread; import bisq.common.crypto.CryptoException; +import bisq.common.crypto.Hash; import bisq.common.crypto.KeyRing; import bisq.common.crypto.Sig; import bisq.common.util.Utilities; @@ -52,25 +53,28 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Stack; import java.util.stream.Collectors; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; @Slf4j public class SignedWitnessService { public static final long SIGNER_AGE_DAYS = 30; private static final long SIGNER_AGE = SIGNER_AGE_DAYS * ChronoUnit.DAYS.getDuration().toMillis(); - static final Coin MINIMUM_TRADE_AMOUNT_FOR_SIGNING = Coin.parseCoin("0.0025"); + public static final Coin MINIMUM_TRADE_AMOUNT_FOR_SIGNING = Coin.parseCoin("0.0025"); private final KeyRing keyRing; private final P2PService p2PService; private final ArbitratorManager arbitratorManager; private final User user; + @Getter private final Map signedWitnessMap = new HashMap<>(); private final FilterManager filterManager; @@ -123,6 +127,8 @@ public class SignedWitnessService { } }); } + // TODO: Enable cleaning of signed witness list when necessary + // cleanSignedWitnesses(); } private void onBootstrapComplete() { @@ -176,7 +182,14 @@ public class SignedWitnessService { .anyMatch(ownerPubKey -> filterManager.isSignerPubKeyBanned(Utils.HEX.encode(ownerPubKey))); } - public String ownerPubKey(AccountAgeWitness accountAgeWitness) { + private byte[] ownerPubKey(AccountAgeWitness accountAgeWitness) { + return getSignedWitnessSet(accountAgeWitness).stream() + .map(SignedWitness::getWitnessOwnerPubKey) + .findFirst() + .orElse(null); + } + + public String ownerPubKeyAsString(AccountAgeWitness accountAgeWitness) { return getSignedWitnessSet(accountAgeWitness).stream() .map(signedWitness -> Utils.HEX.encode(signedWitness.getWitnessOwnerPubKey())) .findFirst() @@ -195,9 +208,42 @@ public class SignedWitnessService { AccountAgeWitness accountAgeWitness, ECKey key, PublicKey peersPubKey) { + signAccountAgeWitness(tradeAmount, accountAgeWitness, key, peersPubKey.getEncoded(), new Date().getTime()); + } + + // Arbitrators sign with EC key + public String signAccountAgeWitness(AccountAgeWitness accountAgeWitness, + ECKey key, + byte[] peersPubKey, + long time) { + var witnessPubKey = peersPubKey == null ? ownerPubKey(accountAgeWitness) : peersPubKey; + return signAccountAgeWitness(MINIMUM_TRADE_AMOUNT_FOR_SIGNING, accountAgeWitness, key, witnessPubKey, time); + } + + // Arbitrators sign with EC key + public String signTraderPubKey(ECKey key, + byte[] peersPubKey, + long childSignTime) { + var time = childSignTime - SIGNER_AGE - 1; + var dummyAccountAgeWitness = new AccountAgeWitness(Hash.getRipemd160hash(peersPubKey), time); + return signAccountAgeWitness(MINIMUM_TRADE_AMOUNT_FOR_SIGNING, dummyAccountAgeWitness, key, peersPubKey, time); + } + + // Arbitrators sign with EC key + private String signAccountAgeWitness(Coin tradeAmount, + AccountAgeWitness accountAgeWitness, + ECKey key, + byte[] peersPubKey, + long time) { if (isSignedAccountAgeWitness(accountAgeWitness)) { - log.warn("Arbitrator trying to sign already signed accountagewitness {}", accountAgeWitness.toString()); - return; + var err = "Arbitrator trying to sign already signed accountagewitness " + accountAgeWitness.toString(); + log.warn(err); + return err; + } + if (peersPubKey == null) { + var err = "Trying to sign accountAgeWitness " + accountAgeWitness.toString() + "\nwith owner pubkey=null"; + log.warn(err); + return err; } String accountAgeWitnessHashAsHex = Utilities.encodeToHex(accountAgeWitness.getHash()); @@ -206,11 +252,12 @@ public class SignedWitnessService { accountAgeWitness.getHash(), signatureBase64.getBytes(Charsets.UTF_8), key.getPubKey(), - peersPubKey.getEncoded(), - new Date().getTime(), + peersPubKey, + time, tradeAmount.value); publishSignedWitness(signedWitness); log.info("Arbitrator signed witness {}", signedWitness.toString()); + return ""; } public void selfSignAccountAgeWitness(AccountAgeWitness accountAgeWitness) throws CryptoException { @@ -306,6 +353,24 @@ public class SignedWitnessService { .collect(Collectors.toSet()); } + public Set getRootSignedWitnessSet(boolean includeSignedByArbitrator) { + return signedWitnessMap.values().stream() + .filter(witness -> getSignedWitnessSetByOwnerPubKey(witness.getSignerPubKey(), new Stack<>()).isEmpty()) + .filter(witness -> includeSignedByArbitrator || + witness.getVerificationMethod() != SignedWitness.VerificationMethod.ARBITRATOR) + .collect(Collectors.toSet()); + } + + // Find first (in time) SignedWitness per missing signer + public Set getUnsignedSignerPubKeys() { + var oldestUnsignedSigners = new HashMap(); + getRootSignedWitnessSet(false).forEach(signedWitness -> + oldestUnsignedSigners.compute(new P2PDataStorage.ByteArray(signedWitness.getSignerPubKey()), + (key, oldValue) -> oldValue == null ? signedWitness : + oldValue.getDate() > signedWitness.getDate() ? signedWitness : oldValue)); + return new HashSet<>(oldestUnsignedSigners.values()); + } + // We go one level up by using the signer Key to lookup for SignedWitness objects which contain the signerKey as // witnessOwnerPubKey private Set getSignedWitnessSetByOwnerPubKey(byte[] ownerPubKey, @@ -405,7 +470,6 @@ public class SignedWitnessService { @VisibleForTesting void addToMap(SignedWitness signedWitness) { - // TODO: Perhaps filter out all but one signedwitness per accountagewitness signedWitnessMap.putIfAbsent(signedWitness.getHashAsByteArray(), signedWitness); } @@ -420,4 +484,19 @@ public class SignedWitnessService { private void doRepublishAllSignedWitnesses() { signedWitnessMap.forEach((e, signedWitness) -> p2PService.addPersistableNetworkPayload(signedWitness, true)); } + + // Remove SignedWitnesses that are signed by TRADE that also have an ARBITRATOR signature + // for the same ownerPubKey and AccountAgeWitnessHash +// private void cleanSignedWitnesses() { +// var orphans = getRootSignedWitnessSet(false); +// var signedWitnessesCopy = new HashSet<>(signedWitnessMap.values()); +// signedWitnessesCopy.forEach(sw -> orphans.forEach(orphan -> { +// if (sw.getVerificationMethod() == SignedWitness.VerificationMethod.ARBITRATOR && +// Arrays.equals(sw.getWitnessOwnerPubKey(), orphan.getWitnessOwnerPubKey()) && +// Arrays.equals(sw.getAccountAgeWitnessHash(), orphan.getAccountAgeWitnessHash())) { +// signedWitnessMap.remove(orphan.getHashAsByteArray()); +// log.info("Remove duplicate SignedWitness: {}", orphan.toString()); +// } +// })); +// } } diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java index 118e4b3d4f..f43a1027e6 100644 --- a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java @@ -17,6 +17,7 @@ package bisq.core.account.witness; +import bisq.core.account.sign.SignedWitness; import bisq.core.account.sign.SignedWitnessService; import bisq.core.filter.FilterManager; import bisq.core.filter.PaymentAccountFilter; @@ -33,7 +34,6 @@ import bisq.core.payment.payload.PaymentMethod; import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.DisputeResult; import bisq.core.support.dispute.arbitration.TraderDataItem; -import bisq.core.trade.Contract; import bisq.core.trade.Trade; import bisq.core.trade.protocol.TradingPeer; import bisq.core.user.User; @@ -72,14 +72,14 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Random; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import javax.annotation.Nullable; - import static com.google.common.base.Preconditions.checkNotNull; @Slf4j @@ -130,7 +130,10 @@ public class AccountAgeWitnessService { private final SignedWitnessService signedWitnessService; private final ChargeBackRisk chargeBackRisk; private final FilterManager filterManager; + @Getter + private final AccountAgeWitnessUtils accountAgeWitnessUtils; + @Getter private final Map accountAgeWitnessMap = new HashMap<>(); @@ -155,6 +158,11 @@ public class AccountAgeWitnessService { this.chargeBackRisk = chargeBackRisk; this.filterManager = filterManager; + accountAgeWitnessUtils = new AccountAgeWitnessUtils( + this, + signedWitnessService, + keyRing); + // We need to add that early (before onAllServicesInitialized) as it will be used at startup. appendOnlyDataStoreService.addService(accountAgeWitnessStorageService); } @@ -225,11 +233,11 @@ public class AccountAgeWitnessService { public byte[] getPeerAccountAgeWitnessHash(Trade trade) { return findTradePeerWitness(trade) - .map(accountAgeWitness -> accountAgeWitness.getHash()) + .map(AccountAgeWitness::getHash) .orElse(null); } - private byte[] getAccountInputDataWithSalt(PaymentAccountPayload paymentAccountPayload) { + byte[] getAccountInputDataWithSalt(PaymentAccountPayload paymentAccountPayload) { return Utilities.concatenateByteArrays(paymentAccountPayload.getAgeWitnessInputData(), paymentAccountPayload.getSalt()); } @@ -241,8 +249,8 @@ public class AccountAgeWitnessService { return new AccountAgeWitness(hash, new Date().getTime()); } - private Optional findWitness(PaymentAccountPayload paymentAccountPayload, - PubKeyRing pubKeyRing) { + Optional findWitness(PaymentAccountPayload paymentAccountPayload, + PubKeyRing pubKeyRing) { byte[] accountInputDataWithSalt = getAccountInputDataWithSalt(paymentAccountPayload); byte[] hash = Hash.getSha256Ripemd160hash(Utilities.concatenateByteArrays(accountInputDataWithSalt, pubKeyRing.getSignaturePubKeyBytes())); @@ -634,6 +642,32 @@ public class AccountAgeWitnessService { signedWitnessService.signAccountAgeWitness(tradeAmount, accountAgeWitness, key, peersPubKey); } + public String arbitratorSignOrphanWitness(AccountAgeWitness accountAgeWitness, + ECKey key, + long time) { + // Find AccountAgeWitness as signedwitness + var signedWitness = signedWitnessService.getSignedWitnessMap().values().stream() + .filter(sw -> Arrays.equals(sw.getAccountAgeWitnessHash(), accountAgeWitness.getHash())) + .findAny() + .orElse(null); + checkNotNull(signedWitness); + return signedWitnessService.signAccountAgeWitness(accountAgeWitness, key, signedWitness.getWitnessOwnerPubKey(), + time); + } + + public String arbitratorSignOrphanPubKey(ECKey key, + byte[] peersPubKey, + long childSignTime) { + return signedWitnessService.signTraderPubKey(key, peersPubKey, childSignTime); + } + + public void arbitratorSignAccountAgeWitness(AccountAgeWitness accountAgeWitness, + ECKey key, + byte[] tradersPubKey, + long time) { + signedWitnessService.signAccountAgeWitness(accountAgeWitness, key, tradersPubKey, time); + } + public void traderSignPeersAccountAgeWitness(Trade trade) { AccountAgeWitness peersWitness = findTradePeerWitness(trade).orElse(null); Coin tradeAmount = trade.getTradeAmount(); @@ -758,7 +792,7 @@ public class AccountAgeWitnessService { public SignState getSignState(AccountAgeWitness accountAgeWitness) { // Add hash to sign state info when running in debug mode String hash = log.isDebugEnabled() ? Utilities.bytesAsHexString(accountAgeWitness.getHash()) + "\n" + - signedWitnessService.ownerPubKey(accountAgeWitness) : ""; + signedWitnessService.ownerPubKeyAsString(accountAgeWitness) : ""; if (signedWitnessService.isFilteredWitness(accountAgeWitness)) { return SignState.BANNED.addHash(hash); } @@ -779,6 +813,13 @@ public class AccountAgeWitnessService { } } + public Set getOrphanSignedWitnesses() { + return signedWitnessService.getRootSignedWitnessSet(false).stream() + .map(signedWitness -> getWitnessByHash(signedWitness.getAccountAgeWitnessHash()).orElse(null)) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + public void signSameNameAccounts() { // Collect accounts that have ownerId to sign unsigned accounts with the same ownderId var signerAccounts = Objects.requireNonNull(user.getPaymentAccounts()).stream() @@ -803,63 +844,7 @@ public class AccountAgeWitnessService { })); } - /////////////////////////////////////////////////////////////////////////////////////////// - // Debug logs - /////////////////////////////////////////////////////////////////////////////////////////// - private String getWitnessDebugLog(PaymentAccountPayload paymentAccountPayload, - PubKeyRing pubKeyRing) { - Optional accountAgeWitness = findWitness(paymentAccountPayload, pubKeyRing); - if (!accountAgeWitness.isPresent()) { - byte[] accountInputDataWithSalt = getAccountInputDataWithSalt(paymentAccountPayload); - byte[] hash = Hash.getSha256Ripemd160hash(Utilities.concatenateByteArrays(accountInputDataWithSalt, - pubKeyRing.getSignaturePubKeyBytes())); - return "No accountAgeWitness found for paymentAccountPayload with hash " + Utilities.bytesAsHexString(hash); - } - - SignState signState = getSignState(accountAgeWitness.get()); - return signState.name() + " " + signState.getPresentation() + - "\n" + accountAgeWitness.toString(); - } - - public void witnessDebugLog(Trade trade, @Nullable AccountAgeWitness myWitness) { - // Log to find why accounts sometimes don't get signed as expected - // TODO: Demote to debug or remove once account signing is working ok - checkNotNull(trade.getContract()); - checkNotNull(trade.getContract().getBuyerPaymentAccountPayload()); - boolean checkingSignTrade = true; - boolean isBuyer = trade.getContract().isMyRoleBuyer(keyRing.getPubKeyRing()); - AccountAgeWitness witness = myWitness; - if (witness == null) { - witness = isBuyer ? - getMyWitness(trade.getContract().getBuyerPaymentAccountPayload()) : - getMyWitness(trade.getContract().getSellerPaymentAccountPayload()); - checkingSignTrade = false; - } - boolean isSignWitnessTrade = accountIsSigner(witness) && - !peerHasSignedWitness(trade) && - tradeAmountIsSufficient(trade.getTradeAmount()); - log.info("AccountSigning: " + - "\ntradeId: {}" + - "\nis buyer: {}" + - "\nbuyer account age witness info: {}" + - "\nseller account age witness info: {}" + - "\nchecking for sign trade: {}" + - "\nis myWitness signer: {}" + - "\npeer has signed witness: {}" + - "\ntrade amount: {}" + - "\ntrade amount is sufficient: {}" + - "\nisSignWitnessTrade: {}", - trade.getId(), - isBuyer, - getWitnessDebugLog(trade.getContract().getBuyerPaymentAccountPayload(), - trade.getContract().getBuyerPubKeyRing()), - getWitnessDebugLog(trade.getContract().getSellerPaymentAccountPayload(), - trade.getContract().getSellerPubKeyRing()), - checkingSignTrade, // Following cases added to use same logic as in seller signing check - accountIsSigner(witness), - peerHasSignedWitness(trade), - trade.getTradeAmount(), - tradeAmountIsSufficient(trade.getTradeAmount()), - isSignWitnessTrade); + public Set getUnsignedSignerPubKeys() { + return signedWitnessService.getUnsignedSignerPubKeys(); } } diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java new file mode 100644 index 0000000000..56983fea86 --- /dev/null +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessUtils.java @@ -0,0 +1,171 @@ +/* + * 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.core.account.witness; + +import bisq.core.account.sign.SignedWitness; +import bisq.core.account.sign.SignedWitnessService; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.trade.Trade; + +import bisq.network.p2p.storage.P2PDataStorage; + +import bisq.common.crypto.Hash; +import bisq.common.crypto.KeyRing; +import bisq.common.crypto.PubKeyRing; +import bisq.common.util.Utilities; + +import java.util.Arrays; +import java.util.Optional; +import java.util.Stack; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class AccountAgeWitnessUtils { + private final AccountAgeWitnessService accountAgeWitnessService; + private final SignedWitnessService signedWitnessService; + private final KeyRing keyRing; + + AccountAgeWitnessUtils(AccountAgeWitnessService accountAgeWitnessService, + SignedWitnessService signedWitnessService, + KeyRing keyRing) { + this.accountAgeWitnessService = accountAgeWitnessService; + this.signedWitnessService = signedWitnessService; + this.keyRing = keyRing; + } + + // Log tree of signed witnesses + public void logSignedWitnesses() { + var orphanSigners = signedWitnessService.getRootSignedWitnessSet(true); + log.info("Orphaned signed account age witnesses:"); + orphanSigners.forEach(w -> { + log.info("{}: Signer PKH: {} Owner PKH: {} time: {}", w.getVerificationMethod().toString(), + Utilities.bytesAsHexString(Hash.getRipemd160hash(w.getSignerPubKey())).substring(0, 7), + Utilities.bytesAsHexString(Hash.getRipemd160hash(w.getWitnessOwnerPubKey())).substring(0, 7), + w.getDate()); + logChild(w, " ", new Stack<>()); + }); + } + + private void logChild(SignedWitness sigWit, String initString, Stack excluded) { + var allSig = signedWitnessService.getSignedWitnessMap(); + log.info("{}AEW: {} PKH: {} time: {}", initString, + Utilities.bytesAsHexString(sigWit.getAccountAgeWitnessHash()).substring(0, 7), + Utilities.bytesAsHexString(Hash.getRipemd160hash(sigWit.getWitnessOwnerPubKey())).substring(0, 7), + sigWit.getDate()); + allSig.values().forEach(w -> { + if (!excluded.contains(new P2PDataStorage.ByteArray(w.getWitnessOwnerPubKey())) && + Arrays.equals(w.getSignerPubKey(), sigWit.getWitnessOwnerPubKey())) { + excluded.push(new P2PDataStorage.ByteArray(w.getWitnessOwnerPubKey())); + logChild(w, initString + " ", excluded); + excluded.pop(); + } + }); + } + + // Log signers per + public void logSigners() { + log.info("Signers per AEW"); + var allSig = signedWitnessService.getSignedWitnessMap(); + allSig.values().forEach(w -> { + log.info("AEW {}", Utilities.bytesAsHexString(w.getAccountAgeWitnessHash())); + allSig.values().forEach(ww -> { + if (Arrays.equals(w.getSignerPubKey(), ww.getWitnessOwnerPubKey())) { + log.info(" {}", Utilities.bytesAsHexString(ww.getAccountAgeWitnessHash())); + } + }); + } + ); + } + + public void logUnsignedSignerPubKeys() { + log.info("Unsigned signer pubkeys"); + signedWitnessService.getUnsignedSignerPubKeys().forEach(signedWitness -> + log.info("PK hash {} date {}", + Utilities.bytesAsHexString(Hash.getRipemd160hash(signedWitness.getSignerPubKey())), + signedWitness.getDate())); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Debug logs + /////////////////////////////////////////////////////////////////////////////////////////// + + private String getWitnessDebugLog(PaymentAccountPayload paymentAccountPayload, + PubKeyRing pubKeyRing) { + Optional accountAgeWitness = + accountAgeWitnessService.findWitness(paymentAccountPayload, pubKeyRing); + if (!accountAgeWitness.isPresent()) { + byte[] accountInputDataWithSalt = + accountAgeWitnessService.getAccountInputDataWithSalt(paymentAccountPayload); + byte[] hash = Hash.getSha256Ripemd160hash(Utilities.concatenateByteArrays(accountInputDataWithSalt, + pubKeyRing.getSignaturePubKeyBytes())); + return "No accountAgeWitness found for paymentAccountPayload with hash " + Utilities.bytesAsHexString(hash); + } + + AccountAgeWitnessService.SignState signState = + accountAgeWitnessService.getSignState(accountAgeWitness.get()); + return signState.name() + " " + signState.getPresentation() + + "\n" + accountAgeWitness.toString(); + } + + public void witnessDebugLog(Trade trade, @Nullable AccountAgeWitness myWitness) { + // Log to find why accounts sometimes don't get signed as expected + // TODO: Demote to debug or remove once account signing is working ok + checkNotNull(trade.getContract()); + checkNotNull(trade.getContract().getBuyerPaymentAccountPayload()); + boolean checkingSignTrade = true; + boolean isBuyer = trade.getContract().isMyRoleBuyer(keyRing.getPubKeyRing()); + AccountAgeWitness witness = myWitness; + if (witness == null) { + witness = isBuyer ? + accountAgeWitnessService.getMyWitness(trade.getContract().getBuyerPaymentAccountPayload()) : + accountAgeWitnessService.getMyWitness(trade.getContract().getSellerPaymentAccountPayload()); + checkingSignTrade = false; + } + boolean isSignWitnessTrade = accountAgeWitnessService.accountIsSigner(witness) && + !accountAgeWitnessService.peerHasSignedWitness(trade) && + accountAgeWitnessService.tradeAmountIsSufficient(trade.getTradeAmount()); + log.info("AccountSigning: " + + "\ntradeId: {}" + + "\nis buyer: {}" + + "\nbuyer account age witness info: {}" + + "\nseller account age witness info: {}" + + "\nchecking for sign trade: {}" + + "\nis myWitness signer: {}" + + "\npeer has signed witness: {}" + + "\ntrade amount: {}" + + "\ntrade amount is sufficient: {}" + + "\nisSignWitnessTrade: {}", + trade.getId(), + isBuyer, + getWitnessDebugLog(trade.getContract().getBuyerPaymentAccountPayload(), + trade.getContract().getBuyerPubKeyRing()), + getWitnessDebugLog(trade.getContract().getSellerPaymentAccountPayload(), + trade.getContract().getSellerPubKeyRing()), + checkingSignTrade, // Following cases added to use same logic as in seller signing check + accountAgeWitnessService.accountIsSigner(witness), + accountAgeWitnessService.peerHasSignedWitness(trade), + trade.getTradeAmount(), + accountAgeWitnessService.tradeAmountIsSufficient(trade.getTradeAmount()), + isSignWitnessTrade); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java b/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java index 6f575be684..27317d7569 100644 --- a/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java +++ b/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java @@ -358,6 +358,6 @@ public class ProcessModel implements Model, PersistablePayload { } void logTrade(Trade trade) { - accountAgeWitnessService.witnessDebugLog(trade, null); + accountAgeWitnessService.getAccountAgeWitnessUtils().witnessDebugLog(trade, null); } } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 78a68b1ffb..9e82bc8624 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2730,6 +2730,23 @@ popup.accountSigning.peerLimitLifted=The initial limit for one of your accounts popup.accountSigning.peerSigner=One of your accounts is mature enough to sign other payment accounts \ and the initial limit for one of your accounts has been lifted.\n\n{0} +popup.accountSigning.singleAccountSelect.headline=Select account age witness +popup.accountSigning.singleAccountSelect.description=Search for account age witness. +popup.accountSigning.singleAccountSelect.datePicker=Select point of time for signing +popup.accountSigning.confirmSingleAccount.headline=Confirm selected account age witness +popup.accountSigning.confirmSingleAccount.selectedHash=Selected witness hash +popup.accountSigning.confirmSingleAccount.button=Sign account age witness +popup.accountSigning.successSingleAccount.description=Witness {0} was signed +popup.accountSigning.successSingleAccount.success.headline=Success +popup.accountSigning.successSingleAccount.signError=Failed to sign witness, {0} + +popup.accountSigning.unsignedPubKeys.headline=Unsigned Pubkeys +popup.accountSigning.unsignedPubKeys.sign=Sign Pubkeys +popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed +popup.accountSigning.unsignedPubKeys.result.headline=Signing completed +popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys +popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign + #################################################################### # Notifications #################################################################### diff --git a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java index e0baf48664..36d52df74d 100644 --- a/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java +++ b/core/src/test/java/bisq/core/account/witness/AccountAgeWitnessServiceTest.java @@ -37,6 +37,7 @@ import bisq.network.p2p.P2PService; import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreService; import bisq.common.crypto.CryptoException; +import bisq.common.crypto.Hash; import bisq.common.crypto.KeyRing; import bisq.common.crypto.KeyStorage; import bisq.common.crypto.PubKeyRing; @@ -53,9 +54,11 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; +import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.Before; @@ -81,12 +84,25 @@ public class AccountAgeWitnessServiceTest { private AccountAgeWitnessService service; private ChargeBackRisk chargeBackRisk; private FilterManager filterManager; + private File dir1; + private File dir2; + private File dir3; @Before - public void setup() { + public void setup() throws IOException { + KeyRing keyRing = mock(KeyRing.class); + setupService(keyRing); + keypair = Sig.generateKeyPair(); + publicKey = keypair.getPublic(); + // Setup temp storage dir + dir1 = makeDir("temp_tests1"); + dir2 = makeDir("temp_tests1"); + dir3 = makeDir("temp_tests1"); + } + + private void setupService(KeyRing keyRing) { chargeBackRisk = mock(ChargeBackRisk.class); AppendOnlyDataStoreService dataStoreService = mock(AppendOnlyDataStoreService.class); - KeyRing keyRing = mock(KeyRing.class); P2PService p2pService = mock(P2PService.class); ArbitratorManager arbitratorManager = mock(ArbitratorManager.class); when(arbitratorManager.isPublicKeyInList(any())).thenReturn(true); @@ -94,8 +110,13 @@ public class AccountAgeWitnessServiceTest { filterManager = mock(FilterManager.class); signedWitnessService = new SignedWitnessService(keyRing, p2pService, arbitratorManager, null, appendOnlyDataStoreService, null, filterManager); service = new AccountAgeWitnessService(null, null, null, signedWitnessService, chargeBackRisk, null, dataStoreService, filterManager); - keypair = Sig.generateKeyPair(); - publicKey = keypair.getPublic(); + } + + private File makeDir(String name) throws IOException { + var dir = File.createTempFile(name, ""); + dir.delete(); + dir.mkdir(); + return dir; } @After @@ -105,21 +126,21 @@ public class AccountAgeWitnessServiceTest { @Ignore @Test - public void testIsTradeDateAfterReleaseDate() throws CryptoException { - Date ageWitnessReleaseDate = new GregorianCalendar(2017, 9, 23).getTime(); - Date tradeDate = new GregorianCalendar(2017, 10, 1).getTime(); + public void testIsTradeDateAfterReleaseDate() { + Date ageWitnessReleaseDate = new GregorianCalendar(2017, Calendar.OCTOBER, 23).getTime(); + Date tradeDate = new GregorianCalendar(2017, Calendar.NOVEMBER, 1).getTime(); assertTrue(service.isDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate, errorMessage -> { })); - tradeDate = new GregorianCalendar(2017, 9, 23).getTime(); + tradeDate = new GregorianCalendar(2017, Calendar.OCTOBER, 23).getTime(); assertTrue(service.isDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate, errorMessage -> { })); - tradeDate = new GregorianCalendar(2017, 9, 22, 0, 0, 1).getTime(); + tradeDate = new GregorianCalendar(2017, Calendar.OCTOBER, 22, 0, 0, 1).getTime(); assertTrue(service.isDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate, errorMessage -> { })); - tradeDate = new GregorianCalendar(2017, 9, 22).getTime(); + tradeDate = new GregorianCalendar(2017, Calendar.OCTOBER, 22).getTime(); assertFalse(service.isDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate, errorMessage -> { })); - tradeDate = new GregorianCalendar(2017, 9, 21).getTime(); + tradeDate = new GregorianCalendar(2017, Calendar.OCTOBER, 21).getTime(); assertFalse(service.isDateAfterReleaseDate(tradeDate.getTime(), ageWitnessReleaseDate, errorMessage -> { })); } @@ -140,15 +161,7 @@ public class AccountAgeWitnessServiceTest { } @Test - public void testArbitratorSignWitness() throws IOException { - // Setup temp storage dir - File dir1 = File.createTempFile("temp_tests1", ""); - dir1.delete(); - dir1.mkdir(); - File dir2 = File.createTempFile("temp_tests1", ""); - dir2.delete(); - dir2.mkdir(); - + public void testArbitratorSignWitness() { KeyRing buyerKeyRing = new KeyRing(new KeyStorage(dir1)); KeyRing sellerKeyRing = new KeyRing(new KeyStorage(dir2)); @@ -238,13 +251,110 @@ public class AccountAgeWitnessServiceTest { buyerPubKeyRing.getSignaturePubKeyBytes()).stream() .findFirst() .orElse(null); + assert foundBuyerSignedWitness != null; assertEquals(Utilities.bytesAsHexString(foundBuyerSignedWitness.getAccountAgeWitnessHash()), Utilities.bytesAsHexString(buyerAccountAgeWitness.getHash())); SignedWitness foundSellerSignedWitness = signedWitnessService.getSignedWitnessSetByOwnerPubKey( sellerPubKeyRing.getSignaturePubKeyBytes()).stream() .findFirst() .orElse(null); + assert foundSellerSignedWitness != null; assertEquals(Utilities.bytesAsHexString(foundSellerSignedWitness.getAccountAgeWitnessHash()), Utilities.bytesAsHexString(sellerAccountAgeWitness.getHash())); } + + // Create a tree of signed witnesses Arb -(SWA)-> aew1 -(SW1)-> aew2 -(SW2)-> aew3 + // Delete SWA signature, none of the account age witnesses are considered signed + // Sign a dummy AccountAgeWitness using the signerPubkey from SW1; aew2 and aew3 are not considered signed. The + // lost SignedWitness isn't possible to recover so aew1 is still not signed, but it's pubkey is a signer. + @Test + public void testArbitratorSignDummyWitness() throws CryptoException { + ECKey arbitratorKey = new ECKey(); + // Init 2 user accounts + var user1KeyRing = new KeyRing(new KeyStorage(dir1)); + var user2KeyRing = new KeyRing(new KeyStorage(dir2)); + var user3KeyRing = new KeyRing(new KeyStorage(dir3)); + var pubKeyRing1 = user1KeyRing.getPubKeyRing(); + var pubKeyRing2 = user2KeyRing.getPubKeyRing(); + var pubKeyRing3 = user3KeyRing.getPubKeyRing(); + var account1 = new SepaAccountPayload(PaymentMethod.SEPA_ID, "1", CountryUtil.getAllSepaCountries()); + var account2 = new SepaAccountPayload(PaymentMethod.SEPA_ID, "2", CountryUtil.getAllSepaCountries()); + var account3 = new SepaAccountPayload(PaymentMethod.SEPA_ID, "3", CountryUtil.getAllSepaCountries()); + var aew1 = service.getNewWitness(account1, pubKeyRing1); + var aew2 = service.getNewWitness(account2, pubKeyRing2); + var aew3 = service.getNewWitness(account3, pubKeyRing3); + // Backdate witness1 70 days + aew1 = new AccountAgeWitness(aew1.getHash(), new Date().getTime() - TimeUnit.DAYS.toMillis(70)); + aew2 = new AccountAgeWitness(aew2.getHash(), new Date().getTime() - TimeUnit.DAYS.toMillis(35)); + aew3 = new AccountAgeWitness(aew3.getHash(), new Date().getTime() - TimeUnit.DAYS.toMillis(1)); + service.addToMap(aew1); + service.addToMap(aew2); + service.addToMap(aew3); + + // Test as user1. It's still possible to sign as arbitrator since the ECKey is passed as an argument. + setupService(user1KeyRing); + + // Arbitrator signs user1 + service.arbitratorSignAccountAgeWitness(aew1, arbitratorKey, pubKeyRing1.getSignaturePubKeyBytes(), + aew1.getDate()); + // user1 signs user2 + signAccountAgeWitness(aew2, pubKeyRing2.getSignaturePubKey(), aew2.getDate(), user1KeyRing); + // user2 signs user3 + signAccountAgeWitness(aew3, pubKeyRing3.getSignaturePubKey(), aew3.getDate(), user2KeyRing); + signedWitnessService.signAccountAgeWitness(SignedWitnessService.MINIMUM_TRADE_AMOUNT_FOR_SIGNING, aew2, + pubKeyRing2.getSignaturePubKey()); + assertTrue(service.accountIsSigner(aew1)); + assertTrue(service.accountIsSigner(aew2)); + assertFalse(service.accountIsSigner(aew3)); + assertTrue(signedWitnessService.isSignedAccountAgeWitness(aew3)); + + // Remove SignedWitness signed by arbitrator + @SuppressWarnings("OptionalGetWithoutIsPresent") + var signedWitnessArb = signedWitnessService.getSignedWitnessMap().values().stream() + .filter(sw -> sw.getVerificationMethod() == SignedWitness.VerificationMethod.ARBITRATOR) + .findAny() + .get(); + signedWitnessService.getSignedWitnessMap().remove(signedWitnessArb.getHashAsByteArray()); + assertEquals(signedWitnessService.getSignedWitnessMap().size(), 2); + + // Check that no account age witness is a signer + assertFalse(service.accountIsSigner(aew1)); + assertFalse(service.accountIsSigner(aew2)); + assertFalse(service.accountIsSigner(aew3)); + assertFalse(signedWitnessService.isSignedAccountAgeWitness(aew2)); + + // Sign dummy AccountAgeWitness using signer key from SW_1 + assertEquals(signedWitnessService.getRootSignedWitnessSet(false).size(), 1); + + // TODO: move this to accountagewitnessservice + @SuppressWarnings("OptionalGetWithoutIsPresent") + var orphanedSignedWitness = signedWitnessService.getRootSignedWitnessSet(false).stream().findAny().get(); + var dummyAccountAgeWitnessHash = Hash.getRipemd160hash(orphanedSignedWitness.getSignerPubKey()); + var dummyAEW = new AccountAgeWitness(dummyAccountAgeWitnessHash, + orphanedSignedWitness.getDate() - + (TimeUnit.DAYS.toMillis(SignedWitnessService.SIGNER_AGE_DAYS + 1))); + service.arbitratorSignAccountAgeWitness( + dummyAEW, arbitratorKey, orphanedSignedWitness.getSignerPubKey(), dummyAEW.getDate()); + + assertFalse(service.accountIsSigner(aew1)); + assertTrue(service.accountIsSigner(aew2)); + assertFalse(service.accountIsSigner(aew3)); + assertTrue(signedWitnessService.isSignedAccountAgeWitness(aew2)); + } + + private void signAccountAgeWitness(AccountAgeWitness accountAgeWitness, + PublicKey witnessOwnerPubKey, + long time, + KeyRing signerKeyRing) throws CryptoException { + byte[] signature = Sig.sign(signerKeyRing.getSignatureKeyPair().getPrivate(), accountAgeWitness.getHash()); + SignedWitness signedWitness = new SignedWitness(SignedWitness.VerificationMethod.TRADE, + accountAgeWitness.getHash(), + signature, + signerKeyRing.getSignatureKeyPair().getPublic().getEncoded(), + witnessOwnerPubKey.getEncoded(), + time, + SignedWitnessService.MINIMUM_TRADE_AMOUNT_FOR_SIGNING.value); + signedWitnessService.getSignedWitnessMap().putIfAbsent(signedWitness.getHashAsByteArray(), signedWitness); + } + } diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/PaymentAccountsView.java b/desktop/src/main/java/bisq/desktop/main/account/content/PaymentAccountsView.java index e71b7a4e61..deb43641a5 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/PaymentAccountsView.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/PaymentAccountsView.java @@ -16,6 +16,8 @@ import bisq.core.payment.PaymentAccount; import bisq.core.payment.payload.PaymentMethod; import bisq.common.UserThread; +import bisq.common.app.DevEnv; +import bisq.common.util.Utilities; import org.apache.commons.lang3.StringUtils; @@ -26,10 +28,14 @@ import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.image.ImageView; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; import javafx.scene.layout.AnchorPane; import javafx.beans.value.ChangeListener; +import javafx.event.EventHandler; + import javafx.collections.ObservableList; import javafx.util.Callback; @@ -41,8 +47,8 @@ public abstract class PaymentAccountsView paymentAccountsListView; private ChangeListener paymentAccountChangeListener; protected Button addAccountButton, exportButton, importButton; - SignedWitnessService signedWitnessService; protected AccountAgeWitnessService accountAgeWitnessService; + private EventHandler keyEventEventHandler; public PaymentAccountsView(M model, AccountAgeWitnessService accountAgeWitnessService) { super(model); @@ -51,6 +57,16 @@ public abstract class PaymentAccountsView { + if (Utilities.isCtrlShiftPressed(KeyCode.L, event)) { + accountAgeWitnessService.getAccountAgeWitnessUtils().logSignedWitnesses(); + } else if (Utilities.isCtrlShiftPressed(KeyCode.S, event)) { + accountAgeWitnessService.getAccountAgeWitnessUtils().logSigners(); + } else if (Utilities.isCtrlShiftPressed(KeyCode.U, event)) { + accountAgeWitnessService.getAccountAgeWitnessUtils().logUnsignedSignerPubKeys(); + } + }; + buildForm(); paymentAccountChangeListener = (observable, oldValue, newValue) -> { if (newValue != null) @@ -68,6 +84,8 @@ public abstract class PaymentAccountsView addNewAccount()); exportButton.setOnAction(event -> exportAccounts()); importButton.setOnAction(event -> importAccounts()); + if (root.getScene() != null) + root.getScene().addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); } @Override @@ -76,6 +94,8 @@ public abstract class PaymentAccountsView. + */ + +package bisq.desktop.main.overlays.windows; + +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.overlays.Overlay; +import bisq.desktop.main.overlays.popups.Popup; + +import bisq.core.account.witness.AccountAgeWitness; +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; + +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Utils; + +import javax.inject.Inject; + +import com.jfoenix.controls.JFXAutoCompletePopup; + +import javafx.scene.control.DatePicker; +import javafx.scene.control.ListCell; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; + +import javafx.geometry.VPos; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.desktop.util.FormBuilder.*; + +@Slf4j +public class SignSpecificWitnessWindow extends Overlay { + + private InputTextField searchTextField; + private JFXAutoCompletePopup searchAutoComplete; + private AccountAgeWitness selectedWitness; + private DatePicker datePicker; + private InputTextField privateKey; + private final AccountAgeWitnessService accountAgeWitnessService; + private final ArbitratorManager arbitratorManager; + + + @Inject + public SignSpecificWitnessWindow(AccountAgeWitnessService accountAgeWitnessService, + ArbitratorManager arbitratorManager) { + this.accountAgeWitnessService = accountAgeWitnessService; + this.arbitratorManager = arbitratorManager; + } + + @Override + public void show() { + width = 1000; + rowIndex = -1; + createGridPane(); + + gridPane.setPrefHeight(600); + gridPane.getColumnConstraints().get(1).setHgrow(Priority.NEVER); + headLine(Res.get("popup.accountSigning.singleAccountSelect.headline")); + type = Type.Attention; + + addHeadLine(); + addSelectWitnessContent(); + addButtons(); + applyStyles(); + + display(); + } + + private void addSelectWitnessContent() { + searchTextField = addInputTextField(gridPane, ++rowIndex, + Res.get("popup.accountSigning.singleAccountSelect.description")); + + searchAutoComplete = new JFXAutoCompletePopup<>(); + searchAutoComplete.setPrefWidth(400); + searchAutoComplete.getSuggestions().addAll(accountAgeWitnessService.getOrphanSignedWitnesses()); + searchAutoComplete.setSuggestionsCellFactory(param -> new ListCell<>() { + @Override + protected void updateItem(AccountAgeWitness item, boolean empty) { + super.updateItem(item, empty); + if (item != null) { + setText(Utilities.bytesAsHexString(item.getHash())); + } else { + setText(""); + } + } + }); + searchAutoComplete.setSelectionHandler(event -> { + searchTextField.setText(Utilities.bytesAsHexString(event.getObject().getHash())); + selectedWitness = event.getObject(); + if (selectedWitness != null) { + datePicker.setValue(Instant.ofEpochMilli(selectedWitness.getDate()).atZone( + ZoneId.systemDefault()).toLocalDate()); + } + }); + + searchTextField.textProperty().addListener(observable -> { + searchAutoComplete.filter(witness -> Utilities.bytesAsHexString(witness.getHash()).startsWith( + searchTextField.getText().toLowerCase())); + if (searchAutoComplete.getFilteredSuggestions().isEmpty()) { + searchAutoComplete.hide(); + } else { + searchAutoComplete.show(searchTextField); + } + }); + + datePicker = addTopLabelDatePicker(gridPane, ++rowIndex, + Res.get("popup.accountSigning.singleAccountSelect.datePicker"), + 0).second; + datePicker.setOnAction(e -> updateWitnessSelectionState()); + } + + private void addECKeyField() { + privateKey = addInputTextField(gridPane, ++rowIndex, Res.get("popup.accountSigning.signAccounts.ECKey")); + GridPane.setVgrow(privateKey, Priority.ALWAYS); + GridPane.setValignment(privateKey, VPos.TOP); + } + + private void updateWitnessSelectionState() { + actionButton.setDisable(selectedWitness == null || datePicker.getValue() == null); + } + + private void removeContent() { + removeRowsFromGridPane(gridPane, 1, 3); + rowIndex = 1; + } + + private void selectAccountAgeWitness() { + removeContent(); + headLineLabel.setText(Res.get("popup.accountSigning.confirmSingleAccount.headline")); + var selectedWitnessTextField = addTopLabelTextField(gridPane, ++rowIndex, + Res.get("popup.accountSigning.confirmSingleAccount.selectedHash")).second; + selectedWitnessTextField.setText(Utilities.bytesAsHexString(selectedWitness.getHash())); + addECKeyField(); + ((AutoTooltipButton) actionButton).updateText(Res.get("popup.accountSigning.confirmSingleAccount.button")); + actionButton.setOnAction(a -> { + var arbitratorKey = arbitratorManager.getRegistrationKey(privateKey.getText()); + if (arbitratorKey != null) { + var arbitratorPubKeyAsHex = Utils.HEX.encode(arbitratorKey.getPubKey()); + var isKeyValid = arbitratorManager.isPublicKeyInList(arbitratorPubKeyAsHex); + if (isKeyValid) { + var result = accountAgeWitnessService.arbitratorSignOrphanWitness(selectedWitness, + arbitratorKey, + datePicker.getValue().atStartOfDay().toEpochSecond(ZoneOffset.UTC) * 1000); + if (result.isEmpty()) { + addSuccessContent(); + } else { + new Popup().error(Res.get("popup.accountSigning.successSingleAccount.signError", result)) + .onClose(this::hide).show(); + } + } + } else { + new Popup().error(Res.get("popup.accountSigning.signAccounts.ECKey.error")).onClose(this::hide).show(); + } + + }); + } + + + private void addSuccessContent() { + removeContent(); + closeButton.setVisible(false); + closeButton.setManaged(false); + headLineLabel.setText(Res.get("popup.accountSigning.successSingleAccount.success.headline")); + var descriptionLabel = addMultilineLabel(gridPane, ++rowIndex, + Res.get("popup.accountSigning.successSingleAccount.description", + Utilities.bytesAsHexString(selectedWitness.getHash()))); + GridPane.setVgrow(descriptionLabel, Priority.ALWAYS); + GridPane.setValignment(descriptionLabel, VPos.TOP); + ((AutoTooltipButton) actionButton).updateText(Res.get("shared.ok")); + actionButton.setOnAction(a -> hide()); + } + + @Override + protected void addButtons() { + var buttonTuple = add2ButtonsAfterGroup(gridPane, ++rowIndex + 1, + Res.get("popup.accountSigning.singleAccountSelect.headline"), Res.get("shared.cancel")); + + actionButton = buttonTuple.first; + actionButton.setDisable(true); + actionButton.setOnAction(e -> selectAccountAgeWitness()); + + closeButton = (AutoTooltipButton) buttonTuple.second; + closeButton.setOnAction(e -> hide()); + + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/SignUnsignedPubKeysWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/SignUnsignedPubKeysWindow.java new file mode 100644 index 0000000000..97f3d67a24 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/SignUnsignedPubKeysWindow.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.desktop.main.overlays.windows; + +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.overlays.Overlay; +import bisq.desktop.main.overlays.popups.Popup; + +import bisq.core.account.sign.SignedWitness; +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.locale.Res; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; + +import bisq.common.crypto.Hash; +import bisq.common.util.Tuple3; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Utils; + +import javax.inject.Inject; + +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +import javafx.geometry.VPos; + +import javafx.collections.FXCollections; + +import javafx.util.Callback; + +import java.util.ArrayList; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.desktop.util.FormBuilder.add2ButtonsAfterGroup; +import static bisq.desktop.util.FormBuilder.addInputTextField; +import static bisq.desktop.util.FormBuilder.addTopLabelListView; +import static bisq.desktop.util.FormBuilder.removeRowsFromGridPane; + +@Slf4j +public class SignUnsignedPubKeysWindow extends Overlay { + + private ListView unsignedPubKeys = new ListView<>(); + private InputTextField privateKey; + private final AccountAgeWitnessService accountAgeWitnessService; + private final ArbitratorManager arbitratorManager; + private List signedWitnessList = new ArrayList<>(); + private List failed = new ArrayList<>(); + private Callback, ListCell> signedWitnessCellFactory; + + @Inject + public SignUnsignedPubKeysWindow(AccountAgeWitnessService accountAgeWitnessService, + ArbitratorManager arbitratorManager) { + this.accountAgeWitnessService = accountAgeWitnessService; + this.arbitratorManager = arbitratorManager; + + signedWitnessCellFactory = new Callback<>() { + @Override + public ListCell call( + ListView param) { + return new ListCell<>() { + @Override + protected void updateItem(SignedWitness item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + setText(Utilities.bytesAsHexString(Hash.getRipemd160hash(item.getSignerPubKey()))); + } else { + setText(null); + } + } + }; + } + }; + } + + @Override + public void show() { + width = 1000; + rowIndex = -1; + createGridPane(); + + gridPane.setPrefHeight(600); + gridPane.getColumnConstraints().get(1).setHgrow(Priority.NEVER); + headLine(Res.get("popup.accountSigning.singleAccountSelect.headline")); + type = Type.Attention; + + addHeadLine(); + addUnsignedPubKeysContent(); + addECKeyField(); + addButtons(); + applyStyles(); + + display(); + } + + private void addUnsignedPubKeysContent() { + Tuple3, VBox> unsignedPubKeysTuple = addTopLabelListView(gridPane, ++rowIndex, + Res.get("popup.accountSigning.unsignedPubKeys.headline")); + unsignedPubKeys = unsignedPubKeysTuple.second; + unsignedPubKeys.setCellFactory(signedWitnessCellFactory); + unsignedPubKeys.setItems(FXCollections.observableArrayList( + accountAgeWitnessService.getUnsignedSignerPubKeys())); + } + + private void addECKeyField() { + privateKey = addInputTextField(gridPane, ++rowIndex, Res.get("popup.accountSigning.signAccounts.ECKey")); + GridPane.setVgrow(privateKey, Priority.ALWAYS); + GridPane.setValignment(privateKey, VPos.TOP); + } + + private void removeContent() { + removeRowsFromGridPane(gridPane, 1, 3); + rowIndex = 1; + } + + private void signPubKeys() { + removeContent(); + headLineLabel.setText(Res.get("popup.accountSigning.unsignedPubKeys.signed")); + var arbitratorKey = arbitratorManager.getRegistrationKey(privateKey.getText()); + if (arbitratorKey != null) { + var arbitratorPubKeyAsHex = Utils.HEX.encode(arbitratorKey.getPubKey()); + var isKeyValid = arbitratorManager.isPublicKeyInList(arbitratorPubKeyAsHex); + failed.clear(); + if (isKeyValid) { + unsignedPubKeys.getItems().forEach(signedWitness -> { + var result = accountAgeWitnessService.arbitratorSignOrphanPubKey(arbitratorKey, + signedWitness.getSignerPubKey(), signedWitness.getDate()); + if (result.isEmpty()) { + signedWitnessList.add(signedWitness); + } else { + failed.add("Signing pubkey " + Utilities.bytesAsHexString(Hash.getRipemd160hash( + signedWitness.getSignerPubKey())) + " failed with error " + result); + } + }); + showResult(); + } + } else { + new Popup().error(Res.get("popup.accountSigning.signAccounts.ECKey.error")).onClose(this::hide).show(); + } + } + + private void showResult() { + removeContent(); + closeButton.setVisible(false); + closeButton.setManaged(false); + + Tuple3, VBox> signedTuple = addTopLabelListView(gridPane, ++rowIndex, + Res.get("popup.accountSigning.unsignedPubKeys.result.signed")); + ListView signedWitnessListView = signedTuple.second; + signedWitnessListView.setCellFactory(signedWitnessCellFactory); + signedWitnessListView.setItems(FXCollections.observableArrayList(signedWitnessList)); + Tuple3, VBox> failedTuple = addTopLabelListView(gridPane, ++rowIndex, + Res.get("popup.accountSigning.unsignedPubKeys.result.failed")); + ListView failedView = failedTuple.second; + failedView.setItems(FXCollections.observableArrayList(failed)); + + ((AutoTooltipButton) actionButton).updateText(Res.get("shared.ok")); + actionButton.setOnAction(a -> hide()); + } + + @Override + protected void addButtons() { + var buttonTuple = add2ButtonsAfterGroup(gridPane, ++rowIndex + 1, + Res.get("popup.accountSigning.unsignedPubKeys.sign"), Res.get("shared.cancel")); + + actionButton = buttonTuple.first; + actionButton.setDisable(unsignedPubKeys.getItems().size() == 0); + actionButton.setOnAction(e -> signPubKeys()); + + closeButton = (AutoTooltipButton) buttonTuple.second; + closeButton.setOnAction(e -> hide()); + + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java index ca871232c4..8e89268c43 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java @@ -374,7 +374,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel