Support watching of scripts/addresses in wallet

This commit is contained in:
Devrandom 2013-11-13 11:36:42 -08:00 committed by Mike Hearn
parent 2271e7198e
commit da2e3e6c98
15 changed files with 1451 additions and 120 deletions

View File

@ -77,6 +77,14 @@ message Key {
optional int64 creation_timestamp = 5;
}
message Script {
required bytes program = 1;
// Timestamp stored as millis since epoch. Useful for skipping block bodies before this point
// when watching for scripts on the blockchain.
required int64 creation_timestamp = 2;
}
message TransactionInput {
// Hash of the transaction this input is using.
required bytes transaction_out_point_hash = 1;
@ -257,6 +265,7 @@ message Wallet {
repeated Key key = 3;
repeated Transaction transaction = 4;
repeated Script watched_script = 15;
optional EncryptionType encryption_type = 5 [default=UNENCRYPTED];
optional ScryptParameters encryption_parameters = 6;
@ -280,5 +289,5 @@ message Wallet {
// can be used to recover a compromised wallet, or just as part of preventative defence-in-depth measures.
optional uint64 key_rotation_time = 13;
// Next tag: 15
// Next tag: 16
}

View File

@ -16,6 +16,8 @@
package com.google.bitcoin.core;
import com.google.bitcoin.script.Script;
import java.math.BigInteger;
import java.util.List;
@ -48,6 +50,11 @@ public abstract class AbstractWalletEventListener implements WalletEventListener
onChange();
}
@Override
public void onScriptsAdded(Wallet wallet, List<Script> scripts) {
onChange();
}
@Override
public void onWalletChanged(Wallet wallet) {
onChange();

View File

@ -40,4 +40,6 @@ public interface PeerFilterProvider {
* Default value should be an empty bloom filter with the given size, falsePositiveRate, and nTweak.
*/
public BloomFilter getBloomFilter(int size, double falsePositiveRate, long nTweak);
boolean isRequiringUpdateAllBloomFilter();
}

View File

@ -20,6 +20,7 @@ package com.google.bitcoin.core;
import com.google.bitcoin.core.Peer.PeerHandler;
import com.google.bitcoin.discovery.PeerDiscovery;
import com.google.bitcoin.discovery.PeerDiscoveryException;
import com.google.bitcoin.script.Script;
import com.google.bitcoin.utils.ListenerRegistration;
import com.google.bitcoin.utils.Threading;
import com.google.common.base.Preconditions;
@ -131,6 +132,7 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
private void onChanged() {
recalculateFastCatchupAndFilter();
}
@Override public void onScriptsAdded(Wallet wallet, List<Script> scripts) { onChanged(); }
@Override public void onKeysAdded(Wallet wallet, List<ECKey> keys) { onChanged(); }
@Override public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { onChanged(); }
@Override public void onCoinsSent(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { onChanged(); }
@ -678,9 +680,11 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
return;
long earliestKeyTimeSecs = Long.MAX_VALUE;
int elements = 0;
boolean requiresUpdateAll = false;
for (PeerFilterProvider p : peerFilterProviders) {
earliestKeyTimeSecs = Math.min(earliestKeyTimeSecs, p.getEarliestKeyCreationTime());
elements += p.getBloomFilterElementCount();
requiresUpdateAll = requiresUpdateAll || p.isRequiringUpdateAllBloomFilter();
}
if (elements > 0) {
@ -689,7 +693,9 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
// The constant 100 here is somewhat arbitrary, but makes sense for small to medium wallets -
// it will likely mean we never need to create a filter with different parameters.
lastBloomFilterElementCount = elements > lastBloomFilterElementCount ? elements + 100 : lastBloomFilterElementCount;
BloomFilter filter = new BloomFilter(lastBloomFilterElementCount, bloomFilterFPRate, bloomFilterTweak);
BloomFilter.BloomUpdate bloomFlags =
requiresUpdateAll ? BloomFilter.BloomUpdate.UPDATE_ALL : BloomFilter.BloomUpdate.UPDATE_P2PUBKEY_ONLY;
BloomFilter filter = new BloomFilter(lastBloomFilterElementCount, bloomFilterFPRate, bloomFilterTweak, bloomFlags);
for (PeerFilterProvider p : peerFilterProviders)
filter.merge(p.getBloomFilter(lastBloomFilterElementCount, bloomFilterFPRate, bloomFilterTweak));
if (!filter.equals(bloomFilter)) {

View File

@ -219,7 +219,7 @@ public class Transaction extends ChildMessage implements Serializable {
// This is tested in WalletTest.
BigInteger v = BigInteger.ZERO;
for (TransactionOutput o : outputs) {
if (!o.isMine(wallet)) continue;
if (!o.isMineOrWatched(wallet)) continue;
if (!includeSpent && !o.isAvailableForSpending()) continue;
v = v.add(o.getValue());
}
@ -234,7 +234,7 @@ public class Transaction extends ChildMessage implements Serializable {
boolean isActuallySpent = true;
for (TransactionOutput o : outputs) {
if (o.isAvailableForSpending()) {
if (o.isMine(wallet)) isActuallySpent = false;
if (o.isMineOrWatched(wallet)) isActuallySpent = false;
if (o.getSpentBy() != null) {
log.error("isAvailableForSpending != spentBy");
return false;
@ -340,7 +340,7 @@ public class Transaction extends ChildMessage implements Serializable {
continue;
// The connected output may be the change to the sender of a previous input sent to this wallet. In this
// case we ignore it.
if (!connected.isMine(wallet))
if (!connected.isMineOrWatched(wallet))
continue;
v = v.add(connected.getValue());
}
@ -405,7 +405,7 @@ public class Transaction extends ChildMessage implements Serializable {
public boolean isEveryOwnedOutputSpent(Wallet wallet) {
maybeParse();
for (TransactionOutput output : outputs) {
if (output.isAvailableForSpending() && output.isMine(wallet))
if (output.isAvailableForSpending() && output.isMineOrWatched(wallet))
return false;
}
return true;

View File

@ -250,6 +250,27 @@ public class TransactionOutput extends ChildMessage implements Serializable {
return scriptBytes;
}
/**
* Returns true if this output is to a key in the wallet or to an address/script we are watching.
*/
public boolean isMineOrWatched(Wallet wallet) {
return isMine(wallet) || isWatched(wallet);
}
/**
* Returns true if this output is to a key, or an address we have the keys for, in the wallet.
*/
public boolean isWatched(Wallet wallet) {
try {
Script script = getScriptPubKey();
return wallet.isWatchedScript(script);
} catch (ScriptException e) {
// Just means we didn't understand the output of this transaction: ignore it.
log.debug("Could not parse tx output script: {}", e.toString());
return false;
}
}
/**
* Returns true if this output is to a key, or an address we have the keys for, in the wallet.
*/

View File

@ -21,6 +21,9 @@ import com.google.bitcoin.core.WalletTransaction.Pool;
import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterException;
import com.google.bitcoin.crypto.KeyCrypterScrypt;
import com.google.bitcoin.script.Script;
import com.google.bitcoin.script.ScriptBuilder;
import com.google.bitcoin.script.ScriptChunk;
import com.google.bitcoin.store.UnreadableWalletException;
import com.google.bitcoin.store.WalletProtobufSerializer;
import com.google.bitcoin.utils.ListenerRegistration;
@ -94,6 +97,7 @@ import static com.google.common.base.Preconditions.*;
public class Wallet implements Serializable, BlockChainListener, PeerFilterProvider {
private static final Logger log = LoggerFactory.getLogger(Wallet.class);
private static final long serialVersionUID = 2L;
private static final int MINIMUM_BLOOM_DATA_LENGTH = 8;
protected final ReentrantLock lock = Threading.lock("wallet");
@ -127,6 +131,9 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
// A list of public/private EC keys owned by this user. Access it using addKey[s], hasKey[s] and findPubKeyFromHash.
private ArrayList<ECKey> keychain;
// A list of scripts watched by this wallet.
private Set<Script> watchedScripts;
private final NetworkParameters params;
private Sha256Hash lastBlockSeenHash;
@ -192,6 +199,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
this.keyCrypter = keyCrypter;
this.params = checkNotNull(params);
keychain = new ArrayList<ECKey>();
watchedScripts = Sets.newHashSet();
unspent = new HashMap<Sha256Hash, Transaction>();
spent = new HashMap<Sha256Hash, Transaction>();
pending = new HashMap<Sha256Hash, Transaction>();
@ -245,6 +253,18 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
}
}
/**
* Returns a snapshot of the watched scripts. This view is not live.
*/
public List<Script> getWatchedScripts() {
lock.lock();
try {
return new ArrayList<Script>(watchedScripts);
} finally {
lock.unlock();
}
}
/**
* Removes the given key from the keychain. Be very careful with this - losing a private key <b>destroys the
* money associated with it</b>.
@ -1922,6 +1942,29 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
}
}
public LinkedList<TransactionOutput> getWatchedOutputs(boolean excludeImmatureCoinbases) {
lock.lock();
try {
LinkedList<TransactionOutput> candidates = Lists.newLinkedList();
for (Transaction tx : Iterables.concat(unspent.values(), pending.values())) {
if (excludeImmatureCoinbases && !tx.isMature()) continue;
for (TransactionOutput output : tx.getOutputs()) {
if (!output.isAvailableForSpending()) continue;
try {
Script scriptPubKey = output.getScriptPubKey();
if (!watchedScripts.contains(scriptPubKey)) continue;
candidates.add(output);
} catch (ScriptException e) {
// Ignore
}
}
}
return candidates;
} finally {
lock.unlock();
}
}
/** Returns the address used for change outputs. Note: this will probably go away in future. */
public Address getChangeAddress() {
lock.lock();
@ -1980,6 +2023,75 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
}
}
/**
* Return true if we are watching this address.
*/
public boolean isAddressWatched(Address address) {
Script script = ScriptBuilder.createOutputScript(address);
return isWatchedScript(script);
}
/** See {@link #addWatchedAddress(Address, long)} */
public boolean addWatchedAddress(final Address address) {
long now = Utils.now().getTime() / 1000;
return addWatchedAddresses(Lists.newArrayList(address), now) == 1;
}
/**
* Adds the given address to the wallet to be watched. Outputs can be retrieved
* by {@link #getWatchedOutputs(boolean)}.
*
* @param creationTime creation time in seconds since the epoch, for scanning the blockchain
*
* @return whether the address was added successfully (not already present)
*/
public boolean addWatchedAddress(final Address address, long creationTime) {
return addWatchedAddresses(Lists.newArrayList(address), creationTime) == 1;
}
/**
* Adds the given address to the wallet to be watched. Outputs can be retrieved
* by {@link #getWatchedOutputs(boolean)}.
*
* @return how many addresses were added successfully
*/
public int addWatchedAddresses(final List<Address> addresses, long creationTime) {
List<Script> scripts = Lists.newArrayList();
for (Address address : addresses) {
Script script = ScriptBuilder.createOutputScript(address);
script.setCreationTimeSeconds(creationTime);
scripts.add(script);
}
return addWatchedScripts(scripts);
}
/**
* Adds the given output scripts to the wallet to be watched. Outputs can be retrieved
* by {@link #getWatchedOutputs(boolean)}.
*
* @return how many scripts were added successfully
*/
public int addWatchedScripts(final List<Script> scripts) {
lock.lock();
try {
int added = 0;
for (final Script script : scripts) {
if (watchedScripts.contains(script)) continue;
watchedScripts.add(script);
added++;
}
queueOnScriptsAdded(scripts);
saveNow();
return added;
} finally {
lock.unlock();
}
}
/**
* Locates a keypair from the keychain given the hash of the public key. This is needed when finding out which
* key we need to use to redeem a transaction output.
@ -2015,6 +2127,16 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
return findKeyFromPubHash(pubkeyHash) != null;
}
/** Returns true if this wallet is watching transactions for outputs with the script. */
public boolean isWatchedScript(Script script) {
lock.lock();
try {
return watchedScripts.contains(script);
} finally {
lock.unlock();
}
}
/**
* Locates a keypair from the keychain given the raw public key bytes.
* @return ECKey or null if no such key was found.
@ -2107,6 +2229,27 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
}
}
/** Returns the available balance, including any unspent balance at watched addresses */
public BigInteger getWatchedBalance() {
return getWatchedBalance(coinSelector);
}
/**
* Returns the balance that would be considered spendable by the given coin selector, including
* any unspent balance at watched addresses.
*/
public BigInteger getWatchedBalance(CoinSelector selector) {
lock.lock();
try {
checkNotNull(selector);
LinkedList<TransactionOutput> candidates = getWatchedOutputs(true);
CoinSelection selection = selector.select(NetworkParameters.MAX_MONEY, candidates);
return selection.valueGathered;
} finally {
lock.unlock();
}
}
@Override
public String toString() {
return toString(false, true, true, null);
@ -2395,8 +2538,8 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
}
/**
* Returns the earliest creation time of the keys in this wallet, in seconds since the epoch, ie the min of
* {@link com.google.bitcoin.core.ECKey#getCreationTimeSeconds()}. This can return zero if at least one key does
* Returns the earliest creation time of keys or watched scripts in this wallet, in seconds since the epoch, ie the min
* of {@link com.google.bitcoin.core.ECKey#getCreationTimeSeconds()}. This can return zero if at least one key does
* not have that data (was created before key timestamping was implemented). <p>
*
* This method is most often used in conjunction with {@link PeerGroup#setFastCatchupTimeSecs(long)} in order to
@ -2410,13 +2553,13 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
public long getEarliestKeyCreationTime() {
lock.lock();
try {
if (keychain.size() == 0) {
return Utils.now().getTime() / 1000;
}
long earliestTime = Long.MAX_VALUE;
for (ECKey key : keychain) {
for (ECKey key : keychain)
earliestTime = Math.min(key.getCreationTimeSeconds(), earliestTime);
}
for (Script script : watchedScripts)
earliestTime = Math.min(script.getCreationTimeSeconds(), earliestTime);
if (earliestTime == Long.MAX_VALUE)
return Utils.now().getTime() / 1000;
return earliestTime;
} finally {
lock.unlock();
@ -2805,9 +2948,24 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
}
}
}
// Some scripts may have more than one bloom element. That should normally be okay,
// because under-counting just increases false-positive rate.
size += watchedScripts.size();
return size;
}
/**
* If we are watching any scripts, the bloom filter must update on peers whenever an output is
* identified. This is because we don't necessarily have the associated pubkey, so we can't
* watch for it on spending transactions.
*/
@Override
public boolean isRequiringUpdateAllBloomFilter() {
return !watchedScripts.isEmpty();
}
/**
* Gets a bloom filter that contains all of the public keys from this wallet, and which will provide the given
* false-positive rate. See the docs for {@link BloomFilter} for a brief explanation of anonymity when using filters.
@ -2815,7 +2973,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
public BloomFilter getBloomFilter(double falsePositiveRate) {
return getBloomFilter(getBloomFilterElementCount(), falsePositiveRate, (long)(Math.random()*Long.MAX_VALUE));
}
/**
* Gets a bloom filter that contains all of the public keys from this wallet,
* and which will provide the given false-positive rate if it has size elements.
@ -2835,6 +2993,17 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
filter.insert(key.getPubKey());
filter.insert(key.getPubKeyHash());
}
for (Script script : watchedScripts) {
for (ScriptChunk chunk : script.getChunks()) {
// Only add long (at least 64 bit) data to the bloom filter.
// If any long constants become popular in scripts, we will need logic
// here to exclude them.
if (!chunk.isOpCode() && chunk.data.length >= MINIMUM_BLOOM_DATA_LENGTH) {
filter.insert(chunk.data);
}
}
}
} finally {
lock.unlock();
}
@ -2842,7 +3011,8 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
for (int i = 0; i < tx.getOutputs().size(); i++) {
TransactionOutput out = tx.getOutputs().get(i);
try {
if (out.isMine(this) && out.getScriptPubKey().isSentToRawPubKey()) {
if ((out.isMine(this) && out.getScriptPubKey().isSentToRawPubKey()) ||
out.isWatched(this)) {
TransactionOutPoint outPoint = new TransactionOutPoint(params, i, tx);
filter.insert(outPoint.bitcoinSerialize());
}
@ -2851,6 +3021,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
}
}
}
return filter;
}
@ -3110,6 +3281,18 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
}
}
private void queueOnScriptsAdded(final List<Script> scripts) {
checkState(lock.isHeldByCurrentThread());
for (final ListenerRegistration<WalletEventListener> registration : eventListeners) {
registration.executor.execute(new Runnable() {
@Override
public void run() {
registration.listener.onScriptsAdded(Wallet.this, scripts);
}
});
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Fee calculation code.

View File

@ -16,6 +16,8 @@
package com.google.bitcoin.core;
import com.google.bitcoin.script.Script;
import java.math.BigInteger;
import java.util.List;
@ -119,4 +121,7 @@ public interface WalletEventListener {
* or due to some other automatic derivation.
*/
void onKeysAdded(Wallet wallet, List<ECKey> keys);
/** Called whenever a new watched script is added to the wallet. */
void onScriptsAdded(Wallet wallet, List<Script> scripts);
}

View File

@ -20,6 +20,7 @@ import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.Transaction;
import com.google.bitcoin.core.Wallet;
import com.google.bitcoin.core.WalletEventListener;
import com.google.bitcoin.script.Script;
import java.math.BigInteger;
import java.util.List;
@ -49,4 +50,7 @@ public class NativeWalletEventListener implements WalletEventListener {
@Override
public native void onKeysAdded(Wallet wallet, List<ECKey> keys);
@Override
public native void onScriptsAdded(Wallet wallet, List<Script> scripts);
}

View File

@ -61,6 +61,9 @@ public class Script {
// must preserve the exact bytes that we read off the wire, along with the parsed form.
protected byte[] program;
// Creation time of the associated keys in seconds since the epoch.
private long creationTimeSeconds;
/** Creates an empty script that serializes to nothing. */
private Script() {
chunks = Lists.newArrayList();
@ -69,6 +72,7 @@ public class Script {
// Used from ScriptBuilder.
Script(List<ScriptChunk> chunks) {
this.chunks = Collections.unmodifiableList(new ArrayList<ScriptChunk>(chunks));
creationTimeSeconds = Utils.now().getTime() / 1000;
}
/**
@ -79,6 +83,21 @@ public class Script {
public Script(byte[] programBytes) throws ScriptException {
program = programBytes;
parse(programBytes);
creationTimeSeconds = Utils.now().getTime() / 1000;
}
public Script(byte[] programBytes, long creationTimeSeconds) throws ScriptException {
program = programBytes;
parse(programBytes);
this.creationTimeSeconds = creationTimeSeconds;
}
public long getCreationTimeSeconds() {
return creationTimeSeconds;
}
public void setCreationTimeSeconds(long creationTimeSeconds) {
this.creationTimeSeconds = creationTimeSeconds;
}
/**
@ -97,6 +116,11 @@ public class Script {
buf.append("] ");
}
}
if (creationTimeSeconds != 0) {
buf.append(" timestamp:").append(creationTimeSeconds);
}
return buf.toString();
}
@ -110,7 +134,8 @@ public class Script {
for (ScriptChunk chunk : chunks) {
chunk.write(bos);
}
return bos.toByteArray();
program = bos.toByteArray();
return program;
} catch (IOException e) {
throw new RuntimeException(e); // Cannot happen.
}
@ -1241,4 +1266,25 @@ public class Script {
throw new ScriptException("P2SH script execution resulted in a non-true stack");
}
}
// Utility that doesn't copy for internal use
private byte[] getQuickProgram() {
if (program != null)
return program;
return getProgram();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Script))
return false;
Script s = (Script)obj;
return Arrays.equals(getQuickProgram(), s.getQuickProgram());
}
@Override
public int hashCode() {
byte[] bytes = getQuickProgram();
return Arrays.hashCode(bytes);
}
}

View File

@ -21,6 +21,8 @@ import com.google.bitcoin.core.TransactionConfidence.ConfidenceType;
import com.google.bitcoin.crypto.EncryptedPrivateKey;
import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterScrypt;
import com.google.bitcoin.script.Script;
import com.google.common.collect.Lists;
import com.google.protobuf.ByteString;
import com.google.protobuf.TextFormat;
import org.bitcoinj.wallet.Protos;
@ -36,6 +38,7 @@ import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
@ -83,7 +86,7 @@ public class WalletProtobufSerializer {
/**
* Formats the given wallet (transactions and keys) to the given output stream in protocol buffer format.<p>
*
*
* Equivalent to <tt>walletToProto(wallet).writeTo(output);</tt>
*/
public void writeWallet(Wallet wallet, OutputStream output) throws IOException {
@ -154,6 +157,16 @@ public class WalletProtobufSerializer {
walletBuilder.addKey(keyBuilder);
}
for (Script script : wallet.getWatchedScripts()) {
Protos.Script protoScript =
Protos.Script.newBuilder()
.setProgram(ByteString.copyFrom(script.getProgram()))
.setCreationTimestamp(script.getCreationTimeSeconds() * 1000)
.build();
walletBuilder.addWatchedScript(protoScript);
}
// Populate the lastSeenBlockHash field.
Sha256Hash lastSeenBlockHash = wallet.getLastBlockSeenHash();
if (lastSeenBlockHash != null) {
@ -403,6 +416,20 @@ public class WalletProtobufSerializer {
wallet.addKey(ecKey);
}
List<Script> scripts = Lists.newArrayList();
for (Protos.Script protoScript : walletProto.getWatchedScriptList()) {
try {
Script script =
new Script(protoScript.getProgram().toByteArray(),
protoScript.getCreationTimestamp() / 1000);
scripts.add(script);
} catch (ScriptException e) {
throw new UnreadableWalletException("Unparseable script in wallet");
}
}
wallet.addWatchedScripts(scripts);
// Read all transactions and insert into the txMap.
for (Protos.Transaction txProto : walletProto.getTransactionList()) {
readTransaction(txProto, wallet.getParams());

File diff suppressed because it is too large Load Diff

View File

@ -127,6 +127,11 @@ public class BitcoindComparisonTool {
return 1;
}
@Override
public boolean isRequiringUpdateAllBloomFilter() {
return false;
}
@Override public BloomFilter getBloomFilter(int size, double falsePositiveRate, long nTweak) {
BloomFilter filter = new BloomFilter(1, 0.99, 0);
filter.setMatchAll();

View File

@ -648,7 +648,7 @@ public class WalletTest extends TestWithWallet {
assertEquals(send1, eventDead[0]);
assertEquals(send2, eventReplacement[0]);
assertEquals(TransactionConfidence.ConfidenceType.DEAD,
send1.getConfidence().getConfidenceType());
send1.getConfidence().getConfidenceType());
assertEquals(send2, received.getOutput(0).getSpentBy().getParentTransaction());
TestUtils.DoubleSpends doubleSpends = TestUtils.createFakeDoubleSpendTxns(params, myAddress);
@ -659,7 +659,7 @@ public class WalletTest extends TestWithWallet {
sendMoneyToWallet(doubleSpends.t2, AbstractBlockChain.NewBlockType.BEST_CHAIN);
Threading.waitForUserCode();
assertEquals(TransactionConfidence.ConfidenceType.DEAD,
doubleSpends.t1.getConfidence().getConfidenceType());
doubleSpends.t1.getConfidence().getConfidenceType());
assertEquals(doubleSpends.t2, doubleSpends.t1.getConfidence().getOverridingTransaction());
assertEquals(5, eventWalletChanged[0]);
}
@ -831,7 +831,7 @@ public class WalletTest extends TestWithWallet {
// Check we got them back in order.
List<Transaction> transactions = wallet.getTransactionsByTime();
assertEquals(tx2, transactions.get(0));
assertEquals(tx1, transactions.get(1));
assertEquals(tx1, transactions.get(1));
assertEquals(2, transactions.size());
// Check we get only the last transaction if we request a subrage.
transactions = wallet.getRecentTransactions(1, false);
@ -873,6 +873,20 @@ public class WalletTest extends TestWithWallet {
assertEquals(now + 60, wallet.getEarliestKeyCreationTime());
}
@Test
public void scriptCreationTime() throws Exception {
wallet = new Wallet(params);
long now = Utils.rollMockClock(0).getTime() / 1000; // Fix the mock clock.
// No keys returns current time.
assertEquals(now, wallet.getEarliestKeyCreationTime());
Utils.rollMockClock(60);
wallet.addWatchedAddress(new ECKey().toAddress(params));
Utils.rollMockClock(60);
wallet.addKey(new ECKey());
assertEquals(now + 60, wallet.getEarliestKeyCreationTime());
}
@Test
public void spendToSameWallet() throws Exception {
// Test that a spend to the same wallet is dealt with correctly.
@ -950,6 +964,73 @@ public class WalletTest extends TestWithWallet {
log.info(t2.toString(chain));
}
@Test
public void watchingScripts() throws Exception {
// Verify that pending transactions to watched addresses are relevant
ECKey key = new ECKey();
Address watchedAddress = key.toAddress(params);
wallet.addWatchedAddress(watchedAddress);
BigInteger value = toNanoCoins(5, 0);
Transaction t1 = createFakeTx(params, value, watchedAddress);
assertTrue(wallet.isPendingTransactionRelevant(t1));
}
@Test
public void watchingScriptsConfirmed() throws Exception {
ECKey key = new ECKey();
Address watchedAddress = key.toAddress(params);
wallet.addWatchedAddress(watchedAddress);
Transaction t1 = createFakeTx(params, CENT, watchedAddress);
StoredBlock b3 = createFakeBlock(blockStore, t1).storedBlock;
wallet.receiveFromBlock(t1, b3, BlockChain.NewBlockType.BEST_CHAIN, 0);
assertEquals(BigInteger.ZERO, wallet.getBalance());
assertEquals(CENT, wallet.getWatchedBalance());
// We can't spend watched balances
Address notMyAddr = new ECKey().toAddress(params);
assertNull(wallet.createSend(notMyAddr, CENT));
}
@Test
public void watchingScriptsSentFrom() throws Exception {
ECKey key = new ECKey();
ECKey notMyAddr = new ECKey();
Address watchedAddress = key.toAddress(params);
wallet.addWatchedAddress(watchedAddress);
Transaction t1 = createFakeTx(params, CENT, watchedAddress);
Transaction t2 = createFakeTx(params, COIN, notMyAddr);
StoredBlock b1 = createFakeBlock(blockStore, t1).storedBlock;
Transaction st2 = new Transaction(params);
st2.addOutput(CENT, notMyAddr);
st2.addOutput(COIN, notMyAddr);
st2.addInput(t1.getOutput(0));
st2.addInput(t2.getOutput(0));
wallet.receiveFromBlock(t1, b1, BlockChain.NewBlockType.BEST_CHAIN, 0);
wallet.receiveFromBlock(st2, b1, BlockChain.NewBlockType.BEST_CHAIN, 0);
assertEquals(CENT, st2.getValueSentFromMe(wallet));
}
@Test
public void watchingScriptsBloomFilter() throws Exception {
assertFalse(wallet.isRequiringUpdateAllBloomFilter());
ECKey key = new ECKey();
Address watchedAddress = key.toAddress(params);
wallet.addWatchedAddress(watchedAddress);
assertTrue(wallet.isRequiringUpdateAllBloomFilter());
Transaction t1 = createFakeTx(params, CENT, watchedAddress);
StoredBlock b1 = createFakeBlock(blockStore, t1).storedBlock;
TransactionOutPoint outPoint = new TransactionOutPoint(params, 0, t1);
// Note that this has a 1e-12 chance of failing this unit test due to a false positive
assertFalse(wallet.getBloomFilter(1e-12).contains(outPoint.bitcoinSerialize()));
wallet.receiveFromBlock(t1, b1, BlockChain.NewBlockType.BEST_CHAIN, 0);
assertTrue(wallet.getBloomFilter(1e-12).contains(outPoint.bitcoinSerialize()));
}
@Test
public void autosaveImmediate() throws Exception {
// Test that the wallet will save itself automatically when it changes.

View File

@ -5,6 +5,7 @@ import com.google.bitcoin.core.*;
import com.google.bitcoin.core.TransactionConfidence.ConfidenceType;
import com.google.bitcoin.params.MainNetParams;
import com.google.bitcoin.params.UnitTestParams;
import com.google.bitcoin.script.ScriptBuilder;
import com.google.bitcoin.utils.BriefLogFormatter;
import com.google.bitcoin.utils.TestUtils;
import com.google.bitcoin.utils.Threading;
@ -18,6 +19,7 @@ import java.io.ByteArrayOutputStream;
import java.math.BigInteger;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.Set;
@ -27,19 +29,24 @@ import static org.junit.Assert.*;
public class WalletProtobufSerializerTest {
static final NetworkParameters params = UnitTestParams.get();
private ECKey myKey;
private ECKey myWatchedKey;
private Address myAddress;
private Wallet myWallet;
public static String WALLET_DESCRIPTION = "The quick brown fox lives in \u4f26\u6566"; // Beijing in Chinese
private long mScriptCreationTime;
@Before
public void setUp() throws Exception {
BriefLogFormatter.initVerbose();
myWatchedKey = new ECKey();
myKey = new ECKey();
myKey.setCreationTimeSeconds(123456789L);
myAddress = myKey.toAddress(params);
myWallet = new Wallet(params);
myWallet.addKey(myKey);
mScriptCreationTime = new Date().getTime() / 1000 - 1234;
myWallet.addWatchedAddress(myWatchedKey.toAddress(params), mScriptCreationTime);
myWallet.setDescription(WALLET_DESCRIPTION);
}
@ -55,6 +62,11 @@ public class WalletProtobufSerializerTest {
wallet1.findKeyFromPubHash(myKey.getPubKeyHash()).getPrivKeyBytes());
assertEquals(myKey.getCreationTimeSeconds(),
wallet1.findKeyFromPubHash(myKey.getPubKeyHash()).getCreationTimeSeconds());
assertEquals(mScriptCreationTime,
wallet1.getWatchedScripts().get(0).getCreationTimeSeconds());
assertEquals(1, wallet1.getWatchedScripts().size());
assertEquals(ScriptBuilder.createOutputScript(myWatchedKey.toAddress(params)),
wallet1.getWatchedScripts().get(0));
assertEquals(WALLET_DESCRIPTION, wallet1.getDescription());
}