From b36169caba63c360fe785553f0287952a83d62e2 Mon Sep 17 00:00:00 2001 From: Adam Mackler Date: Sun, 26 Oct 2014 12:28:31 -0400 Subject: [PATCH] Make `TransactionBroadcast` recognize network rejection of transmitted transaction. --- .../src/main/java/org/bitcoinj/core/Peer.java | 2 + .../java/org/bitcoinj/core/RejectMessage.java | 7 ++++ .../core/RejectedTransactionException.java | 40 +++++++++++++++++++ .../bitcoinj/core/TransactionBroadcast.java | 25 ++++++++++-- 4 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 core/src/main/java/org/bitcoinj/core/RejectedTransactionException.java diff --git a/core/src/main/java/org/bitcoinj/core/Peer.java b/core/src/main/java/org/bitcoinj/core/Peer.java index 9bee391fc..408535043 100644 --- a/core/src/main/java/org/bitcoinj/core/Peer.java +++ b/core/src/main/java/org/bitcoinj/core/Peer.java @@ -396,6 +396,8 @@ public class Peer extends PeerSocketHandler { utxosFuture = null; future.set((UTXOsMessage)m); } + } else if (m instanceof RejectMessage) { + log.error("Received Message {}", m); } else { log.warn("Received unhandled message: {}", m); } diff --git a/core/src/main/java/org/bitcoinj/core/RejectMessage.java b/core/src/main/java/org/bitcoinj/core/RejectMessage.java index 660e4f0fe..2b8d23de6 100644 --- a/core/src/main/java/org/bitcoinj/core/RejectMessage.java +++ b/core/src/main/java/org/bitcoinj/core/RejectMessage.java @@ -137,6 +137,13 @@ public class RejectMessage extends Message { return reason; } + + /** + * A String representation of the relevant details of this reject message. + * Be aware that the value returned by this method includes the value returned by + * {@link #getReasonString() getReasonString}, which is taken from the reject message unchecked. + * Through malice or otherwise, it might contain control characters or other harmful content. + */ @Override public String toString() { Sha256Hash hash = getRejectedObjectHash(); diff --git a/core/src/main/java/org/bitcoinj/core/RejectedTransactionException.java b/core/src/main/java/org/bitcoinj/core/RejectedTransactionException.java new file mode 100644 index 000000000..6b4091888 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/core/RejectedTransactionException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2014 Adam Mackler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bitcoinj.core; + +/** + * This exception is used by the TransactionBroadcast class to indicate that a broadcast + * Transaction has been rejected by the network, for example because it violates a + * protocol rule. Note that not all invalid transactions generate a reject message, and + * some peers may never do so. + */ +public class RejectedTransactionException extends Exception { + private Transaction tx; + private RejectMessage rejectMessage; + + public RejectedTransactionException(Transaction tx, RejectMessage rejectMessage) { + super(rejectMessage.toString()); + this.tx = tx; + this.rejectMessage = rejectMessage; + } + + /** Return the original Transaction object whose broadcast was rejected. */ + public Transaction getTransaction() { return tx; } + + /** Return the RejectMessage object representing the broadcast rejection. */ + public RejectMessage getRejectMessage() { return rejectMessage; } +} diff --git a/core/src/main/java/org/bitcoinj/core/TransactionBroadcast.java b/core/src/main/java/org/bitcoinj/core/TransactionBroadcast.java index 14afd04bb..f3528a1de 100644 --- a/core/src/main/java/org/bitcoinj/core/TransactionBroadcast.java +++ b/core/src/main/java/org/bitcoinj/core/TransactionBroadcast.java @@ -16,6 +16,9 @@ package org.bitcoinj.core; +import org.bitcoinj.core.AbstractPeerEventListener; +import org.bitcoinj.core.RejectMessage; +import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.utils.Threading; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; @@ -32,9 +35,8 @@ import java.util.Random; * Represents a single transaction broadcast that we are performing. A broadcast occurs after a new transaction is created * (typically by a {@link Wallet} and needs to be sent to the network. A broadcast can succeed or fail. A success is * defined as seeing the transaction be announced by peers via inv messages, thus indicating their acceptance. A failure - * is defined as not reaching acceptance within a timeout period, or getting an explicit error message from peers - * indicating that the transaction was not acceptable (this isn't currently implemented in v0.8 of the network protocol - * but should be coming in 0.9). + * is defined as not reaching acceptance within a timeout period, or getting an explicit reject message from a peer + * indicating that the transaction was not acceptable. */ public class TransactionBroadcast { private static final Logger log = LoggerFactory.getLogger(TransactionBroadcast.class); @@ -64,7 +66,22 @@ public class TransactionBroadcast { this.minConnections = minConnections; } + private PeerEventListener rejectionListener = new AbstractPeerEventListener() { + @Override + public Message onPreMessageReceived(Peer peer, Message m) { + if (m instanceof RejectMessage) { + RejectMessage rejectMessage = (RejectMessage)m; + if (tx.getHash().equals(rejectMessage.getRejectedObjectHash())) { + future.setException(new RejectedTransactionException(tx, rejectMessage)); + peerGroup.removeEventListener(this); + } + } + return m; + } + }; + public ListenableFuture broadcast() { + peerGroup.addEventListener(rejectionListener, Threading.SAME_THREAD); log.info("Waiting for {} peers required for broadcast ...", minConnections); peerGroup.waitForPeers(minConnections).addListener(new EnoughAvailablePeers(), Threading.SAME_THREAD); return future; @@ -118,6 +135,7 @@ public class TransactionBroadcast { // So we just have to assume we're done, at that point. This happens when we're not given // any peer discovery source and the user just calls connectTo() once. if (minConnections == 1) { + peerGroup.removeEventListener(rejectionListener); future.set(pinnedTx); } } @@ -148,6 +166,7 @@ public class TransactionBroadcast { // point to avoid triggering inversions when the Future completes. log.info("broadcastTransaction: {} complete", pinnedTx.getHashAsString()); tx.getConfidence().removeEventListener(this); + peerGroup.removeEventListener(rejectionListener); future.set(pinnedTx); // RE-ENTRANCY POINT } }