mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-01-31 07:12:17 +00:00
Refactor some of the wallet/peer interaction. Analyze dependencies of relevant pending transactions.
The Peer object now asks each connected Wallet if it cares about a transaction. If it does, then receivePending() is not called immediately, but rather after downloading of dependencies and with those dependencies. If any dependencies are time locked, a new wallet property controls whether they are discarded or not.
This commit is contained in:
parent
bfcf67ee5a
commit
35d6084bbf
@ -69,6 +69,7 @@ public class Peer {
|
|||||||
private int blocksAnnounced;
|
private int blocksAnnounced;
|
||||||
// A class that tracks recent transactions that have been broadcast across the network, counts how many
|
// A class that tracks recent transactions that have been broadcast across the network, counts how many
|
||||||
// peers announced them and updates the transaction confidence data. It is passed to each Peer.
|
// peers announced them and updates the transaction confidence data. It is passed to each Peer.
|
||||||
|
// TODO: Make this final and unsynchronized.
|
||||||
private MemoryPool memoryPool;
|
private MemoryPool memoryPool;
|
||||||
// Each wallet added to the peer will be notified of downloaded transaction data.
|
// Each wallet added to the peer will be notified of downloaded transaction data.
|
||||||
private CopyOnWriteArrayList<Wallet> wallets;
|
private CopyOnWriteArrayList<Wallet> wallets;
|
||||||
@ -415,13 +416,43 @@ public class Peer {
|
|||||||
// We may get back a different transaction object.
|
// We may get back a different transaction object.
|
||||||
tx = memoryPool.seen(tx, getAddress());
|
tx = memoryPool.seen(tx, getAddress());
|
||||||
}
|
}
|
||||||
if (maybeHandleRequestedData(tx))
|
final Transaction fTx = tx;
|
||||||
|
if (maybeHandleRequestedData(fTx))
|
||||||
return;
|
return;
|
||||||
// Tell all wallets about this tx so they can check if it's relevant or not.
|
// Tell all wallets about this tx so they can check if it's relevant or not.
|
||||||
for (ListIterator<Wallet> it = wallets.listIterator(); it.hasNext();) {
|
for (ListIterator<Wallet> it = wallets.listIterator(); it.hasNext();) {
|
||||||
Wallet wallet = it.next();
|
final Wallet wallet = it.next();
|
||||||
try {
|
try {
|
||||||
wallet.receivePending(tx);
|
if (wallet.isPendingTransactionRelevant(fTx)) {
|
||||||
|
// This transaction seems interesting to us, so let's download its dependencies. This has several
|
||||||
|
// purposes: we can check that the sender isn't attacking us by engaging in protocol abuse games,
|
||||||
|
// like depending on a time-locked transaction that will never confirm, or building huge chains
|
||||||
|
// of unconfirmed transactions (again - so they don't confirm and the money can be taken
|
||||||
|
// back with a Finney attack). Knowing the dependencies also lets us store them in a serialized
|
||||||
|
// wallet so we always have enough data to re-announce to the network and get the payment into
|
||||||
|
// the chain, in case the sender goes away and the network starts to forget.
|
||||||
|
// TODO: Not all the above things are implemented.
|
||||||
|
|
||||||
|
Futures.addCallback(downloadDependencies(fTx), new FutureCallback<List<Transaction>>() {
|
||||||
|
public void onSuccess(List<Transaction> dependencies) {
|
||||||
|
try {
|
||||||
|
log.info("Dependency download complete!");
|
||||||
|
wallet.receivePending(fTx, dependencies);
|
||||||
|
} catch (VerificationException e) {
|
||||||
|
log.error("Wallet failed to process pending transaction {}", fTx.getHashAsString());
|
||||||
|
log.error("Error was: ", e);
|
||||||
|
// Not much more we can do at this point.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onFailure(Throwable throwable) {
|
||||||
|
log.error("Could not download dependencies of tx {}", fTx.getHashAsString());
|
||||||
|
log.error("Error was: ", throwable);
|
||||||
|
// Not much more we can do at this point.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
} catch (VerificationException e) {
|
} catch (VerificationException e) {
|
||||||
log.error("Wallet failed to verify tx", e);
|
log.error("Wallet failed to verify tx", e);
|
||||||
// Carry on, listeners may still want to know.
|
// Carry on, listeners may still want to know.
|
||||||
@ -446,7 +477,7 @@ public class Peer {
|
|||||||
*
|
*
|
||||||
* <p>For example, if tx has 2 inputs that connect to transactions A and B, and transaction B is unconfirmed and
|
* <p>For example, if tx has 2 inputs that connect to transactions A and B, and transaction B is unconfirmed and
|
||||||
* has one input connecting to transaction C that is unconfirmed, and transaction C connects to transaction D
|
* has one input connecting to transaction C that is unconfirmed, and transaction C connects to transaction D
|
||||||
* that is in the chain, then this method will return either {B, C} or {C, B}.</p>
|
* that is in the chain, then this method will return either {B, C} or {C, B}. No ordering is guaranteed.</p>
|
||||||
*
|
*
|
||||||
* <p>This method is useful for apps that want to learn about how long an unconfirmed transaction might take
|
* <p>This method is useful for apps that want to learn about how long an unconfirmed transaction might take
|
||||||
* to confirm, by checking for unexpectedly time locked transactions, unusually deep dependency trees or fee-paying
|
* to confirm, by checking for unexpectedly time locked transactions, unusually deep dependency trees or fee-paying
|
||||||
@ -492,6 +523,7 @@ public class Peer {
|
|||||||
for (TransactionInput input : tx.getInputs()) {
|
for (TransactionInput input : tx.getInputs()) {
|
||||||
// There may be multiple inputs that connect to the same transaction.
|
// There may be multiple inputs that connect to the same transaction.
|
||||||
Sha256Hash hash = input.getOutpoint().getHash();
|
Sha256Hash hash = input.getOutpoint().getHash();
|
||||||
|
synchronized (this) {
|
||||||
Transaction dep = memoryPool.get(hash);
|
Transaction dep = memoryPool.get(hash);
|
||||||
if (dep == null) {
|
if (dep == null) {
|
||||||
needToRequest.add(hash);
|
needToRequest.add(hash);
|
||||||
@ -499,6 +531,7 @@ public class Peer {
|
|||||||
dependencies.add(dep);
|
dependencies.add(dep);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
results.addAll(dependencies);
|
results.addAll(dependencies);
|
||||||
try {
|
try {
|
||||||
// Build the request for the missing dependencies.
|
// Build the request for the missing dependencies.
|
||||||
|
@ -1074,7 +1074,10 @@ public class PeerGroup extends AbstractIdleService {
|
|||||||
synchronized (PeerGroup.this) {
|
synchronized (PeerGroup.this) {
|
||||||
for (Wallet wallet : wallets) {
|
for (Wallet wallet : wallets) {
|
||||||
try {
|
try {
|
||||||
wallet.receivePending(pinnedTx);
|
if (wallet.isPendingTransactionRelevant(pinnedTx)) {
|
||||||
|
// Assumption here is there are no dependencies of the created transaction.
|
||||||
|
wallet.receivePending(pinnedTx, null);
|
||||||
|
}
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
future.setException(t);
|
future.setException(t);
|
||||||
return;
|
return;
|
||||||
@ -1106,7 +1109,10 @@ public class PeerGroup extends AbstractIdleService {
|
|||||||
// wallet now we know it's valid.
|
// wallet now we know it's valid.
|
||||||
for (Wallet wallet : wallets) {
|
for (Wallet wallet : wallets) {
|
||||||
try {
|
try {
|
||||||
wallet.receivePending(pinnedTx);
|
if (wallet.isPendingTransactionRelevant(pinnedTx)) {
|
||||||
|
// Assumption here is there are no dependencies of the created transaction.
|
||||||
|
wallet.receivePending(pinnedTx, null);
|
||||||
|
}
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
future.setException(t);
|
future.setException(t);
|
||||||
return;
|
return;
|
||||||
|
@ -192,6 +192,8 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||||||
// in receive() via Transaction.setBlockAppearance(). As the BlockChain always calls notifyNewBestBlock even if
|
// in receive() via Transaction.setBlockAppearance(). As the BlockChain always calls notifyNewBestBlock even if
|
||||||
// it sent transactions to the wallet, without this we'd double count.
|
// it sent transactions to the wallet, without this we'd double count.
|
||||||
private transient HashSet<Sha256Hash> ignoreNextNewBlock;
|
private transient HashSet<Sha256Hash> ignoreNextNewBlock;
|
||||||
|
// Whether or not to ignore nLockTime > 0 transactions that are received to the mempool.
|
||||||
|
private boolean acceptTimeLockedTransactions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new, empty wallet with no keys and no transactions. If you want to restore a wallet from disk instead,
|
* Creates a new, empty wallet with no keys and no transactions. If you want to restore a wallet from disk instead,
|
||||||
@ -226,6 +228,7 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||||||
invokeOnWalletChanged();
|
invokeOnWalletChanged();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
acceptTimeLockedTransactions = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public NetworkParameters getNetworkParameters() {
|
public NetworkParameters getNetworkParameters() {
|
||||||
@ -290,6 +293,28 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||||||
saveToFile(temp, f);
|
saveToFile(temp, f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Whether or not the wallet will ignore transactions that have a lockTime parameter > 0. By default, all such
|
||||||
|
* transactions are ignored, because they are useful only in special protocols and such a transaction may not
|
||||||
|
* confirm as fast as an app typically expects. By setting this property to true, you are acknowledging that
|
||||||
|
* you understand what time-locked transactions are, and that your code is capable of handling them without risk.
|
||||||
|
* For instance you are not providing anything valuable in return for an unconfirmed transaction that has a lock
|
||||||
|
* time far in the future (which opens you up to Finney attacks).</p>
|
||||||
|
*
|
||||||
|
* <p>Note that this property is not serialized. So you have to set it to true each time you load or create a
|
||||||
|
* wallet.</p>
|
||||||
|
*/
|
||||||
|
public void setAcceptTimeLockedTransactions(boolean acceptTimeLockedTransactions) {
|
||||||
|
this.acceptTimeLockedTransactions = acceptTimeLockedTransactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See {@link Wallet#setAcceptTimeLockedTransactions(boolean)} for an explanation of this property.
|
||||||
|
*/
|
||||||
|
public boolean doesAcceptTimeLockedTransactions() {
|
||||||
|
return acceptTimeLockedTransactions;
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-saving can be done on a background thread if the user wishes it, this is to avoid stalling threads calling
|
// Auto-saving can be done on a background thread if the user wishes it, this is to avoid stalling threads calling
|
||||||
// into the wallet on serialization/disk access all the time which is important in GUI apps where you don't want
|
// into the wallet on serialization/disk access all the time which is important in GUI apps where you don't want
|
||||||
// the main thread to ever wait on disk (otherwise you lose a lot of responsiveness). The primary case where it
|
// the main thread to ever wait on disk (otherwise you lose a lot of responsiveness). The primary case where it
|
||||||
@ -640,35 +665,35 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||||||
receive(tx, block, blockType, false);
|
receive(tx, block, blockType, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static class AnalysisResult {
|
||||||
|
// Which tx, if any, had a non-zero lock time.
|
||||||
|
Transaction timeLocked;
|
||||||
|
// In future, depth, fees, if any are non-standard, anything else that's interesting ...
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when we have found a transaction (via network broadcast or otherwise) that is relevant to this wallet
|
* <p>Called when we have found a transaction (via network broadcast or otherwise) that is relevant to this wallet
|
||||||
* and want to record it. Note that we <b>cannot verify these transactions at all</b>, they may spend fictional
|
* and want to record it. Note that we <b>cannot verify these transactions at all</b>, they may spend fictional
|
||||||
* coins or be otherwise invalid. They are useful to inform the user about coins they can expect to receive soon,
|
* coins or be otherwise invalid. They are useful to inform the user about coins they can expect to receive soon,
|
||||||
* and if you trust the sender of the transaction you can choose to assume they are in fact valid and will not
|
* and if you trust the sender of the transaction you can choose to assume they are in fact valid and will not
|
||||||
* be double spent as an optimization.
|
* be double spent as an optimization.</p>
|
||||||
*
|
*
|
||||||
* @param tx
|
* <p>Before this method is called, {@link Wallet#isPendingTransactionRelevant(Transaction)} should have been
|
||||||
* @throws VerificationException
|
* called to decide whether the wallet cares about the transaction - if it does, then this method expects the
|
||||||
|
* transaction and any dependencies it has which are still in the memory pool.</p>
|
||||||
*/
|
*/
|
||||||
public synchronized void receivePending(Transaction tx) throws VerificationException {
|
public synchronized void receivePending(Transaction tx, List<Transaction> dependencies) throws VerificationException {
|
||||||
// Can run in a peer thread.
|
// Can run in a peer thread. This method will only be called if a prior call to isPendingTransactionRelevant
|
||||||
|
// returned true, so we already know by this point that it sends coins to or from our wallet, or is a double
|
||||||
// Ignore it if we already know about this transaction. Receiving a pending transaction never moves it
|
// spend against one of our other pending transactions.
|
||||||
// between pools.
|
//
|
||||||
EnumSet<Pool> containingPools = getContainingPools(tx);
|
// Do a brief risk analysis of the transaction and its dependencies to check for any possible attacks.
|
||||||
if (!containingPools.equals(EnumSet.noneOf(Pool.class))) {
|
AnalysisResult analysis = analyzeTransactionAndDependencies(tx, dependencies);
|
||||||
log.debug("Received tx we already saw in a block or created ourselves: " + tx.getHashAsString());
|
if (analysis.timeLocked != null && !doesAcceptTimeLockedTransactions()) {
|
||||||
|
log.warn("Transaction {}, dependency of {} has a time lock value of {}", new Object[] {
|
||||||
|
analysis.timeLocked.getHashAsString(), tx.getHashAsString(), analysis.timeLocked.getLockTime()});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We only care about transactions that:
|
|
||||||
// - Send us coins
|
|
||||||
// - Spend our coins
|
|
||||||
if (!isTransactionRelevant(tx)) {
|
|
||||||
log.debug("Received tx that isn't relevant to this wallet, discarding.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
BigInteger valueSentToMe = tx.getValueSentToMe(this);
|
BigInteger valueSentToMe = tx.getValueSentToMe(this);
|
||||||
BigInteger valueSentFromMe = tx.getValueSentFromMe(this);
|
BigInteger valueSentFromMe = tx.getValueSentFromMe(this);
|
||||||
if (log.isInfoEnabled()) {
|
if (log.isInfoEnabled()) {
|
||||||
@ -676,7 +701,6 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||||||
" and sends us %s BTC", tx.getHashAsString(), Utils.bitcoinValueToFriendlyString(valueSentFromMe),
|
" and sends us %s BTC", tx.getHashAsString(), Utils.bitcoinValueToFriendlyString(valueSentFromMe),
|
||||||
Utils.bitcoinValueToFriendlyString(valueSentToMe)));
|
Utils.bitcoinValueToFriendlyString(valueSentToMe)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark the tx as having been seen but is not yet in the chain. This will normally have been done already by
|
// Mark the tx as having been seen but is not yet in the chain. This will normally have been done already by
|
||||||
// the Peer before we got to this point, but in some cases (unit tests, other sources of transactions) it may
|
// the Peer before we got to this point, but in some cases (unit tests, other sources of transactions) it may
|
||||||
// have been missed out.
|
// have been missed out.
|
||||||
@ -687,7 +711,6 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||||||
// txConfidenceListener wasn't added.
|
// txConfidenceListener wasn't added.
|
||||||
invokeOnTransactionConfidenceChanged(tx);
|
invokeOnTransactionConfidenceChanged(tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this tx spends any of our unspent outputs, mark them as spent now, then add to the pending pool. This
|
// If this tx spends any of our unspent outputs, mark them as spent now, then add to the pending pool. This
|
||||||
// ensures that if some other client that has our keys broadcasts a spend we stay in sync. Also updates the
|
// ensures that if some other client that has our keys broadcasts a spend we stay in sync. Also updates the
|
||||||
// timestamp on the transaction and registers/runs event listeners.
|
// timestamp on the transaction and registers/runs event listeners.
|
||||||
@ -696,6 +719,51 @@ public class Wallet implements Serializable, BlockChainListener {
|
|||||||
commitTx(tx);
|
commitTx(tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AnalysisResult analyzeTransactionAndDependencies(Transaction tx, List<Transaction> dependencies) {
|
||||||
|
AnalysisResult result = new AnalysisResult();
|
||||||
|
if (tx.getLockTime() > 0)
|
||||||
|
result.timeLocked = tx;
|
||||||
|
if (dependencies != null) {
|
||||||
|
for (Transaction dep : dependencies) {
|
||||||
|
if (dep.getLockTime() > 0) {
|
||||||
|
result.timeLocked = dep;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is used by a {@link Peer} to find out if a transaction that has been announced is interesting,
|
||||||
|
* that is, whether we should bother downloading its dependencies and exploring the transaction to decide how
|
||||||
|
* risky it is. If this method returns true then {@link Wallet#receivePending(Transaction, java.util.List)}
|
||||||
|
* will soon be called with the transactions dependencies as well.
|
||||||
|
*/
|
||||||
|
boolean isPendingTransactionRelevant(Transaction tx) throws ScriptException {
|
||||||
|
// Ignore it if we already know about this transaction. Receiving a pending transaction never moves it
|
||||||
|
// between pools.
|
||||||
|
EnumSet<Pool> containingPools = getContainingPools(tx);
|
||||||
|
if (!containingPools.equals(EnumSet.noneOf(Pool.class))) {
|
||||||
|
log.debug("Received tx we already saw in a block or created ourselves: " + tx.getHashAsString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only care about transactions that:
|
||||||
|
// - Send us coins
|
||||||
|
// - Spend our coins
|
||||||
|
if (!isTransactionRelevant(tx)) {
|
||||||
|
log.debug("Received tx that isn't relevant to this wallet, discarding.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tx.getLockTime() > 0 && !acceptTimeLockedTransactions) {
|
||||||
|
log.warn("Received transaction {} with a lock time of {}, but not configured to accept these, discarding",
|
||||||
|
tx.getHashAsString(), tx.getLockTime());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Boilerplate that allows event listeners to delete themselves during execution, and auto locks the listener.
|
// Boilerplate that allows event listeners to delete themselves during execution, and auto locks the listener.
|
||||||
private void invokeOnCoinsReceived(final Transaction tx, final BigInteger balance, final BigInteger newBalance) {
|
private void invokeOnCoinsReceived(final Transaction tx, final BigInteger balance, final BigInteger newBalance) {
|
||||||
EventListenerInvoker.invoke(eventListeners, new EventListenerInvoker<WalletEventListener>() {
|
EventListenerInvoker.invoke(eventListeners, new EventListenerInvoker<WalletEventListener>() {
|
||||||
|
@ -119,6 +119,9 @@ public class PeerGroupTest extends TestWithPeerGroup {
|
|||||||
assertNull(outbound(p2)); // Only one peer is used to download.
|
assertNull(outbound(p2)); // Only one peer is used to download.
|
||||||
inbound(p1, t1);
|
inbound(p1, t1);
|
||||||
assertNull(outbound(p2));
|
assertNull(outbound(p2));
|
||||||
|
// Asks for dependency.
|
||||||
|
GetDataMessage getdata = (GetDataMessage) outbound(p1);
|
||||||
|
inbound(p1, new NotFoundMessage(unitTestParams, getdata.getItems()));
|
||||||
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
||||||
peerGroup.stop();
|
peerGroup.stop();
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ import org.junit.Test;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
@ -252,11 +253,13 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
inv.addItem(item);
|
inv.addItem(item);
|
||||||
inbound(peer, inv);
|
inbound(peer, inv);
|
||||||
// Peer hasn't seen it before, so will ask for it.
|
// Peer hasn't seen it before, so will ask for it.
|
||||||
|
GetDataMessage getdata = (GetDataMessage) outbound();
|
||||||
GetDataMessage message = (GetDataMessage) event.getValue().getMessage();
|
assertEquals(1, getdata.getItems().size());
|
||||||
assertEquals(1, message.getItems().size());
|
assertEquals(tx.getHash(), getdata.getItems().get(0).hash);
|
||||||
assertEquals(tx.getHash(), message.getItems().get(0).hash);
|
|
||||||
inbound(peer, tx);
|
inbound(peer, tx);
|
||||||
|
// Ask for the dependency, it's not in the mempool (in chain).
|
||||||
|
getdata = (GetDataMessage) outbound();
|
||||||
|
inbound(peer, new NotFoundMessage(unitTestParams, getdata.getItems()));
|
||||||
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -475,19 +478,15 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void recursiveDownload() throws Exception {
|
public void recursiveDownload() throws Exception {
|
||||||
// Our peer announces a normal transaction that depends on a different transaction that is time locked such
|
// Check that we can download all dependencies of an unconfirmed relevant transaction from the mempool.
|
||||||
// that it will never confirm. There's not currently any use case for doing that to us, so it's an attack.
|
ECKey to = new ECKey();
|
||||||
//
|
|
||||||
// Peer should notice this by downloading all transaction dependencies and searching for timelocked ones.
|
|
||||||
// Also, if a dependency chain is absurdly deep, the wallet shouldn't hear about it because it may just be
|
|
||||||
// a different way to achieve the same thing (a payment that will not confirm for a very long time).
|
|
||||||
control.replay();
|
control.replay();
|
||||||
connect();
|
connect();
|
||||||
|
|
||||||
final Transaction[] onTx = new Transaction[1];
|
final Transaction[] onTx = new Transaction[1];
|
||||||
peer.addEventListener(new AbstractPeerEventListener() {
|
peer.addEventListener(new AbstractPeerEventListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onTransaction(Peer peer, Transaction t) {
|
public void onTransaction(Peer peer1, Transaction t) {
|
||||||
onTx[0] = t;
|
onTx[0] = t;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -496,7 +495,6 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
// t1 -> t2 -> [t5]
|
// t1 -> t2 -> [t5]
|
||||||
// -> t3 -> t4 -> [t6]
|
// -> t3 -> t4 -> [t6]
|
||||||
// The ones in brackets are assumed to be in the chain and are represented only by hashes.
|
// The ones in brackets are assumed to be in the chain and are represented only by hashes.
|
||||||
ECKey to = new ECKey();
|
|
||||||
Transaction t2 = TestUtils.createFakeTx(unitTestParams, Utils.toNanoCoins(1, 0), to);
|
Transaction t2 = TestUtils.createFakeTx(unitTestParams, Utils.toNanoCoins(1, 0), to);
|
||||||
Sha256Hash t5 = t2.getInput(0).getOutpoint().getHash();
|
Sha256Hash t5 = t2.getInput(0).getOutpoint().getHash();
|
||||||
Transaction t4 = TestUtils.createFakeTx(unitTestParams, Utils.toNanoCoins(1, 0), new ECKey());
|
Transaction t4 = TestUtils.createFakeTx(unitTestParams, Utils.toNanoCoins(1, 0), new ECKey());
|
||||||
@ -559,6 +557,113 @@ public class PeerTest extends TestWithNetworkConnections {
|
|||||||
assertTrue(results.contains(t4));
|
assertTrue(results.contains(t4));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void timeLockedTransaction() throws Exception {
|
||||||
|
// Test that if we receive a relevant transaction that has a lock time, it doesn't result in a notification
|
||||||
|
// until we explicitly opt in to seeing those.
|
||||||
|
control.replay();
|
||||||
|
connect();
|
||||||
|
|
||||||
|
// Initial setup.
|
||||||
|
ECKey key = new ECKey();
|
||||||
|
Wallet wallet = new Wallet(unitTestParams);
|
||||||
|
wallet.addKey(key);
|
||||||
|
peer.addWallet(wallet);
|
||||||
|
final Transaction[] vtx = new Transaction[1];
|
||||||
|
wallet.addEventListener(new AbstractWalletEventListener() {
|
||||||
|
@Override
|
||||||
|
public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
|
||||||
|
vtx[0] = tx;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Send a normal relevant transaction, it's received correctly.
|
||||||
|
Transaction t1 = TestUtils.createFakeTx(unitTestParams, Utils.toNanoCoins(1, 0), key);
|
||||||
|
inbound(peer, t1);
|
||||||
|
GetDataMessage getdata = (GetDataMessage) outbound();
|
||||||
|
inbound(peer, new NotFoundMessage(unitTestParams, getdata.getItems()));
|
||||||
|
assertNotNull(vtx[0]);
|
||||||
|
vtx[0] = null;
|
||||||
|
// Send a timelocked transaction, nothing happens.
|
||||||
|
Transaction t2 = TestUtils.createFakeTx(unitTestParams, Utils.toNanoCoins(2, 0), key);
|
||||||
|
t2.setLockTime(999999);
|
||||||
|
inbound(peer, t2);
|
||||||
|
assertNull(vtx[0]);
|
||||||
|
// Now we want to hear about them. Send another, we are told about it.
|
||||||
|
wallet.setAcceptTimeLockedTransactions(true);
|
||||||
|
inbound(peer, t2);
|
||||||
|
getdata = (GetDataMessage) outbound();
|
||||||
|
inbound(peer, new NotFoundMessage(unitTestParams, getdata.getItems()));
|
||||||
|
assertEquals(t2, vtx[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rejectTimeLockedDependency() throws Exception {
|
||||||
|
// Check that we also verify the lock times of dependencies. Otherwise an attacker could still build a tx that
|
||||||
|
// looks legitimate and useful but won't actually ever confirm, by sending us a normal tx that spends a
|
||||||
|
// timelocked tx.
|
||||||
|
checkTimeLockedDependency(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void acceptTimeLockedDependency() throws Exception {
|
||||||
|
checkTimeLockedDependency(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkTimeLockedDependency(boolean shouldAccept) throws Exception {
|
||||||
|
// Initial setup.
|
||||||
|
control.replay();
|
||||||
|
connect();
|
||||||
|
ECKey key = new ECKey();
|
||||||
|
Wallet wallet = new Wallet(unitTestParams);
|
||||||
|
wallet.addKey(key);
|
||||||
|
wallet.setAcceptTimeLockedTransactions(shouldAccept);
|
||||||
|
peer.addWallet(wallet);
|
||||||
|
final Transaction[] vtx = new Transaction[1];
|
||||||
|
wallet.addEventListener(new AbstractWalletEventListener() {
|
||||||
|
@Override
|
||||||
|
public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
|
||||||
|
vtx[0] = tx;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// t1 -> t2 [locked] -> t3 (not available)
|
||||||
|
Transaction t2 = new Transaction(unitTestParams);
|
||||||
|
t2.setLockTime(999999);
|
||||||
|
// Add a fake input to t3 that goes nowhere.
|
||||||
|
Sha256Hash t3 = Sha256Hash.create("abc".getBytes(Charset.forName("UTF-8")));
|
||||||
|
t2.addInput(new TransactionInput(unitTestParams, t2, new byte[]{}, new TransactionOutPoint(unitTestParams, 0,
|
||||||
|
t3)));
|
||||||
|
t2.addOutput(Utils.toNanoCoins(1, 0), new ECKey());
|
||||||
|
Transaction t1 = new Transaction(unitTestParams);
|
||||||
|
t1.addInput(t2.getOutput(0));
|
||||||
|
t1.addOutput(Utils.toNanoCoins(1, 0), key); // Make it relevant.
|
||||||
|
// Announce t1.
|
||||||
|
InventoryMessage inv = new InventoryMessage(unitTestParams);
|
||||||
|
inv.addTransaction(t1);
|
||||||
|
inbound(peer, inv);
|
||||||
|
// Send it.
|
||||||
|
GetDataMessage getdata = (GetDataMessage) outbound();
|
||||||
|
assertEquals(t1.getHash(), getdata.getItems().get(0).hash);
|
||||||
|
inbound(peer, t1);
|
||||||
|
// Nothing arrived at our event listener yet.
|
||||||
|
assertNull(vtx[0]);
|
||||||
|
// We request t2.
|
||||||
|
getdata = (GetDataMessage) outbound();
|
||||||
|
assertEquals(t2.getHash(), getdata.getItems().get(0).hash);
|
||||||
|
inbound(peer, t2);
|
||||||
|
// We request t3.
|
||||||
|
getdata = (GetDataMessage) outbound();
|
||||||
|
assertEquals(t3, getdata.getItems().get(0).hash);
|
||||||
|
// Can't find it: bottom of tree.
|
||||||
|
NotFoundMessage notFound = new NotFoundMessage(unitTestParams);
|
||||||
|
notFound.addItem(new InventoryItem(InventoryItem.Type.Transaction, t3));
|
||||||
|
inbound(peer, notFound);
|
||||||
|
// We're done but still not notified because it was timelocked.
|
||||||
|
if (shouldAccept)
|
||||||
|
assertNotNull(vtx[0]);
|
||||||
|
else
|
||||||
|
assertNull(vtx[0]);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Use generics here to avoid unnecessary casting.
|
// TODO: Use generics here to avoid unnecessary casting.
|
||||||
private Message outbound() {
|
private Message outbound() {
|
||||||
List<DownstreamMessageEvent> messages = event.getValues();
|
List<DownstreamMessageEvent> messages = event.getValues();
|
||||||
|
@ -443,7 +443,7 @@ public class WalletTest {
|
|||||||
|
|
||||||
TestUtils.DoubleSpends doubleSpends = TestUtils.createFakeDoubleSpendTxns(params, myAddress);
|
TestUtils.DoubleSpends doubleSpends = TestUtils.createFakeDoubleSpendTxns(params, myAddress);
|
||||||
// t1 spends to our wallet. t2 double spends somewhere else.
|
// t1 spends to our wallet. t2 double spends somewhere else.
|
||||||
wallet.receivePending(doubleSpends.t1);
|
wallet.receivePending(doubleSpends.t1, null);
|
||||||
assertEquals(TransactionConfidence.ConfidenceType.NOT_SEEN_IN_CHAIN,
|
assertEquals(TransactionConfidence.ConfidenceType.NOT_SEEN_IN_CHAIN,
|
||||||
doubleSpends.t1.getConfidence().getConfidenceType());
|
doubleSpends.t1.getConfidence().getConfidenceType());
|
||||||
BlockPair bp3 = createFakeBlock(params, blockStore, doubleSpends.t2);
|
BlockPair bp3 = createFakeBlock(params, blockStore, doubleSpends.t2);
|
||||||
@ -484,12 +484,13 @@ public class WalletTest {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
wallet.receivePending(t1);
|
if (wallet.isPendingTransactionRelevant(t1))
|
||||||
|
wallet.receivePending(t1, null);
|
||||||
assertTrue(flags[0]);
|
assertTrue(flags[0]);
|
||||||
assertTrue(flags[1]); // is pending
|
assertTrue(flags[1]); // is pending
|
||||||
flags[0] = false;
|
flags[0] = false;
|
||||||
// Check we don't get notified if we receive it again.
|
// Check we don't get notified if we receive it again.
|
||||||
wallet.receivePending(t1);
|
assertFalse(wallet.isPendingTransactionRelevant(t1));
|
||||||
assertFalse(flags[0]);
|
assertFalse(flags[0]);
|
||||||
// Now check again, that we should NOT be notified when we receive it via a block (we were already notified).
|
// Now check again, that we should NOT be notified when we receive it via a block (we were already notified).
|
||||||
// However the confidence should be updated.
|
// However the confidence should be updated.
|
||||||
@ -513,7 +514,8 @@ public class WalletTest {
|
|||||||
flags[0] = false;
|
flags[0] = false;
|
||||||
flags[1] = false;
|
flags[1] = false;
|
||||||
Transaction irrelevant = createFakeTx(params, nanos, new ECKey().toAddress(params));
|
Transaction irrelevant = createFakeTx(params, nanos, new ECKey().toAddress(params));
|
||||||
wallet.receivePending(irrelevant);
|
if (wallet.isPendingTransactionRelevant(irrelevant))
|
||||||
|
wallet.receivePending(irrelevant, null);
|
||||||
assertFalse(flags[0]);
|
assertFalse(flags[0]);
|
||||||
assertEquals(2, walletChanged[0]);
|
assertEquals(2, walletChanged[0]);
|
||||||
}
|
}
|
||||||
@ -542,7 +544,8 @@ public class WalletTest {
|
|||||||
BigInteger halfNanos = Utils.toNanoCoins(0, 50);
|
BigInteger halfNanos = Utils.toNanoCoins(0, 50);
|
||||||
Transaction t2 = wallet.createSend(new ECKey().toAddress(params), halfNanos);
|
Transaction t2 = wallet.createSend(new ECKey().toAddress(params), halfNanos);
|
||||||
// Now receive it as pending.
|
// Now receive it as pending.
|
||||||
wallet.receivePending(t2);
|
if (wallet.isPendingTransactionRelevant(t2))
|
||||||
|
wallet.receivePending(t2, null);
|
||||||
// We received an onCoinsSent() callback.
|
// We received an onCoinsSent() callback.
|
||||||
assertEquals(t2, txn[0]);
|
assertEquals(t2, txn[0]);
|
||||||
assertEquals(nanos, bigints[0]);
|
assertEquals(nanos, bigints[0]);
|
||||||
@ -590,7 +593,8 @@ public class WalletTest {
|
|||||||
});
|
});
|
||||||
|
|
||||||
assertEquals(BigInteger.ZERO, wallet.getBalance());
|
assertEquals(BigInteger.ZERO, wallet.getBalance());
|
||||||
wallet.receivePending(t1);
|
if (wallet.isPendingTransactionRelevant(t1))
|
||||||
|
wallet.receivePending(t1, null);
|
||||||
assertEquals(t1, called[0]);
|
assertEquals(t1, called[0]);
|
||||||
assertEquals(nanos, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
assertEquals(nanos, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
||||||
// Now receive a double spend on the main chain.
|
// Now receive a double spend on the main chain.
|
||||||
@ -724,7 +728,8 @@ public class WalletTest {
|
|||||||
wallet.addKey(key1);
|
wallet.addKey(key1);
|
||||||
BigInteger value = toNanoCoins(5, 0);
|
BigInteger value = toNanoCoins(5, 0);
|
||||||
Transaction t1 = createFakeTx(params, value, key1);
|
Transaction t1 = createFakeTx(params, value, key1);
|
||||||
wallet.receivePending(t1);
|
if (wallet.isPendingTransactionRelevant(t1))
|
||||||
|
wallet.receivePending(t1, null);
|
||||||
// TX should have been seen as relevant.
|
// TX should have been seen as relevant.
|
||||||
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
||||||
assertEquals(BigInteger.ZERO, wallet.getBalance(Wallet.BalanceType.AVAILABLE));
|
assertEquals(BigInteger.ZERO, wallet.getBalance(Wallet.BalanceType.AVAILABLE));
|
||||||
@ -754,7 +759,8 @@ public class WalletTest {
|
|||||||
assertFalse(hash1.equals(hash2)); // File has changed.
|
assertFalse(hash1.equals(hash2)); // File has changed.
|
||||||
|
|
||||||
Transaction t1 = createFakeTx(params, toNanoCoins(5, 0), key);
|
Transaction t1 = createFakeTx(params, toNanoCoins(5, 0), key);
|
||||||
wallet.receivePending(t1);
|
if (wallet.isPendingTransactionRelevant(t1))
|
||||||
|
wallet.receivePending(t1, null);
|
||||||
Sha256Hash hash3 = Sha256Hash.hashFileContents(f);
|
Sha256Hash hash3 = Sha256Hash.hashFileContents(f);
|
||||||
assertFalse(hash2.equals(hash3)); // File has changed again.
|
assertFalse(hash2.equals(hash3)); // File has changed again.
|
||||||
|
|
||||||
@ -803,7 +809,8 @@ public class WalletTest {
|
|||||||
results[0] = results[1] = null;
|
results[0] = results[1] = null;
|
||||||
|
|
||||||
Transaction t1 = createFakeTx(params, toNanoCoins(5, 0), key);
|
Transaction t1 = createFakeTx(params, toNanoCoins(5, 0), key);
|
||||||
wallet.receivePending(t1);
|
if (wallet.isPendingTransactionRelevant(t1))
|
||||||
|
wallet.receivePending(t1, null);
|
||||||
Sha256Hash hash3 = Sha256Hash.hashFileContents(f);
|
Sha256Hash hash3 = Sha256Hash.hashFileContents(f);
|
||||||
assertTrue(hash2.equals(hash3)); // File has NOT changed.
|
assertTrue(hash2.equals(hash3)); // File has NOT changed.
|
||||||
assertNull(results[0]);
|
assertNull(results[0]);
|
||||||
|
@ -87,7 +87,7 @@ public class WalletProtobufSerializerTest {
|
|||||||
// Check that we can serialize double spends correctly, as this is a slightly tricky case.
|
// Check that we can serialize double spends correctly, as this is a slightly tricky case.
|
||||||
TestUtils.DoubleSpends doubleSpends = TestUtils.createFakeDoubleSpendTxns(params, myAddress);
|
TestUtils.DoubleSpends doubleSpends = TestUtils.createFakeDoubleSpendTxns(params, myAddress);
|
||||||
// t1 spends to our wallet.
|
// t1 spends to our wallet.
|
||||||
myWallet.receivePending(doubleSpends.t1);
|
myWallet.receivePending(doubleSpends.t1, null);
|
||||||
// t2 rolls back t1 and spends somewhere else.
|
// t2 rolls back t1 and spends somewhere else.
|
||||||
myWallet.receiveFromBlock(doubleSpends.t2, null, BlockChain.NewBlockType.BEST_CHAIN);
|
myWallet.receiveFromBlock(doubleSpends.t2, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||||
Wallet wallet1 = roundTrip(myWallet);
|
Wallet wallet1 = roundTrip(myWallet);
|
||||||
|
Loading…
Reference in New Issue
Block a user