3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-13 10:45:51 +00:00

Wallet: track UTXOs explicitly and use that to try and accelerate Bloom filtering for large wallets.

This commit is contained in:
Mike Hearn 2015-03-12 10:45:01 -07:00
parent ed3ef7d15e
commit 40ee90cc0c
3 changed files with 103 additions and 58 deletions

View File

@ -402,27 +402,6 @@ public class Transaction extends ChildMessage implements Serializable {
return fee; return fee;
} }
boolean disconnectInputs() {
boolean disconnected = false;
maybeParse();
for (TransactionInput input : inputs) {
disconnected |= input.disconnect();
}
return disconnected;
}
/**
* Returns true if every output is marked as spent.
*/
public boolean isEveryOutputSpent() {
maybeParse();
for (TransactionOutput output : outputs) {
if (output.isAvailableForSpending())
return false;
}
return true;
}
/** /**
* Returns true if any of the outputs is marked as spent. * Returns true if any of the outputs is marked as spent.
*/ */

View File

@ -138,6 +138,10 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
// All transactions together. // All transactions together.
protected final Map<Sha256Hash, Transaction> transactions; protected final Map<Sha256Hash, Transaction> transactions;
// All the TransactionOutput objects that we could spend (ignoring whether we have the private key or not).
// Used to speed up various calculations.
protected final HashSet<TransactionOutput> myUnspents = Sets.newHashSet();
// Transactions that were dropped by the risk analysis system. These are not in any pools and not serialized // Transactions that were dropped by the risk analysis system. These are not in any pools and not serialized
// to disk. We have to keep them around because if we ignore a tx because we think it will never confirm, but // to disk. We have to keep them around because if we ignore a tx because we think it will never confirm, but
// then it actually does confirm and does so within the same network session, remote peers will not resend us // then it actually does confirm and does so within the same network session, remote peers will not resend us
@ -1781,7 +1785,10 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
// re-connected by processTxFromBestChain below. // re-connected by processTxFromBestChain below.
for (TransactionOutput output : tx.getOutputs()) { for (TransactionOutput output : tx.getOutputs()) {
final TransactionInput spentBy = output.getSpentBy(); final TransactionInput spentBy = output.getSpentBy();
if (spentBy != null) spentBy.disconnect(); if (spentBy != null) {
checkState(myUnspents.add(output));
spentBy.disconnect();
}
} }
} }
processTxFromBestChain(tx, wasPending); processTxFromBestChain(tx, wasPending);
@ -2003,6 +2010,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
} }
} }
TransactionOutput output = checkNotNull(input.getConnectedOutput());
if (result == TransactionInput.ConnectionResult.ALREADY_SPENT) { if (result == TransactionInput.ConnectionResult.ALREADY_SPENT) {
if (fromChain) { if (fromChain) {
// Can be: // Can be:
@ -2018,7 +2026,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
log.warn("Saw two pending transactions double spend each other"); log.warn("Saw two pending transactions double spend each other");
log.warn(" offending input is input {}", tx.getInputs().indexOf(input)); log.warn(" offending input is input {}", tx.getInputs().indexOf(input));
log.warn("{}: {}", tx.getHash(), Utils.HEX.encode(tx.unsafeBitcoinSerialize())); log.warn("{}: {}", tx.getHash(), Utils.HEX.encode(tx.unsafeBitcoinSerialize()));
Transaction other = input.getConnectedOutput().getSpentBy().getParentTransaction(); Transaction other = output.getSpentBy().getParentTransaction();
log.warn("{}: {}", other.getHash(), Utils.HEX.encode(tx.unsafeBitcoinSerialize())); log.warn("{}: {}", other.getHash(), Utils.HEX.encode(tx.unsafeBitcoinSerialize()));
} }
} else if (result == TransactionInput.ConnectionResult.SUCCESS) { } else if (result == TransactionInput.ConnectionResult.SUCCESS) {
@ -2028,6 +2036,10 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
Transaction connected = checkNotNull(input.getOutpoint().fromTx); Transaction connected = checkNotNull(input.getOutpoint().fromTx);
log.info(" marked {} as spent", input.getOutpoint()); log.info(" marked {} as spent", input.getOutpoint());
maybeMovePool(connected, "prevtx"); maybeMovePool(connected, "prevtx");
// Just because it's connected doesn't mean it's actually ours: sometimes we have total visibility.
if (output.isMineOrWatched(this)) {
checkState(myUnspents.remove(output));
}
} }
} }
// Now check each output and see if there is a pending transaction which spends it. This shouldn't normally // Now check each output and see if there is a pending transaction which spends it. This shouldn't normally
@ -2047,6 +2059,10 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
if (result == TransactionInput.ConnectionResult.SUCCESS) { if (result == TransactionInput.ConnectionResult.SUCCESS) {
log.info("Connected pending tx input {}:{}", log.info("Connected pending tx input {}:{}",
pendingTx.getHashAsString(), pendingTx.getInputs().indexOf(input)); pendingTx.getHashAsString(), pendingTx.getInputs().indexOf(input));
// The unspents map might not have it if we never saw this tx until it was included in the chain
// and thus becomes spent the moment we become aware of it.
if (myUnspents.remove(input.getConnectedOutput()))
log.info("Removed from UNSPENTS: {}", input.getConnectedOutput());
} }
} }
} }
@ -2074,6 +2090,8 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
for (TransactionInput deadInput : tx.getInputs()) { for (TransactionInput deadInput : tx.getInputs()) {
Transaction connected = deadInput.getOutpoint().fromTx; Transaction connected = deadInput.getOutpoint().fromTx;
if (connected == null) continue; if (connected == null) continue;
checkState(myUnspents.add(deadInput.getConnectedOutput()));
log.info("Adding to UNSPENTS: {}", deadInput.getConnectedOutput());
deadInput.disconnect(); deadInput.disconnect();
maybeMovePool(connected, "kill"); maybeMovePool(connected, "kill");
} }
@ -2095,10 +2113,14 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
TransactionInput.ConnectionResult result = input.connect(unspent, TransactionInput.ConnectMode.DISCONNECT_ON_CONFLICT); TransactionInput.ConnectionResult result = input.connect(unspent, TransactionInput.ConnectMode.DISCONNECT_ON_CONFLICT);
if (result == TransactionInput.ConnectionResult.SUCCESS) { if (result == TransactionInput.ConnectionResult.SUCCESS) {
maybeMovePool(input.getOutpoint().fromTx, "kill"); maybeMovePool(input.getOutpoint().fromTx, "kill");
myUnspents.add(input.getConnectedOutput());
log.info("Adding to UNSPENTS: {}", input.getConnectedOutput());
} else { } else {
result = input.connect(spent, TransactionInput.ConnectMode.DISCONNECT_ON_CONFLICT); result = input.connect(spent, TransactionInput.ConnectMode.DISCONNECT_ON_CONFLICT);
if (result == TransactionInput.ConnectionResult.SUCCESS) { if (result == TransactionInput.ConnectionResult.SUCCESS) {
maybeMovePool(input.getOutpoint().fromTx, "kill"); maybeMovePool(input.getOutpoint().fromTx, "kill");
myUnspents.add(input.getConnectedOutput());
log.info("Adding to UNSPENTS: {}", input.getConnectedOutput());
} }
} }
} }
@ -2142,6 +2164,12 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
log.info("commitTx of {}", tx.getHashAsString()); log.info("commitTx of {}", tx.getHashAsString());
Coin balance = getBalance(); Coin balance = getBalance();
tx.setUpdateTime(Utils.now()); tx.setUpdateTime(Utils.now());
// Put any outputs that are sending money back to us into the unspents map, and calculate their total value.
Coin valueSentToMe = Coin.ZERO;
for (TransactionOutput o : tx.getOutputs()) {
if (!o.isMineOrWatched(this)) continue;
valueSentToMe = valueSentToMe.add(o.getValue());
}
// Mark the outputs we're spending as spent so we won't try and use them in future creations. This will also // Mark the outputs we're spending as spent so we won't try and use them in future creations. This will also
// move any transactions that are now fully spent to the spent map so we can skip them when creating future // move any transactions that are now fully spent to the spent map so we can skip them when creating future
// spends. // spends.
@ -2157,7 +2185,6 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
markKeysAsUsed(tx); markKeysAsUsed(tx);
try { try {
Coin valueSentFromMe = tx.getValueSentFromMe(this); Coin valueSentFromMe = tx.getValueSentFromMe(this);
Coin valueSentToMe = tx.getValueSentToMe(this);
Coin newBalance = balance.add(valueSentToMe).subtract(valueSentFromMe); Coin newBalance = balance.add(valueSentToMe).subtract(valueSentFromMe);
if (valueSentToMe.signum() > 0) { if (valueSentToMe.signum() > 0) {
checkBalanceFuturesLocked(null); checkBalanceFuturesLocked(null);
@ -2394,6 +2421,12 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
default: default:
throw new RuntimeException("Unknown wallet transaction type " + pool); throw new RuntimeException("Unknown wallet transaction type " + pool);
} }
if (pool == Pool.UNSPENT || pool == Pool.PENDING) {
for (TransactionOutput output : tx.getOutputs()) {
if (output.isAvailableForSpending() && output.isMineOrWatched(this))
myUnspents.add(output);
}
}
// This is safe even if the listener has been added before, as TransactionConfidence ignores duplicate // This is safe even if the listener has been added before, as TransactionConfidence ignores duplicate
// registration requests. That makes the code in the wallet simpler. // registration requests. That makes the code in the wallet simpler.
tx.getConfidence().addEventListener(txConfidenceListener, Threading.SAME_THREAD); tx.getConfidence().addEventListener(txConfidenceListener, Threading.SAME_THREAD);
@ -2561,7 +2594,16 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
if (isTransactionRisky(tx, null) && !acceptRiskyTransactions) { if (isTransactionRisky(tx, null) && !acceptRiskyTransactions) {
log.debug("Found risky transaction {} in wallet during cleanup.", tx.getHashAsString()); log.debug("Found risky transaction {} in wallet during cleanup.", tx.getHashAsString());
if (!tx.isAnyOutputSpent()) { if (!tx.isAnyOutputSpent()) {
tx.disconnectInputs(); // Sync myUnspents with the change.
for (TransactionInput input : tx.getInputs()) {
TransactionOutput output = input.getConnectedOutput();
if (output == null) continue;
myUnspents.add(output);
input.disconnect();
}
for (TransactionOutput output : tx.getOutputs())
myUnspents.remove(output);
i.remove(); i.remove();
transactions.remove(tx.getHash()); transactions.remove(tx.getHash());
dirty = true; dirty = true;
@ -2624,6 +2666,16 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
} }
} }
/** Returns a copy of the internal unspent outputs list */
List<TransactionOutput> getUnspents() {
lock.lock();
try {
return new ArrayList<TransactionOutput>(myUnspents);
} finally {
lock.unlock();
}
}
@Override @Override
public String toString() { public String toString() {
return toString(false, true, true, null); return toString(false, true, true, null);
@ -3994,9 +4046,12 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
} else { } else {
for (TransactionOutput output : tx.getOutputs()) { for (TransactionOutput output : tx.getOutputs()) {
TransactionInput input = output.getSpentBy(); TransactionInput input = output.getSpentBy();
if (input != null) input.disconnect(); if (input != null) {
if (output.isMineOrWatched(this))
checkState(myUnspents.add(output));
input.disconnect();
}
} }
tx.disconnectInputs();
oldChainTxns.add(tx); oldChainTxns.add(tx);
unspent.remove(txHash); unspent.remove(txHash);
spent.remove(txHash); spent.remove(txHash);
@ -4086,14 +4141,36 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
//region Bloom filtering //region Bloom filtering
// keychainLock isn't semantically correct but happens to be the most convenient to use.
@GuardedBy("keychainLock") private ArrayList<TransactionOutPoint> bloomOutPoints = new ArrayList<TransactionOutPoint>();
@Override @Override
public void beginBloomFilterCalculation() { public void beginBloomFilterCalculation() {
lock.lock(); lock.lock();
keychainLock.lock(); keychainLock.lock();
calcBloomOutPoints();
} }
@Override @GuardedBy("keychainLock")
private void calcBloomOutPoints() {
// TODO: This could be done once and then kept up to date.
bloomOutPoints.clear();
for (Transaction tx : getTransactions(false)) {
for (TransactionOutput out : tx.getOutputs()) {
try {
if (isTxOutputBloomFilterable(out))
bloomOutPoints.add(out.getOutPointFor());
} catch (ScriptException e) {
// If it is ours, we parsed the script correctly, so this shouldn't happen.
throw new RuntimeException(e);
}
}
}
}
@Override @GuardedBy("keychainLock")
public void endBloomFilterCalculation() { public void endBloomFilterCalculation() {
bloomOutPoints.clear();
keychainLock.unlock(); keychainLock.unlock();
lock.unlock(); lock.unlock();
} }
@ -4104,20 +4181,11 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
*/ */
@Override @Override
public int getBloomFilterElementCount() { public int getBloomFilterElementCount() {
int size = 0;
for (Transaction tx : getTransactions(false)) {
for (TransactionOutput out : tx.getOutputs()) {
try {
if (isTxOutputBloomFilterable(out))
size++;
} catch (ScriptException e) {
// If it is ours, we parsed the script correctly, so this shouldn't happen.
throw new RuntimeException(e);
}
}
}
keychainLock.lock(); keychainLock.lock();
try { try {
if (bloomOutPoints.isEmpty())
calcBloomOutPoints();
int size = bloomOutPoints.size();
size += keychain.getBloomFilterElementCount(); size += keychain.getBloomFilterElementCount();
// Some scripts may have more than one bloom element. That should normally be okay, because under-counting // Some scripts may have more than one bloom element. That should normally be okay, because under-counting
// just increases false-positive rate. // just increases false-positive rate.
@ -4183,19 +4251,10 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
} }
} }
} }
for (Transaction tx : getTransactions(false)) { if (bloomOutPoints.isEmpty())
for (int i = 0; i < tx.getOutputs().size(); i++) { calcBloomOutPoints();
TransactionOutput out = tx.getOutputs().get(i); for (TransactionOutPoint point : bloomOutPoints)
try { filter.insert(point.bitcoinSerialize());
if (isTxOutputBloomFilterable(out)) {
TransactionOutPoint outPoint = new TransactionOutPoint(params, i, tx);
filter.insert(outPoint.bitcoinSerialize());
}
} catch (ScriptException e) {
throw new RuntimeException(e); // If it is ours, we parsed the script correctly, so this shouldn't happen
}
}
}
return filter; return filter;
} finally { } finally {
keychainLock.unlock(); keychainLock.unlock();
@ -4203,10 +4262,11 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
} }
} }
// Returns true if the output is one that won't be selected by a data element matching in the scriptSig.
private boolean isTxOutputBloomFilterable(TransactionOutput out) { private boolean isTxOutputBloomFilterable(TransactionOutput out) {
boolean isScriptTypeSupported = out.getScriptPubKey().isSentToRawPubKey() || out.getScriptPubKey().isPayToScriptHash(); Script script = out.getScriptPubKey();
return (out.isMine(this) && isScriptTypeSupported) || boolean isScriptTypeSupported = script.isSentToRawPubKey() || script.isPayToScriptHash();
out.isWatched(this); return (isScriptTypeSupported && myUnspents.contains(out)) || watchedScripts.contains(script);
} }
/** /**

View File

@ -352,11 +352,16 @@ public class WalletTest extends TestWithWallet {
basicSanityChecks(wallet, t2, destination); basicSanityChecks(wallet, t2, destination);
// Broadcast the transaction and commit. // Broadcast the transaction and commit.
List<TransactionOutput> unspents1 = wallet.getUnspents();
assertEquals(1, unspents1.size());
broadcastAndCommit(wallet, t2); broadcastAndCommit(wallet, t2);
List<TransactionOutput> unspents2 = wallet.getUnspents();
assertNotEquals(unspents1, unspents2.size());
// Now check that we can spend the unconfirmed change, with a new change address of our own selection. // Now check that we can spend the unconfirmed change, with a new change address of our own selection.
// (req.aesKey is null for unencrypted / the correct aesKey for encrypted.) // (req.aesKey is null for unencrypted / the correct aesKey for encrypted.)
spendUnconfirmedChange(wallet, t2, req.aesKey); wallet = spendUnconfirmedChange(wallet, t2, req.aesKey);
assertNotEquals(unspents2, wallet.getUnspents());
} }
private void receiveATransaction(Wallet wallet, Address toAddress) throws Exception { private void receiveATransaction(Wallet wallet, Address toAddress) throws Exception {
@ -422,7 +427,7 @@ public class WalletTest extends TestWithWallet {
assertEquals(1, txns.size()); assertEquals(1, txns.size());
} }
private void spendUnconfirmedChange(Wallet wallet, Transaction t2, KeyParameter aesKey) throws Exception { private Wallet spendUnconfirmedChange(Wallet wallet, Transaction t2, KeyParameter aesKey) throws Exception {
if (wallet.getTransactionSigners().size() == 1) // don't bother reconfiguring the p2sh wallet if (wallet.getTransactionSigners().size() == 1) // don't bother reconfiguring the p2sh wallet
wallet = roundTrip(wallet); wallet = roundTrip(wallet);
Coin v3 = valueOf(0, 49); Coin v3 = valueOf(0, 49);
@ -444,6 +449,7 @@ public class WalletTest extends TestWithWallet {
wallet.receiveFromBlock(t3, bp.storedBlock, AbstractBlockChain.NewBlockType.BEST_CHAIN, 1); wallet.receiveFromBlock(t3, bp.storedBlock, AbstractBlockChain.NewBlockType.BEST_CHAIN, 1);
wallet.notifyNewBestBlock(bp.storedBlock); wallet.notifyNewBestBlock(bp.storedBlock);
assertTrue(wallet.isConsistent()); assertTrue(wallet.isConsistent());
return wallet;
} }
@Test @Test