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 94c1cb11a3a..b7e908699d9 100644 --- a/core/src/main/java/bisq/core/account/sign/SignedWitness.java +++ b/core/src/main/java/bisq/core/account/sign/SignedWitness.java @@ -117,7 +117,7 @@ public protobuf.PersistableNetworkPayload toProtoMessage() { return protobuf.PersistableNetworkPayload.newBuilder().setSignedWitness(builder).build(); } - protobuf.SignedWitness toProtoSignedWitness() { + public protobuf.SignedWitness toProtoSignedWitness() { return toProtoMessage().getSignedWitness(); } 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 e3a22f08982..a926045df4e 100644 --- a/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java +++ b/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java @@ -56,6 +56,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.Stack; import java.util.stream.Collectors; @@ -203,6 +204,17 @@ public Set getSignedWitnessSetByOwnerPubKey(byte[] ownerPubKey) { .collect(Collectors.toSet()); } + public boolean publishOwnSignedWitness(SignedWitness signedWitness) { + if (!Arrays.equals(signedWitness.getWitnessOwnerPubKey(), keyRing.getPubKeyRing().getSignaturePubKeyBytes()) || + !verifySigner(signedWitness)) { + return false; + } + + log.info("Publish own signedWitness {}", signedWitness); + publishSignedWitness(signedWitness); + return true; + } + // Arbitrators sign with EC key public void signAccountAgeWitness(Coin tradeAmount, AccountAgeWitness accountAgeWitness, @@ -267,17 +279,17 @@ public void selfSignAccountAgeWitness(AccountAgeWitness accountAgeWitness) throw } // Any peer can sign with DSA key - public void signAccountAgeWitness(Coin tradeAmount, - AccountAgeWitness accountAgeWitness, - PublicKey peersPubKey) throws CryptoException { + public Optional signAccountAgeWitness(Coin tradeAmount, + AccountAgeWitness accountAgeWitness, + PublicKey peersPubKey) throws CryptoException { if (isSignedAccountAgeWitness(accountAgeWitness)) { log.warn("Trader trying to sign already signed accountagewitness {}", accountAgeWitness.toString()); - return; + return Optional.empty(); } if (!isSufficientTradeAmountForSigning(tradeAmount)) { log.warn("Trader tried to sign account with too little trade amount"); - return; + return Optional.empty(); } byte[] signature = Sig.sign(keyRing.getSignatureKeyPair().getPrivate(), accountAgeWitness.getHash()); @@ -290,6 +302,7 @@ public void signAccountAgeWitness(Coin tradeAmount, tradeAmount.value); publishSignedWitness(signedWitness); log.info("Trader signed witness {}", signedWitness.toString()); + return Optional.of(signedWitness); } public boolean verifySignature(SignedWitness signedWitness) { @@ -366,8 +379,8 @@ 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)); + (key, oldValue) -> oldValue == null ? signedWitness : + oldValue.getDate() > signedWitness.getDate() ? signedWitness : oldValue)); return new HashSet<>(oldestUnsignedSigners.values()); } @@ -393,6 +406,11 @@ public boolean isSufficientTradeAmountForSigning(Coin tradeAmount) { return !tradeAmount.isLessThan(MINIMUM_TRADE_AMOUNT_FOR_SIGNING); } + private boolean verifySigner(SignedWitness signedWitness) { + return getSignedWitnessSetByOwnerPubKey(signedWitness.getWitnessOwnerPubKey(), new Stack<>()).stream() + .anyMatch(w -> isValidSignerWitnessInternal(w, signedWitness.getDate(), new Stack<>())); + } + /** * Checks whether the accountAgeWitness has a valid signature from a peer/arbitrator and is allowed to sign * other accounts. @@ -417,7 +435,7 @@ private boolean isSignerAccountAgeWitness(AccountAgeWitness accountAgeWitness, l * Helper to isValidAccountAgeWitness(accountAgeWitness) * * @param signedWitness the signedWitness to validate - * @param childSignedWitnessDateMillis the date the child SignedWitness was signed or current time if it is a leave. + * @param childSignedWitnessDateMillis the date the child SignedWitness was signed or current time if it is a leaf. * @param excludedPubKeys stack to prevent recursive loops * @return true if signedWitness is valid, false otherwise. */ 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 f43a1027e65..f4620c7d0e2 100644 --- a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java @@ -668,7 +668,7 @@ public void arbitratorSignAccountAgeWitness(AccountAgeWitness accountAgeWitness, signedWitnessService.signAccountAgeWitness(accountAgeWitness, key, tradersPubKey, time); } - public void traderSignPeersAccountAgeWitness(Trade trade) { + public Optional traderSignPeersAccountAgeWitness(Trade trade) { AccountAgeWitness peersWitness = findTradePeerWitness(trade).orElse(null); Coin tradeAmount = trade.getTradeAmount(); checkNotNull(trade.getProcessModel().getTradingPeer().getPubKeyRing(), "Peer must have a keyring"); @@ -678,10 +678,15 @@ public void traderSignPeersAccountAgeWitness(Trade trade) { checkNotNull(peersPubKey, "Peers pub key must not be null"); try { - signedWitnessService.signAccountAgeWitness(tradeAmount, peersWitness, peersPubKey); + return signedWitnessService.signAccountAgeWitness(tradeAmount, peersWitness, peersPubKey); } catch (CryptoException e) { log.warn("Trader failed to sign witness, exception {}", e.toString()); } + return Optional.empty(); + } + + public boolean publishOwnSignedWitness(SignedWitness signedWitness) { + return signedWitnessService.publishOwnSignedWitness(signedWitness); } // Arbitrator signing 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 6b436ca78e2..8d57a75c14b 100644 --- a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java +++ b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java @@ -58,6 +58,7 @@ import bisq.core.trade.messages.PayoutTxPublishedMessage; import bisq.core.trade.messages.PeerPublishedDelayedPayoutTxMessage; import bisq.core.trade.messages.RefreshTradeStateRequest; +import bisq.core.trade.messages.TraderSignedWitnessMessage; import bisq.core.trade.statistics.TradeStatistics; import bisq.network.p2p.AckMessage; @@ -165,6 +166,8 @@ public NetworkEnvelope fromProto(protobuf.NetworkEnvelope proto) throws Protobuf return PayoutTxPublishedMessage.fromProto(proto.getPayoutTxPublishedMessage(), messageVersion); case PEER_PUBLISHED_DELAYED_PAYOUT_TX_MESSAGE: return PeerPublishedDelayedPayoutTxMessage.fromProto(proto.getPeerPublishedDelayedPayoutTxMessage(), messageVersion); + case TRADER_SIGNED_WITNESS_MESSAGE: + return TraderSignedWitnessMessage.fromProto(proto.getTraderSignedWitnessMessage(), messageVersion); case MEDIATED_PAYOUT_TX_SIGNATURE_MESSAGE: return MediatedPayoutTxSignatureMessage.fromProto(proto.getMediatedPayoutTxSignatureMessage(), messageVersion); diff --git a/core/src/main/java/bisq/core/trade/messages/TraderSignedWitnessMessage.java b/core/src/main/java/bisq/core/trade/messages/TraderSignedWitnessMessage.java new file mode 100644 index 00000000000..c1172f08741 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/TraderSignedWitnessMessage.java @@ -0,0 +1,88 @@ +/* + * 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.trade.messages; + +import bisq.core.account.sign.SignedWitness; + +import bisq.network.p2p.MailboxMessage; +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@EqualsAndHashCode(callSuper = true) +@Value +public class TraderSignedWitnessMessage extends TradeMessage implements MailboxMessage { + private final NodeAddress senderNodeAddress; + private final SignedWitness signedWitness; + + public TraderSignedWitnessMessage(String uid, + String tradeId, + NodeAddress senderNodeAddress, + SignedWitness signedWitness) { + this(Version.getP2PMessageVersion(), + uid, + tradeId, + senderNodeAddress, + signedWitness); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private TraderSignedWitnessMessage(int messageVersion, + String uid, + String tradeId, + NodeAddress senderNodeAddress, + SignedWitness signedWitness) { + super(messageVersion, tradeId, uid); + this.senderNodeAddress = senderNodeAddress; + this.signedWitness = signedWitness; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + final protobuf.TraderSignedWitnessMessage.Builder builder = protobuf.TraderSignedWitnessMessage.newBuilder(); + builder.setUid(uid) + .setTradeId(tradeId) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setSignedWitness(signedWitness.toProtoSignedWitness()); + return getNetworkEnvelopeBuilder().setTraderSignedWitnessMessage(builder).build(); + } + + public static TraderSignedWitnessMessage fromProto(protobuf.TraderSignedWitnessMessage proto, int messageVersion) { + return new TraderSignedWitnessMessage(messageVersion, + proto.getUid(), + proto.getTradeId(), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + SignedWitness.fromProto(proto.getSignedWitness())); + } + + @Override + public String toString() { + return "TraderSignedWitnessMessage{" + + "\n senderNodeAddress=" + senderNodeAddress + + "\n signedWitness=" + signedWitness + + "\n} " + super.toString(); + } + +} diff --git a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java index cc2a59c3187..4cf78045db2 100644 --- a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java @@ -26,6 +26,7 @@ import bisq.core.trade.messages.MediatedPayoutTxSignatureMessage; import bisq.core.trade.messages.PeerPublishedDelayedPayoutTxMessage; import bisq.core.trade.messages.TradeMessage; +import bisq.core.trade.messages.TraderSignedWitnessMessage; import bisq.core.trade.protocol.tasks.ApplyFilter; import bisq.core.trade.protocol.tasks.ProcessPeerPublishedDelayedPayoutTxMessage; import bisq.core.trade.protocol.tasks.mediation.BroadcastMediatedPayoutTx; @@ -228,6 +229,15 @@ private void handle(PeerPublishedDelayedPayoutTxMessage tradeMessage, NodeAddres taskRunner.run(); } + /////////////////////////////////////////////////////////////////////////////////////////// + // Peer has sent a SignedWitness + /////////////////////////////////////////////////////////////////////////////////////////// + + private void handle(TraderSignedWitnessMessage tradeMessage) { + // Publish signed witness, if it is valid and ours + processModel.getAccountAgeWitnessService().publishOwnSignedWitness(tradeMessage.getSignedWitness()); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Dispatcher @@ -240,6 +250,8 @@ protected void doHandleDecryptedMessage(TradeMessage tradeMessage, NodeAddress s handle((MediatedPayoutTxPublishedMessage) tradeMessage, sender); } else if (tradeMessage instanceof PeerPublishedDelayedPayoutTxMessage) { handle((PeerPublishedDelayedPayoutTxMessage) tradeMessage, sender); + } else if (tradeMessage instanceof TraderSignedWitnessMessage) { + handle((TraderSignedWitnessMessage) tradeMessage); } } @@ -287,6 +299,8 @@ protected void doApplyMailboxTradeMessage(TradeMessage tradeMessage, NodeAddress handle((MediatedPayoutTxPublishedMessage) tradeMessage, peerNodeAddress); } else if (tradeMessage instanceof PeerPublishedDelayedPayoutTxMessage) { handle((PeerPublishedDelayedPayoutTxMessage) tradeMessage, peerNodeAddress); + } else if (tradeMessage instanceof TraderSignedWitnessMessage) { + handle((TraderSignedWitnessMessage) tradeMessage); } } 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 8e89268c43a..09b23eb8d13 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 @@ -22,6 +22,7 @@ import bisq.desktop.util.DisplayUtils; import bisq.desktop.util.GUIUtil; +import bisq.core.account.sign.SignedWitness; import bisq.core.account.witness.AccountAgeWitness; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.btc.wallet.Restrictions; @@ -33,13 +34,17 @@ import bisq.core.trade.Contract; import bisq.core.trade.Trade; import bisq.core.trade.closed.ClosedTradableManager; +import bisq.core.trade.messages.RefreshTradeStateRequest; +import bisq.core.trade.messages.TraderSignedWitnessMessage; import bisq.core.user.User; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.BsqFormatter; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.validation.BtcAddressValidator; +import bisq.network.p2p.NodeAddress; import bisq.network.p2p.P2PService; +import bisq.network.p2p.SendMailboxMessageListener; import bisq.common.ClockWatcher; import bisq.common.app.DevEnv; @@ -58,6 +63,7 @@ import javafx.beans.property.SimpleObjectProperty; import java.util.Date; +import java.util.UUID; import java.util.stream.Collectors; import lombok.Getter; @@ -383,10 +389,45 @@ public boolean isSignWitnessTrade() { public void maybeSignWitness() { if (isSignWitnessTrade()) { - accountAgeWitnessService.traderSignPeersAccountAgeWitness(trade); + var signedWitness = accountAgeWitnessService.traderSignPeersAccountAgeWitness(trade); + signedWitness.ifPresent(this::sendSignedWitnessToPeer); } } + private void sendSignedWitnessToPeer(SignedWitness signedWitness) { + Trade trade = getTrade(); + if (trade == null) return; + + NodeAddress tradingPeerNodeAddress = trade.getTradingPeerNodeAddress(); + var traderSignedWitnessMessage = new TraderSignedWitnessMessage(UUID.randomUUID().toString(), trade.getId(), + tradingPeerNodeAddress, signedWitness); + + p2PService.sendEncryptedMailboxMessage( + tradingPeerNodeAddress, + trade.getProcessModel().getTradingPeer().getPubKeyRing(), + traderSignedWitnessMessage, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("SendMailboxMessageListener onArrived tradeId={} at peer {} SignedWitness {}", + trade.getId(), tradingPeerNodeAddress, signedWitness); + } + + @Override + public void onStoredInMailbox() { + log.info("SendMailboxMessageListener onStoredInMailbox tradeId={} at peer {} SignedWitness {}", + trade.getId(), tradingPeerNodeAddress, signedWitness); + } + + @Override + public void onFault(String errorMessage) { + log.error("SendMailboxMessageListener onFault tradeId={} at peer {} SignedWitness {}", + trade.getId(), tradingPeerNodeAddress, signedWitness); + } + } + ); + } + /////////////////////////////////////////////////////////////////////////////////////////// // States /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 89901abac4c..117dbb4cb53 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -77,6 +77,7 @@ message NetworkEnvelope { PeerPublishedDelayedPayoutTxMessage peer_published_delayed_payout_tx_message = 49; RefreshTradeStateRequest refresh_trade_state_request = 50; + TraderSignedWitnessMessage trader_signed_witness_message = 51; } } @@ -329,6 +330,13 @@ message RefreshTradeStateRequest { NodeAddress sender_node_address = 3; } +message TraderSignedWitnessMessage { + string uid = 1; + string trade_id = 2; + NodeAddress sender_node_address = 3; + SignedWitness signed_witness = 4; +} + // dispute enum SupportType {