org.bitcoinj.governance.GovernanceVoteBroadcast Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2013 Google Inc.
*
* 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.governance;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import org.bitcoinj.core.*;
import org.bitcoinj.core.listeners.PreMessageReceivedEventListener;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.Wallet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.util.*;
import java.util.concurrent.Executor;
import static com.google.common.base.Preconditions.checkState;
/**
* 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 reject message from a peer
* indicating that the transaction was not acceptable.
*/
public class GovernanceVoteBroadcast {
private static final Logger log = LoggerFactory.getLogger(GovernanceVoteBroadcast.class);
private final SettableFuture future = SettableFuture.create();
private final PeerGroup peerGroup;
private final GovernanceVote vote;
private int minConnections;
private int numWaitingFor;
/** Used for shuffling the peers before broadcast: unit tests can replace this to make themselves deterministic. */
@VisibleForTesting
public static Random random = new Random();
// Tracks which nodes sent us a reject message about this broadcast, if any. Useful for debugging.
private Map rejects = Collections.synchronizedMap(new HashMap());
public GovernanceVoteBroadcast(PeerGroup peerGroup, GovernanceVote vote) {
this.peerGroup = peerGroup;
this.vote = vote;
this.minConnections = Math.max(1, peerGroup.getMinBroadcastConnections());
}
// Only for mock broadcasts.
private GovernanceVoteBroadcast(GovernanceVote vote) {
this.peerGroup = null;
this.vote = vote;
}
@VisibleForTesting
public static GovernanceVoteBroadcast createMockBroadcast(GovernanceVote vote, final SettableFuture future) {
return new GovernanceVoteBroadcast(vote) {
@Override
public ListenableFuture broadcast() {
return future;
}
@Override
public ListenableFuture future() {
return future;
}
};
}
public ListenableFuture future() {
return future;
}
public void setMinConnections(int minConnections) {
this.minConnections = minConnections;
}
private PreMessageReceivedEventListener rejectionListener = new PreMessageReceivedEventListener() {
@Override
public Message onPreMessageReceived(Peer peer, Message m) {
if (m instanceof RejectMessage) {
RejectMessage rejectMessage = (RejectMessage)m;
if (vote.getHash().equals(rejectMessage.getRejectedObjectHash())) {
rejects.put(peer, rejectMessage);
int size = rejects.size();
long threshold = Math.round(numWaitingFor / 2.0);
if (size > threshold) {
log.warn("Threshold for considering broadcast rejected has been reached ({}/{})", size, threshold);
future.setException(new RejectedGovernanceVoteException(vote, rejectMessage));
peerGroup.removePreMessageReceivedEventListener(this);
}
}
}
return m;
}
};
public ListenableFuture broadcast() {
peerGroup.addPreMessageReceivedEventListener(Threading.SAME_THREAD, rejectionListener);
log.info("Waiting for {} peers required for broadcasting vote, we have {} ...", minConnections, peerGroup.getConnectedPeers().size());
peerGroup.waitForPeers(minConnections).addListener(new EnoughAvailablePeers(), Threading.SAME_THREAD);
return future;
}
private class EnoughAvailablePeers implements Runnable {
@Override
public void run() {
// We now have enough connected peers to send the transaction.
// This can be called immediately if we already have enough. Otherwise it'll be called from a peer
// thread.
// We will send the vote simultaneously to half the connected peers and wait to hear back from at least half
// of the other half, i.e., with 4 peers connected we will send the vote to 2 randomly chosen peers, and then
// wait for it to show up on one of the other two. This will be taken as sign of network acceptance. As can
// be seen, 4 peers is probably too little - it doesn't taken many broken peers for vote propagation to have
// a big effect.
List peers = peerGroup.getConnectedPeers(); // snapshots
// Prepare to send the transaction by adding a listener that'll be called when confidence changes.
// Only bother with this if we might actually hear back:
if (minConnections > 1)
vote.getConfidence().addEventListener(new ConfidenceChange());
// Dash Core sends an inv in this case and then lets the peer request the vote data. We just
// blast out the TX here for a couple of reasons. Firstly it's simpler: in the case where we have
// just a single connection we don't have to wait for getdata to be received and handled before
// completing the future in the code immediately below. Secondly, it's faster. The reason the
// Dash Core sends an inv is privacy - it means you can't tell if the peer originated the
// transaction or not. However, we are not a fully validating node and this is advertised in
// our version message, as SPV nodes cannot relay it doesn't give away any additional information
// to skip the inv here - we wouldn't send invs anyway.
int numConnected = peers.size();
int numToBroadcastTo = (int) Math.max(1, Math.round(Math.ceil(peers.size() / 2.0)));
numWaitingFor = (int) Math.ceil((peers.size() - numToBroadcastTo) / 2.0);
Collections.shuffle(peers, random);
peers = peers.subList(0, numToBroadcastTo);
log.info("broadcastGovernanceVote: We have {} peers, adding {} to the memory pool", numConnected, vote.getHash().toString());
log.info("Sending to {} peers, will wait for {}, sending to: {}", numToBroadcastTo, numWaitingFor, Joiner.on(",").join(peers));
InventoryMessage inv = new InventoryMessage(vote.getParams());
inv.addItem(new InventoryItem(InventoryItem.Type.GovernanceObjectVote, vote.getHash()));
for (Peer peer : peers) {
try {
peer.sendMessage(inv);
// We don't record the peer as having seen the vote in the memory pool because we want to track only
// how many peers announced to us.
} catch (Exception e) {
log.error("Caught exception sending to {}", peer, e);
}
}
// If we've been limited to talk to only one peer, we can't wait to hear back because the
// remote peer won't tell us about transactions we just announced to it for obvious reasons.
// 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.removePreMessageReceivedEventListener(rejectionListener);
future.set(vote);
}
}
}
private int numSeemPeers;
private class ConfidenceChange implements GovernanceVoteConfidence.Listener {
@Override
public void onConfidenceChanged(GovernanceVoteConfidence conf, ChangeReason reason) {
// The number of peers that announced this vote has gone up.
int numSeenPeers = conf.numBroadcastPeers() + rejects.size();
//boolean mined = vote.getAppearsInHashes() != null;
log.info("broadcastTransaction: {}: TX {} seen by {} peers", reason, vote.getHash().toString(),
numSeenPeers);
// Progress callback on the requested thread.
invokeAndRecord(numSeenPeers);
if (numSeenPeers >= numWaitingFor) {
// We've seen the min required number of peers announce the transaction, or it was included
// in a block. Normally we'd expect to see it fully propagate before it gets mined, but
// it can be that a block is solved very soon after broadcast, and it's also possible that
// due to version skew and changes in the relay rules our transaction is not going to
// fully propagate yet can get mined anyway.
//
// Note that we can't wait for the current number of connected peers right now because we
// could have added more peers after the broadcast took place, which means they won't
// have seen the transaction. In future when peers sync up their memory pools after they
// connect we could come back and change this.
//
// We're done! It's important that the PeerGroup lock is not held (by this thread) at this
// point to avoid triggering inversions when the Future completes.
log.info("broadcastTransaction: {} complete", vote.getHash());
peerGroup.removePreMessageReceivedEventListener(rejectionListener);
conf.removeEventListener(this);
future.set(vote); // RE-ENTRANCY POINT
}
}
}
private void invokeAndRecord(int numSeenPeers) {
synchronized (this) {
this.numSeemPeers = numSeenPeers;
}
invokeProgressCallback(numSeenPeers);
}
private void invokeProgressCallback(int numSeenPeers) {
final ProgressCallback callback;
Executor executor;
synchronized (this) {
callback = this.callback;
executor = this.progressCallbackExecutor;
}
if (callback != null) {
final double progress = Math.min(1.0, numSeenPeers / (double) numWaitingFor);
checkState(progress >= 0.0 && progress <= 1.0, progress);
try {
if (executor == null)
callback.onBroadcastProgress(progress);
else
executor.execute(new Runnable() {
@Override
public void run() {
callback.onBroadcastProgress(progress);
}
});
} catch (Throwable e) {
log.error("Exception during progress callback", e);
}
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
/** An interface for receiving progress information on the propagation of the vote, from 0.0 to 1.0 */
public interface ProgressCallback {
/**
* onBroadcastProgress will be invoked on the provided executor when the progress of the transaction
* broadcast has changed, because the transaction has been announced by another peer or because the transaction
* was found inside a mined block (in this case progress will go to 1.0 immediately). Any exceptions thrown
* by this callback will be logged and ignored.
*/
void onBroadcastProgress(double progress);
}
@Nullable private ProgressCallback callback;
@Nullable private Executor progressCallbackExecutor;
/**
* Sets the given callback for receiving progress values, which will run on the user thread. See
* {@link Threading} for details. If the broadcast has already started then the callback will
* be invoked immediately with the current progress.
*/
public void setProgressCallback(ProgressCallback callback) {
setProgressCallback(callback, Threading.USER_THREAD);
}
/**
* Sets the given callback for receiving progress values, which will run on the given executor. If the executor
* is null then the callback will run on a network thread and may be invoked multiple times in parallel. You
* probably want to provide your UI thread or Threading.USER_THREAD for the second parameter. If the broadcast
* has already started then the callback will be invoked immediately with the current progress.
*/
public void setProgressCallback(ProgressCallback callback, @Nullable Executor executor) {
boolean shouldInvoke;
int num;
boolean mined;
synchronized (this) {
this.callback = callback;
this.progressCallbackExecutor = executor;
num = this.numSeemPeers;
shouldInvoke = numWaitingFor > 0;
}
if (shouldInvoke)
invokeProgressCallback(num);
}
}