First cut at transaction confidence levels. Expose a TransactionConfidence object that is updated by peers and wallets, which tracks how many peers have announced the transaction and the height of the transaction in the best chain (if any). Unit tests that check it does the right thing with re-orgs. Various small cleanups and simplifications in the tests.

This commit is contained in:
Mike Hearn 2011-12-16 18:13:55 +01:00 committed by Miron Cuperman
parent a5fc1c8cc5
commit 0a4dbb77cf
12 changed files with 451 additions and 90 deletions

View file

@ -46,4 +46,11 @@ public class InventoryMessage extends ListMessage {
super(params);
}
public void addBlock(Block block) {
addItem(new InventoryItem(InventoryItem.Type.Block, block.getHash()));
}
public void addTransaction(Transaction tx) {
addItem(new InventoryItem(InventoryItem.Type.Transaction, tx.getHash()));
}
}

View file

@ -63,4 +63,9 @@ public interface NetworkConnection {
/** Returns the version message received from the other end of the connection during the handshake. */
VersionMessage getVersionMessage();
/**
* @return The address of the other side of the network connection.
*/
public PeerAddress getPeerAddress();
}

View file

@ -20,9 +20,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.*;
import java.util.concurrent.*;
/**
@ -51,6 +49,10 @@ public class Peer {
// primary peer. This is to avoid redundant work and concurrency problems with downloading the same chain
// in parallel.
private boolean downloadData = true;
// Maps announced transaction hashes to the Transaction objects. If this is not a download peer, the Transaction
// objects must be provided from elsewhere (ie, a PeerGroup object). If the Transaction hasn't been downloaded or
// provided yet, the map value is null.
private Map<Sha256Hash, Transaction> announcedTransactionHashes;
/**
* If true, we do some things that may only make sense on constrained devices like Android phones. Currently this
@ -79,6 +81,7 @@ public class Peer {
this.pendingGetBlockFutures = new ArrayList<GetDataFuture<Block>>();
this.eventListeners = new ArrayList<PeerEventListener>();
this.fastCatchupTimeSecs = params.genesisBlock.getTimeSeconds();
this.announcedTransactionHashes = new HashMap<Sha256Hash, Transaction>();
}
/**
@ -95,6 +98,7 @@ public class Peer {
public Peer(NetworkParameters params, BlockChain blockChain, NetworkConnection connection) {
this(params, null, 0, blockChain);
this.conn = connection;
this.address = connection.getPeerAddress();
}
public synchronized void addEventListener(PeerEventListener listener) {
@ -298,16 +302,16 @@ public class Peer {
private void processInv(InventoryMessage inv) throws IOException {
// This should be called in the network loop thread for this peer.
List<InventoryItem> items = inv.getItems();
updateTransactionConfidenceLevels(items);
// If this peer isn't responsible for downloading stuff, ignore inv messages.
// TODO: In future, we should not ignore but count them. This allows a guesstimate of trustworthyness.
// If this peer isn't responsible for downloading stuff, don't go further.
if (!downloadData)
return;
// The peer told us about some blocks or transactions they have. For now we only care about blocks.
Block topBlock = blockChain.getUnconnectedBlock();
Sha256Hash topHash = (topBlock != null ? topBlock.getHash() : null);
List<InventoryItem> items = inv.getItems();
if (isNewBlockTickle(topHash, items)) {
// An inv with a single hash containing our most recent unconnected block is a special inv,
// it's kind of like a tickle from the peer telling us that it's time to download more blocks to catch up to
@ -326,6 +330,60 @@ public class Peer {
conn.writeMessage(getdata);
}
/**
* When a peer broadcasts an "inv" containing a transaction hash, it means the peer validated it and won't accept
* double spends of those coins. So by measuring what proportion of our total connected peers have seen a
* transaction we can make a guesstimate of how likely it is to be included in a block, assuming our internet
* connection is trustworthy.<p>
*
* This method keeps a map of transaction hashes to {@link Transaction} objects. It may not have the associated
* transaction objects available, if they weren't downloaded yet. Once a Transaction is downloaded, it's set as
* the value in the txSeen map. If this Peer isn't the download peer, the {@link PeerGroup} will manage distributing
* the Transaction objects to every peer, at which point the peer is expected to update the
* {@link TransactionConfidence} object itself.
*
* @param items Inventory items that were just announced.
*/
private void updateTransactionConfidenceLevels(List<InventoryItem> items) {
// Announced hashes may be updated by other threads in response to messages coming in from other peers.
synchronized (announcedTransactionHashes) {
for (InventoryItem item : items) {
if (item.type != InventoryItem.Type.Transaction) continue;
Transaction transaction = announcedTransactionHashes.get(item.hash);
if (transaction == null) {
// We didn't see this tx before.
log.debug("Newly announced undownloaded transaction ", item.hash);
announcedTransactionHashes.put(item.hash, null);
} else {
// It's been downloaded. Update the confidence levels. This may be called multiple times for
// the same transaction and the same peer, there is no obligation in the protocol to avoid
// redundant advertisements.
log.debug("Marking tx {} as seen by {}", item.hash, toString());
transaction.getConfidence().markBroadcastBy(address);
}
}
}
}
/**
* Called by {@link PeerGroup} to tell the Peer about a transaction that was just downloaded. If we have tracked
* the announcement, update the transactions confidence level at this time. Otherwise wait for it to appear.
*/
void trackTransaction(Transaction tx) {
// May run on arbitrary peer threads.
synchronized (announcedTransactionHashes) {
if (announcedTransactionHashes.containsKey(tx.getHash())) {
Transaction storedTx = announcedTransactionHashes.get(tx.getHash());
assert storedTx == tx || storedTx == null : "single Transaction instance";
log.debug("Provided with a downloaded transaction we have seen before: {}", tx.getHash());
tx.getConfidence().markBroadcastBy(address);
} else {
log.debug("Provided with a downloaded transaction we didn't see broadcast yet: {}", tx.getHash());
announcedTransactionHashes.put(tx.getHash(), tx);
}
}
}
/** A new block tickle is an inv with a hash containing the topmost block. */
private boolean isNewBlockTickle(Sha256Hash topHash, List<InventoryItem> items) {
return items.size() == 1 &&

View file

@ -79,7 +79,7 @@ public class PeerGroup {
// Callback for events related to chain download
private PeerEventListener downloadListener;
// Callbacks for events related to peer connection/disconnection
private Set<PeerEventListener> peerEventListeners;
private List<PeerEventListener> peerEventListeners;
// Peer discovery sources, will be polled occasionally if there aren't enough inactives.
private Set<PeerDiscovery> peerDiscoverers;
@ -111,7 +111,6 @@ public class PeerGroup {
inactives = new LinkedBlockingQueue<PeerAddress>();
peers = Collections.synchronizedSet(new HashSet<Peer>());
peerEventListeners = Collections.synchronizedSet(new HashSet<PeerEventListener>());
peerDiscoverers = Collections.synchronizedSet(new HashSet<PeerDiscovery>());
peerPool = new ThreadPoolExecutor(
DEFAULT_CONNECTIONS,
@ -119,6 +118,24 @@ public class PeerGroup {
THREAD_KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(1),
new PeerGroupThreadFactory());
// Peer event listeners get a subset of events seen by the group. We add our own internal listener to this so
// when we download a transaction, we can distribute it to each Peer in the pool so they can update the
// transactions confidence level if they've seen it be announced/when they see it be announced.
peerEventListeners = Collections.synchronizedList(new ArrayList<PeerEventListener>());
addEventListener(new AbstractPeerEventListener() {
@Override
public void onTransaction(Peer peer, Transaction t) {
handleBroadcastTransaction(t);
}
});
}
private synchronized void handleBroadcastTransaction(Transaction tx) {
// Called on the download peer thread when we have downloaded an advertised Transaction. Distribute it to all
// the peers in the group so they can update the confidence if they saw it be advertised or when they do see it.
for (Peer p : peers) {
p.trackTransaction(tx);
}
}
/**
@ -135,6 +152,7 @@ public class PeerGroup {
* to stop until the listener returns.</p>
*/
public void addEventListener(PeerEventListener listener) {
assert listener != null;
peerEventListeners.add(listener);
}

View file

@ -164,4 +164,8 @@ public class TCPNetworkConnection implements NetworkConnection {
public VersionMessage getVersionMessage() {
return versionMessage;
}
public PeerAddress getPeerAddress() {
return new PeerAddress(remoteIp, params.port);
}
}

View file

@ -32,7 +32,13 @@ import static com.google.bitcoin.core.Utils.*;
* It implements TWO serialization protocols - the BitCoin proprietary format which is identical to the C++
* implementation and is used for reading/writing transactions to the wire and for hashing. It also implements Java
* serialization which is used for the wallet. This allows us to easily add extra fields used for our own accounting
* or UI purposes.
* or UI purposes.<p>
*
* All Bitcoin transactions are at risk of being reversed, though the risk is much less than with traditional payment
* systems. Transactions have <i>confidence levels</i>, which help you decide whether to trust a transaction or not.
* Whether to trust a transaction is something that needs to be decided on a case by case basis - a rule that makes
* sense for selling MP3s might not make sense for selling cars, or accepting payments from a family member. If you
* are building a wallet, how to present confidence to your users is something to consider carefully.
*/
public class Transaction extends ChildMessage implements Serializable {
private static final Logger log = LoggerFactory.getLogger(Transaction.class);
@ -69,6 +75,9 @@ public class Transaction extends ChildMessage implements Serializable {
// This is an in memory helper only.
transient Sha256Hash hash;
// Data about how confirmed this tx is. Serialized, may be null.
private TransactionConfidence confidence;
Transaction(NetworkParameters params) {
super(params);
version = 1;
@ -98,7 +107,6 @@ public class Transaction extends ChildMessage implements Serializable {
* @param params NetworkParameters object.
* @param msg Bitcoin protocol formatted byte array containing message content.
* @param offset The location of the first msg byte within the array.
* @param protocolVersion Bitcoin protocol version.
* @param parseLazy Whether to perform a full parse immediately or delay until a read is requested.
* @param parseRetain Whether to retain the backing byte array for quick reserialization.
* If true and the backing byte array is invalidated due to modification of a field then
@ -176,31 +184,42 @@ public class Transaction extends ChildMessage implements Serializable {
return appearsIn;
}
/** Returns true if this transaction hasn't been seen in any block yet. */
/**
* Convenience wrapper around getConfidence().getAppearedAtChainHeight()
* @return true if this transaction hasn't been seen in any block yet.
*/
public boolean isPending() {
if (appearsIn == null)
return true;
if (appearsIn.size() == 0)
return true;
return false;
return getConfidence().getAppearedAtChainHeight() == TransactionConfidence.NOT_SEEN_IN_CHAIN;
}
/**
* Adds the given block to the internal serializable set of blocks in which this transaction appears. This is
* Puts the given block in the internal serializable set of blocks in which this transaction appears. This is
* used by the wallet to ensure transactions that appear on side chains are recorded properly even though the
* block stores do not save the transaction data at all.
* block stores do not save the transaction data at all.<p>
*
* If there is a re-org this will be called once for each block that was previously seen, to update which block
* is the best chain. The best chain block is guaranteed to be called last. So this must be idempotent.
*
* @param block The {@link StoredBlock} in which the transaction has appeared.
* @param bestChain whether to set the updatedAt timestamp from the block header (only if not already set)
*/
void addBlockAppearance(StoredBlock block, boolean bestChain) {
if (bestChain && updatedAt == null) {
updatedAt = new Date(block.getHeader().getTimeSeconds() * 1000);
}
void setBlockAppearance(StoredBlock block, boolean bestChain) {
if (appearsIn == null) {
appearsIn = new HashSet<StoredBlock>();
}
appearsIn.add(block);
if (bestChain) {
if (updatedAt == null) {
updatedAt = new Date(block.getHeader().getTimeSeconds() * 1000);
}
getConfidence().setAppearedAtChainHeight(block.getHeight());
}
}
/** Called by the wallet once a re-org means we don't appear in the best chain anymore. */
void notifyNotOnBestChain() {
getConfidence().setAppearedAtChainHeight(TransactionConfidence.NOT_IN_BEST_CHAIN);
}
/**
@ -655,6 +674,13 @@ public class Transaction extends ChildMessage implements Serializable {
return Collections.unmodifiableList(outputs);
}
public synchronized TransactionConfidence getConfidence() {
if (confidence == null) {
confidence = new TransactionConfidence();
}
return confidence;
}
@Override
public boolean equals(Object other) {
if (!(other instanceof Transaction)) return false;
@ -677,5 +703,4 @@ public class Transaction extends ChildMessage implements Serializable {
maybeParse();
out.defaultWriteObject();
}
}

View file

@ -0,0 +1,126 @@
/*
* Copyright 2011 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 com.google.bitcoin.core;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* <p>A <tt>TransactionConfidence</tt> object tracks data you can use to make a confidence decision about a transaction.
* It also contains some pre-canned rules for common scenarios: if you aren't really sure what level of confidence
* you need, these should prove useful.</p>
*
* <p>Confidence in a transaction can come in multiple ways:</p>
*
* <ul>
* <li>Because you created it yourself and only you have the necessary keys.</li>
* <li>Receiving it from a fully validating peer you know is trustworthy, for instance, because it's run by yourself.</li>
* <li>Receiving it from a peer on the network you randomly chose. If your network connection is not being
* intercepted, you have a pretty good chance of connecting to a node that is following the rules.</li>
* <li>Receiving it from multiple peers on the network. If your network connection is not being intercepted,
* hearing about a transaction from multiple peers indicates the network has accepted the transaction and
* thus miners likely have too (miners have the final say in whether a transaction becomes valid or not).</li>
* <li>Seeing the transaction appear appear in a block on the main chain. Your confidence increases as the transaction
* becomes further buried under work. Work can be measured either in blocks (roughly, units of time), or
* amount of work done.</li>
* </ul>
*
* <p>Alternatively, you may know beyond doubt that the transaction is "dead", that is, one or more of its inputs have
* been double spent and will never confirm.</p>
*
* <p>TransactionConfidence is purely a data structure, it doesn't try and keep itself up to date. To have fresh
* confidence data, you need to ensure the owning {@link Transaction} is being updated by something, like
* a {@link Wallet}.</p>
*/
public class TransactionConfidence implements Serializable {
private static final long serialVersionUID = 4577920141400556444L;
/**
* The peers that have announced the transaction to us. Network nodes don't have stable identities, so we use
* IP address as an approximation. It's obviously vulnerable to being gamed if we allow arbitrary people to connect
* to us, so only peers we explicitly connected to should go here.
*/
private Set<PeerAddress> broadcastBy;
public static final int NOT_SEEN_IN_CHAIN = -1;
public static final int NOT_IN_BEST_CHAIN = -2;
private int appearedAtChainHeight = NOT_SEEN_IN_CHAIN;
public TransactionConfidence() {
// Assume a default number of peers for our set.
broadcastBy = Collections.synchronizedSet(new HashSet<PeerAddress>(10));
}
/**
* The chain height at which the transaction appeared, or {@link TransactionConfidence#NOT_IN_BEST_CHAIN} or
* {@link TransactionConfidence@NOT_IN_BEST_CHAIN}.
*/
public synchronized int getAppearedAtChainHeight() {
return appearedAtChainHeight;
}
/**
* The chain height at which the transaction appeared, or {@link TransactionConfidence#NOT_IN_BEST_CHAIN} or
* {@link TransactionConfidence@NOT_IN_BEST_CHAIN}.
*/
public synchronized void setAppearedAtChainHeight(int appearedAtChainHeight) {
if (appearedAtChainHeight < NOT_IN_BEST_CHAIN)
throw new IllegalArgumentException("appearedAtChainHeight out of range");
this.appearedAtChainHeight = appearedAtChainHeight;
}
/**
* Called by a {@link Peer} when a transaction is pending and announced by a peer. The more peers announce the
* transaction, the more peers have validated it (assuming your internet connection is not being intercepted).
* @param address IP address of the peer, used as a proxy for identity.
*/
public void markBroadcastBy(PeerAddress address) {
broadcastBy.add(address);
}
/**
* @return how many peers have been passed to {@link TransactionConfidence#markBroadcastBy}.
*/
public int numBroadcastPeers() {
return broadcastBy.size();
}
/**
* @return A synchronized set of {@link PeerAddress}es that announced the transaction.
*/
public Set<PeerAddress> getBroadcastBy() {
return broadcastBy;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Seen by ");
builder.append(numBroadcastPeers());
builder.append(" peers. ");
int height = getAppearedAtChainHeight();
switch (height) {
case NOT_IN_BEST_CHAIN: builder.append("Not currently in best chain."); break;
case NOT_SEEN_IN_CHAIN: builder.append("Not seen in any block yet."); break;
default: builder.append("Appeared in best chain at height "); builder.append(height); break;
}
return builder.toString();
}
}

View file

@ -337,9 +337,10 @@ public class Wallet implements Serializable {
// A transaction we created appeared in a block. Probably this is a spend we broadcast that has been
// accepted by the network.
//
// Mark the tx as appearing in this block so we can find it later after a re-org.
// Mark the tx as appearing in this block so we can find it later after a re-org. This also lets the
// transaction update its confidence and timestamp bookkeeping data.
if (block != null)
tx.addBlockAppearance(block, bestChain);
tx.setBlockAppearance(block, bestChain);
if (bestChain) {
if (valueSentToMe.equals(BigInteger.ZERO)) {
// There were no change transactions so this tx is fully spent.
@ -369,11 +370,11 @@ public class Wallet implements Serializable {
pending.put(tx.getHash(), tx);
}
} else {
if (!reorg) {
// Mark the tx as appearing in this block so we can find it later after a re-org.
// Mark the tx as appearing in this block so we can find it later after a re-org. If this IS a re-org taking
// place, this call will let the Transaction update its confidence and timestamp data to reflect the new
// best chain, as long as the new best chain blocks are passed to receive() last.
if (block != null)
tx.addBlockAppearance(block, bestChain);
}
tx.setBlockAppearance(block, bestChain);
// This TX didn't originate with us. It could be sending us coins and also spending our own coins if keys
// are being shared between different wallets.
if (sideChain) {
@ -961,7 +962,7 @@ public class Wallet implements Serializable {
* we need to go through our transactions and find out if any have become invalid. It's possible for our balance
* to go down in this case: money we thought we had can suddenly vanish if the rest of the network agrees it
* should be so.<p>
* <p/>
*
* The oldBlocks/newBlocks lists are ordered height-wise from top first to bottom last.
*/
synchronized void reorganize(List<StoredBlock> oldBlocks, List<StoredBlock> newBlocks) throws VerificationException {
@ -974,9 +975,9 @@ public class Wallet implements Serializable {
//
// receive() has been called on the block that is triggering the re-org before this is called.
log.info(" Old part of chain (top to bottom):");
log.info("Old part of chain (top to bottom):");
for (StoredBlock b : oldBlocks) log.info(" {}", b.getHeader().getHashAsString());
log.info(" New part of chain (top to bottom):");
log.info("New part of chain (top to bottom):");
for (StoredBlock b : newBlocks) log.info(" {}", b.getHeader().getHashAsString());
// Transactions that appear in the old chain segment.
@ -1046,6 +1047,7 @@ public class Wallet implements Serializable {
assert badInput == null : "Failed to connect " + tx.getHashAsString() + ", " + badInput.toString();
}
// Recalculate the unspent/spent buckets for the transactions the re-org did not affect.
log.info("Moving transactions");
unspent.clear();
spent.clear();
inactive.clear();
@ -1062,6 +1064,11 @@ public class Wallet implements Serializable {
spent.put(tx.getHash(), tx);
}
}
// Inform all transactions that exist only in the old chain that they have moved, so they can update confidence
// and timestamps. Transactions will be told they're on the new best chain when the blocks are replayed.
for (Transaction tx : onlyOldChainTransactions.values()) {
tx.notifyNotOnBestChain();
}
// Now replay the act of receiving the blocks that were previously in a side chain. This will:
// - Move any transactions that were pending and are now accepted into the right bucket.
// - Connect the newly active transactions.
@ -1097,7 +1104,7 @@ public class Wallet implements Serializable {
Map<Sha256Hash, Transaction> toReprocess = new HashMap<Sha256Hash, Transaction>();
toReprocess.putAll(onlyOldChainTransactions);
toReprocess.putAll(pending);
log.info("Reprocessing:");
log.info("Reprocessing transactions not in new best chain:");
// Note, we must reprocess dead transactions first. The reason is that if there is a double spend across
// chains from our own coins we get a complicated situation:
//
@ -1128,7 +1135,7 @@ public class Wallet implements Serializable {
}
private void reprocessTxAfterReorg(Map<Sha256Hash, Transaction> pool, Transaction tx) {
log.info(" TX {}", tx.getHashAsString());
log.info("TX {}", tx.getHashAsString());
int numInputs = tx.getInputs().size();
int noSuchTx = 0;
int success = 0;
@ -1208,7 +1215,16 @@ public class Wallet implements Serializable {
// This object is used to receive events from a Peer or PeerGroup. Currently it is only used to receive
// transactions. Note that it does NOT pay attention to block message because they will be received from the
// BlockChain object along with extra data we need for correct handling of re-orgs.
private PeerEventListener peerEventListener = new AbstractPeerEventListener() {
private transient PeerEventListener peerEventListener;
/**
* Use the returned object can be used to connect the wallet to a {@link Peer} or {@link PeerGroup} in order to
* receive and process blocks and transactions.
*/
public PeerEventListener getPeerEventListener() {
if (peerEventListener == null) {
// Instantiate here to avoid issues with wallets resurrected from serialized copies.
peerEventListener = new AbstractPeerEventListener() {
@Override
public void onTransaction(Peer peer, Transaction t) {
// Runs locked on a peer thread.
@ -1223,12 +1239,7 @@ public class Wallet implements Serializable {
}
}
};
/**
* Use the returned object can be used to connect the wallet to a {@link Peer} or {@link PeerGroup} in order to
* receive and process blocks and transactions.
*/
public PeerEventListener getPeerEventListener() {
}
return peerEventListener;
}
}

View file

@ -90,6 +90,8 @@ public class PingService {
peerGroup.setFastCatchupTimeSecs((new Date().getTime() / 1000) - (60 * 60 * 24));
if (peerHost != null) {
// TEMP!
peerGroup.addAddress(new PeerAddress(InetAddress.getLocalHost(), peerPort));
peerGroup.addAddress(new PeerAddress(InetAddress.getByName(peerHost), peerPort));
} else {
peerGroup.addPeerDiscovery(new DnsDiscovery(params));
@ -110,6 +112,7 @@ public class PingService {
if (tx.isPending()) {
System.out.println("Received pending tx for " + Utils.bitcoinValueToFriendlyString(value) +
": " + tx);
System.out.println(tx.getConfidence());
try {
w.saveToFile(walletFile);
} catch (IOException e) {
@ -118,6 +121,9 @@ public class PingService {
// Ignore for now, as we won't be allowed to spend until the tx is no longer pending. We'll get
// another callback here when the tx is confirmed.
return;
} else {
System.out.println("Saw tx be incorporated into chain");
System.out.println(tx.getConfidence());
}
// It's impossible to pick one specific identity that you receive coins from in BitCoin as there

View file

@ -17,29 +17,30 @@
package com.google.bitcoin.core;
import com.google.bitcoin.store.MemoryBlockStore;
import com.google.bitcoin.utils.BriefLogFormatter;
import org.junit.Before;
import org.junit.Test;
import java.math.BigInteger;
import java.util.ArrayList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.*;
public class ChainSplitTests {
private NetworkParameters unitTestParams;
private Wallet wallet;
private BlockChain chain;
private Address coinbaseTo;
private Address coinsTo;
private Address someOtherGuy;
@Before
public void setUp() throws Exception {
BriefLogFormatter.init();
unitTestParams = NetworkParameters.unitTests();
wallet = new Wallet(unitTestParams);
wallet.addKey(new ECKey());
chain = new BlockChain(unitTestParams, wallet, new MemoryBlockStore(unitTestParams));
coinbaseTo = wallet.keychain.get(0).toAddress(unitTestParams);
coinsTo = wallet.keychain.get(0).toAddress(unitTestParams);
someOtherGuy = new ECKey().toAddress(unitTestParams);
}
@ -48,7 +49,6 @@ public class ChainSplitTests {
// Check that if the block chain forks, we end up using the right chain. Only tests inbound transactions
// (receiving coins). Checking that we understand reversed spends is in testForking2.
// TODO: Change this test to not use coinbase transactions as they are special (maturity rules).
final boolean[] reorgHappened = new boolean[1];
reorgHappened[0] = false;
wallet.addEventListener(new AbstractWalletEventListener() {
@ -59,8 +59,8 @@ public class ChainSplitTests {
});
// Start by building a couple of blocks on top of the genesis block.
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo);
Block b2 = b1.createNextBlock(coinbaseTo);
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinsTo);
Block b2 = b1.createNextBlock(coinsTo);
assertTrue(chain.add(b1));
assertTrue(chain.add(b2));
assertFalse(reorgHappened[0]);
@ -90,8 +90,8 @@ public class ChainSplitTests {
// We lost some coins! b2 is no longer a part of the best chain so our available balance should drop to 50.
assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
// ... and back to the first chain.
Block b5 = b2.createNextBlock(coinbaseTo);
Block b6 = b5.createNextBlock(coinbaseTo);
Block b5 = b2.createNextBlock(coinsTo);
Block b6 = b5.createNextBlock(coinsTo);
assertTrue(chain.add(b5));
assertTrue(chain.add(b6));
//
@ -113,7 +113,7 @@ public class ChainSplitTests {
// genesis -> b1 -> b2
// \-> b3 -> b4
assertEquals(BigInteger.ZERO, wallet.getBalance());
Block b3 = b1.createNextBlock(coinbaseTo);
Block b3 = b1.createNextBlock(coinsTo);
Block b4 = b3.createNextBlock(someOtherGuy);
assertTrue(chain.add(b3));
assertEquals(BigInteger.ZERO, wallet.getBalance());
@ -124,7 +124,7 @@ public class ChainSplitTests {
@Test
public void testForking3() throws Exception {
// Check that we can handle our own spends being rolled back by a fork.
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo);
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinsTo);
chain.add(b1);
assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
Address dest = new ECKey().toAddress(unitTestParams);
@ -155,7 +155,7 @@ public class ChainSplitTests {
// Check that we can handle external spends on an inactive chain becoming active. An external spend is where
// we see a transaction that spends our own coins but we did not broadcast it ourselves. This happens when
// keys are being shared between wallets.
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo);
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinsTo);
chain.add(b1);
assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
Address dest = new ECKey().toAddress(unitTestParams);
@ -192,7 +192,7 @@ public class ChainSplitTests {
}
});
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo);
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinsTo);
chain.add(b1);
Transaction t1 = wallet.createSend(someOtherGuy, Utils.toNanoCoins(10, 0));
@ -234,7 +234,7 @@ public class ChainSplitTests {
});
// Start with 50 coins.
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo);
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinsTo);
chain.add(b1);
Transaction t1 = wallet.createSend(someOtherGuy, Utils.toNanoCoins(10, 0));
@ -272,4 +272,67 @@ public class ChainSplitTests {
assertEquals(Utils.toNanoCoins(0, 0), wallet.getBalance());
assertEquals(Utils.toNanoCoins(40, 0), wallet.getBalance(Wallet.BalanceType.ESTIMATED));
}
@Test
public void txConfidenceLevels() throws Exception {
// Check that as the chain forks and re-orgs, the confidence data associated with each transaction is
// maintained correctly.
final ArrayList<Transaction> txns = new ArrayList<Transaction>(2);
wallet.addEventListener(new AbstractWalletEventListener() {
@Override
public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
txns.add(tx);
}
});
// Start by building a couple of blocks on top of the genesis block.
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinsTo);
Block b2 = b1.createNextBlock(coinsTo);
assertTrue(chain.add(b1));
assertTrue(chain.add(b2));
// Check the transaction confidence levels are correct.
assertEquals(2, txns.size());
assertEquals(1, txns.get(0).getConfidence().getAppearedAtChainHeight());
assertEquals(2, txns.get(1).getConfidence().getAppearedAtChainHeight());
// We now have the following chain:
// genesis -> b1 -> b2
//
// so fork like this:
//
// genesis -> b1 -> b2
// \-> b3
//
// Nothing should happen at this point. We saw b2 first so it takes priority.
Block b3 = b1.createNextBlock(someOtherGuy);
assertTrue(chain.add(b3));
assertEquals(2, txns.size());
assertEquals(1, txns.get(0).getConfidence().getAppearedAtChainHeight());
assertEquals(2, txns.get(1).getConfidence().getAppearedAtChainHeight());
// Now we add another block to make the alternative chain longer.
assertTrue(chain.add(b3.createNextBlock(someOtherGuy)));
//
// genesis -> b1 -> b2
// \-> b3 -> b4
//
assertEquals(2, txns.size());
assertEquals(1, txns.get(0).getConfidence().getAppearedAtChainHeight());
assertEquals(TransactionConfidence.NOT_IN_BEST_CHAIN, txns.get(1).getConfidence().getAppearedAtChainHeight());
// ... and back to the first chain.
Block b5 = b2.createNextBlock(coinsTo);
Block b6 = b5.createNextBlock(coinsTo);
assertTrue(chain.add(b5));
assertTrue(chain.add(b6));
//
// genesis -> b1 -> b2 -> b5 -> b6
// \-> b3 -> b4
//
// This should be enabled, once we figure out the best way to inform the user of how the wallet is changing
// during the re-org.
// assertEquals(4, txns.size());
assertEquals(1, txns.get(0).getConfidence().getAppearedAtChainHeight());
assertEquals(2, txns.get(1).getConfidence().getAppearedAtChainHeight());
assertEquals("200.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
}
}

View file

@ -17,6 +17,8 @@
package com.google.bitcoin.core;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
@ -31,9 +33,17 @@ public class MockNetworkConnection implements NetworkConnection {
private Object disconnectMarker = new Object();
private VersionMessage versionMessage;
private static int fakePort = 1;
private PeerAddress peerAddress;
public MockNetworkConnection() {
inboundMessageQ = new ArrayBlockingQueue<Object>(10);
outboundMessageQ = new ArrayBlockingQueue<Message>(10);
try {
peerAddress = new PeerAddress(InetAddress.getLocalHost(), fakePort++);
} catch (UnknownHostException e) {
throw new RuntimeException(e); // Cannot happen.
}
}
public void ping() throws IOException {
@ -101,6 +111,11 @@ public class MockNetworkConnection implements NetworkConnection {
return versionMessage;
}
public PeerAddress getPeerAddress() {
return peerAddress;
}
/** Call this to add a message which will be received by the NetworkConnection user. Wakes up the network thread. */
public void inbound(Message m) {
try {

View file

@ -112,13 +112,10 @@ public class PeerGroupTest extends TestWithNetworkConnections {
BigInteger value = Utils.toNanoCoins(1, 0);
Transaction t1 = TestUtils.createFakeTx(unitTestParams, value, address);
InventoryMessage inv = new InventoryMessage(unitTestParams);
inv.addItem(new InventoryItem(InventoryItem.Type.Transaction, t1.getHash()));
n1.inbound(inv);
n2.inbound(inv);
GetDataMessage getdata = (GetDataMessage) n1.outbound();
assertNull(n2.outbound()); // Only one peer is used to download.
n1.inbound(t1);
n1.outbound(); // Wait for processing to complete.
inv.addTransaction(t1);
assertTrue(n1.exchange(inv) instanceof GetDataMessage);
assertNull(n2.exchange(inv)); // Only one peer is used to download.
assertNull(n1.exchange(t1));
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
}
@ -145,18 +142,14 @@ public class PeerGroupTest extends TestWithNetworkConnections {
// Peer 1 and 2 receives an inv advertising a newly solved block.
InventoryMessage inv = new InventoryMessage(params);
inv.addItem(new InventoryItem(InventoryItem.Type.Block, b3.getHash()));
n1.inbound(inv);
n2.inbound(inv);
inv.addBlock(b3);
// Only peer 1 tries to download it.
assertTrue(n1.outbound() instanceof GetDataMessage);
assertNull(n2.outbound());
assertTrue(n1.exchange(inv) instanceof GetDataMessage);
assertNull(n2.exchange(inv));
// Peer 1 goes away.
disconnectAndWait(n1);
// Peer 2 fetches it next time it hears an inv (should it fetch immediately?).
n2.inbound(inv);
assertTrue(n2.outbound() instanceof GetDataMessage);
assertTrue(n2.exchange(inv) instanceof GetDataMessage);
peerGroup.stop();
}
@ -186,22 +179,52 @@ public class PeerGroupTest extends TestWithNetworkConnections {
assertEquals(Sha256Hash.ZERO_HASH, getblocks.getStopHash());
// We give back an inv with some blocks in it.
InventoryMessage inv = new InventoryMessage(params);
inv.addItem(new InventoryItem(InventoryItem.Type.Block, b1.getHash()));
inv.addItem(new InventoryItem(InventoryItem.Type.Block, b2.getHash()));
inv.addItem(new InventoryItem(InventoryItem.Type.Block, b3.getHash()));
n1.inbound(inv);
// Peer creates a getdata message.
@SuppressWarnings("unused")
GetDataMessage getdata = (GetDataMessage) n1.outbound();
inv.addBlock(b1);
inv.addBlock(b2);
inv.addBlock(b3);
assertTrue(n1.exchange(inv) instanceof GetDataMessage);
// We hand back the first block.
n1.inbound(b1);
// Now we successfully connect to another peer. There should be no messages sent.
peerGroup.addPeer(p2);
Message message = n2.outbound();
assertNull(message == null ? "" : message.toString(), message);
}
@Test
public void transactionConfidence() throws Exception {
// Checks that we correctly count how many peers broadcast a transaction, so we can establish some measure of
// its trustworthyness assuming an untampered with internet connection.
MockNetworkConnection n1 = createMockNetworkConnection();
Peer p1 = new Peer(params, blockChain, n1);
MockNetworkConnection n2 = createMockNetworkConnection();
Peer p2 = new Peer(params, blockChain, n2);
MockNetworkConnection n3 = createMockNetworkConnection();
Peer p3 = new Peer(params, blockChain, n3);
peerGroup.start();
peerGroup.addPeer(p1);
peerGroup.addPeer(p2);
peerGroup.addPeer(p3);
Transaction tx = TestUtils.createFakeTx(params, Utils.toNanoCoins(20, 0), address);
InventoryMessage inv = new InventoryMessage(params);
inv.addTransaction(tx);
// Peer 2 advertises the tx but does not download it.
assertNull(n2.exchange(inv));
assertEquals(0, tx.getConfidence().numBroadcastPeers());
// Peer 1 (the download peer) advertises the tx, we download it.
n1.exchange(inv); // returns getdata
n1.exchange(tx); // returns nothing after a queue drain.
// Two peers saw this tx hash.
assertEquals(2, tx.getConfidence().numBroadcastPeers());
assertTrue(tx.getConfidence().getBroadcastBy().contains(n1.getPeerAddress()));
assertTrue(tx.getConfidence().getBroadcastBy().contains(n2.getPeerAddress()));
// A straggler reports in.
n3.exchange(inv);
assertEquals(3, tx.getConfidence().numBroadcastPeers());
assertTrue(tx.getConfidence().getBroadcastBy().contains(n3.getPeerAddress()));
}
private void disconnectAndWait(MockNetworkConnection conn) throws IOException, InterruptedException {
conn.disconnect();
disconnectedPeers.take();