3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-01-31 15:22:16 +00:00

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; 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 { message TransactionInput {
// Hash of the transaction this input is using. // Hash of the transaction this input is using.
required bytes transaction_out_point_hash = 1; required bytes transaction_out_point_hash = 1;
@ -257,6 +265,7 @@ message Wallet {
repeated Key key = 3; repeated Key key = 3;
repeated Transaction transaction = 4; repeated Transaction transaction = 4;
repeated Script watched_script = 15;
optional EncryptionType encryption_type = 5 [default=UNENCRYPTED]; optional EncryptionType encryption_type = 5 [default=UNENCRYPTED];
optional ScryptParameters encryption_parameters = 6; 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. // can be used to recover a compromised wallet, or just as part of preventative defence-in-depth measures.
optional uint64 key_rotation_time = 13; optional uint64 key_rotation_time = 13;
// Next tag: 15 // Next tag: 16
} }

View File

@ -16,6 +16,8 @@
package com.google.bitcoin.core; package com.google.bitcoin.core;
import com.google.bitcoin.script.Script;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.List; import java.util.List;
@ -48,6 +50,11 @@ public abstract class AbstractWalletEventListener implements WalletEventListener
onChange(); onChange();
} }
@Override
public void onScriptsAdded(Wallet wallet, List<Script> scripts) {
onChange();
}
@Override @Override
public void onWalletChanged(Wallet wallet) { public void onWalletChanged(Wallet wallet) {
onChange(); 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. * Default value should be an empty bloom filter with the given size, falsePositiveRate, and nTweak.
*/ */
public BloomFilter getBloomFilter(int size, double falsePositiveRate, long 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.core.Peer.PeerHandler;
import com.google.bitcoin.discovery.PeerDiscovery; import com.google.bitcoin.discovery.PeerDiscovery;
import com.google.bitcoin.discovery.PeerDiscoveryException; import com.google.bitcoin.discovery.PeerDiscoveryException;
import com.google.bitcoin.script.Script;
import com.google.bitcoin.utils.ListenerRegistration; import com.google.bitcoin.utils.ListenerRegistration;
import com.google.bitcoin.utils.Threading; import com.google.bitcoin.utils.Threading;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
@ -131,6 +132,7 @@ public class PeerGroup extends AbstractIdleService implements TransactionBroadca
private void onChanged() { private void onChanged() {
recalculateFastCatchupAndFilter(); recalculateFastCatchupAndFilter();
} }
@Override public void onScriptsAdded(Wallet wallet, List<Script> scripts) { onChanged(); }
@Override public void onKeysAdded(Wallet wallet, List<ECKey> keys) { 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 onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { onChanged(); }
@Override public void onCoinsSent(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; return;
long earliestKeyTimeSecs = Long.MAX_VALUE; long earliestKeyTimeSecs = Long.MAX_VALUE;
int elements = 0; int elements = 0;
boolean requiresUpdateAll = false;
for (PeerFilterProvider p : peerFilterProviders) { for (PeerFilterProvider p : peerFilterProviders) {
earliestKeyTimeSecs = Math.min(earliestKeyTimeSecs, p.getEarliestKeyCreationTime()); earliestKeyTimeSecs = Math.min(earliestKeyTimeSecs, p.getEarliestKeyCreationTime());
elements += p.getBloomFilterElementCount(); elements += p.getBloomFilterElementCount();
requiresUpdateAll = requiresUpdateAll || p.isRequiringUpdateAllBloomFilter();
} }
if (elements > 0) { 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 - // 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. // it will likely mean we never need to create a filter with different parameters.
lastBloomFilterElementCount = elements > lastBloomFilterElementCount ? elements + 100 : lastBloomFilterElementCount; 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) for (PeerFilterProvider p : peerFilterProviders)
filter.merge(p.getBloomFilter(lastBloomFilterElementCount, bloomFilterFPRate, bloomFilterTweak)); filter.merge(p.getBloomFilter(lastBloomFilterElementCount, bloomFilterFPRate, bloomFilterTweak));
if (!filter.equals(bloomFilter)) { if (!filter.equals(bloomFilter)) {

View File

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

View File

@ -250,6 +250,27 @@ public class TransactionOutput extends ChildMessage implements Serializable {
return scriptBytes; 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. * 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.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterException; import com.google.bitcoin.crypto.KeyCrypterException;
import com.google.bitcoin.crypto.KeyCrypterScrypt; 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.UnreadableWalletException;
import com.google.bitcoin.store.WalletProtobufSerializer; import com.google.bitcoin.store.WalletProtobufSerializer;
import com.google.bitcoin.utils.ListenerRegistration; import com.google.bitcoin.utils.ListenerRegistration;
@ -94,6 +97,7 @@ import static com.google.common.base.Preconditions.*;
public class Wallet implements Serializable, BlockChainListener, PeerFilterProvider { public class Wallet implements Serializable, BlockChainListener, PeerFilterProvider {
private static final Logger log = LoggerFactory.getLogger(Wallet.class); private static final Logger log = LoggerFactory.getLogger(Wallet.class);
private static final long serialVersionUID = 2L; private static final long serialVersionUID = 2L;
private static final int MINIMUM_BLOOM_DATA_LENGTH = 8;
protected final ReentrantLock lock = Threading.lock("wallet"); 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. // A list of public/private EC keys owned by this user. Access it using addKey[s], hasKey[s] and findPubKeyFromHash.
private ArrayList<ECKey> keychain; private ArrayList<ECKey> keychain;
// A list of scripts watched by this wallet.
private Set<Script> watchedScripts;
private final NetworkParameters params; private final NetworkParameters params;
private Sha256Hash lastBlockSeenHash; private Sha256Hash lastBlockSeenHash;
@ -192,6 +199,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
this.keyCrypter = keyCrypter; this.keyCrypter = keyCrypter;
this.params = checkNotNull(params); this.params = checkNotNull(params);
keychain = new ArrayList<ECKey>(); keychain = new ArrayList<ECKey>();
watchedScripts = Sets.newHashSet();
unspent = new HashMap<Sha256Hash, Transaction>(); unspent = new HashMap<Sha256Hash, Transaction>();
spent = new HashMap<Sha256Hash, Transaction>(); spent = new HashMap<Sha256Hash, Transaction>();
pending = 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 * Removes the given key from the keychain. Be very careful with this - losing a private key <b>destroys the
* money associated with it</b>. * 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. */ /** Returns the address used for change outputs. Note: this will probably go away in future. */
public Address getChangeAddress() { public Address getChangeAddress() {
lock.lock(); 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 * 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. * 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; 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. * Locates a keypair from the keychain given the raw public key bytes.
* @return ECKey or null if no such key was found. * @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 @Override
public String toString() { public String toString() {
return toString(false, true, true, null); 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 * Returns the earliest creation time of keys or watched scripts in this wallet, in seconds since the epoch, ie the min
* {@link com.google.bitcoin.core.ECKey#getCreationTimeSeconds()}. This can return zero if at least one key does * 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> * 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 * 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() { public long getEarliestKeyCreationTime() {
lock.lock(); lock.lock();
try { try {
if (keychain.size() == 0) {
return Utils.now().getTime() / 1000;
}
long earliestTime = Long.MAX_VALUE; long earliestTime = Long.MAX_VALUE;
for (ECKey key : keychain) { for (ECKey key : keychain)
earliestTime = Math.min(key.getCreationTimeSeconds(), earliestTime); 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; return earliestTime;
} finally { } finally {
lock.unlock(); 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; 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 * 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. * 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) { public BloomFilter getBloomFilter(double falsePositiveRate) {
return getBloomFilter(getBloomFilterElementCount(), falsePositiveRate, (long)(Math.random()*Long.MAX_VALUE)); return getBloomFilter(getBloomFilterElementCount(), falsePositiveRate, (long)(Math.random()*Long.MAX_VALUE));
} }
/** /**
* Gets a bloom filter that contains all of the public keys from this wallet, * 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. * 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.getPubKey());
filter.insert(key.getPubKeyHash()); 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 { } finally {
lock.unlock(); lock.unlock();
} }
@ -2842,7 +3011,8 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
for (int i = 0; i < tx.getOutputs().size(); i++) { for (int i = 0; i < tx.getOutputs().size(); i++) {
TransactionOutput out = tx.getOutputs().get(i); TransactionOutput out = tx.getOutputs().get(i);
try { 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); TransactionOutPoint outPoint = new TransactionOutPoint(params, i, tx);
filter.insert(outPoint.bitcoinSerialize()); filter.insert(outPoint.bitcoinSerialize());
} }
@ -2851,6 +3021,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
} }
} }
} }
return filter; 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. // Fee calculation code.

View File

@ -16,6 +16,8 @@
package com.google.bitcoin.core; package com.google.bitcoin.core;
import com.google.bitcoin.script.Script;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.List; import java.util.List;
@ -119,4 +121,7 @@ public interface WalletEventListener {
* or due to some other automatic derivation. * or due to some other automatic derivation.
*/ */
void onKeysAdded(Wallet wallet, List<ECKey> keys); 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.Transaction;
import com.google.bitcoin.core.Wallet; import com.google.bitcoin.core.Wallet;
import com.google.bitcoin.core.WalletEventListener; import com.google.bitcoin.core.WalletEventListener;
import com.google.bitcoin.script.Script;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.List; import java.util.List;
@ -49,4 +50,7 @@ public class NativeWalletEventListener implements WalletEventListener {
@Override @Override
public native void onKeysAdded(Wallet wallet, List<ECKey> keys); 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. // must preserve the exact bytes that we read off the wire, along with the parsed form.
protected byte[] program; 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. */ /** Creates an empty script that serializes to nothing. */
private Script() { private Script() {
chunks = Lists.newArrayList(); chunks = Lists.newArrayList();
@ -69,6 +72,7 @@ public class Script {
// Used from ScriptBuilder. // Used from ScriptBuilder.
Script(List<ScriptChunk> chunks) { Script(List<ScriptChunk> chunks) {
this.chunks = Collections.unmodifiableList(new ArrayList<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 { public Script(byte[] programBytes) throws ScriptException {
program = programBytes; program = programBytes;
parse(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("] "); buf.append("] ");
} }
} }
if (creationTimeSeconds != 0) {
buf.append(" timestamp:").append(creationTimeSeconds);
}
return buf.toString(); return buf.toString();
} }
@ -110,7 +134,8 @@ public class Script {
for (ScriptChunk chunk : chunks) { for (ScriptChunk chunk : chunks) {
chunk.write(bos); chunk.write(bos);
} }
return bos.toByteArray(); program = bos.toByteArray();
return program;
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); // Cannot happen. 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"); 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.EncryptedPrivateKey;
import com.google.bitcoin.crypto.KeyCrypter; import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterScrypt; 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.ByteString;
import com.google.protobuf.TextFormat; import com.google.protobuf.TextFormat;
import org.bitcoinj.wallet.Protos; import org.bitcoinj.wallet.Protos;
@ -36,6 +38,7 @@ import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.ListIterator; import java.util.ListIterator;
import java.util.Map; 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> * 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> * Equivalent to <tt>walletToProto(wallet).writeTo(output);</tt>
*/ */
public void writeWallet(Wallet wallet, OutputStream output) throws IOException { public void writeWallet(Wallet wallet, OutputStream output) throws IOException {
@ -154,6 +157,16 @@ public class WalletProtobufSerializer {
walletBuilder.addKey(keyBuilder); 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. // Populate the lastSeenBlockHash field.
Sha256Hash lastSeenBlockHash = wallet.getLastBlockSeenHash(); Sha256Hash lastSeenBlockHash = wallet.getLastBlockSeenHash();
if (lastSeenBlockHash != null) { if (lastSeenBlockHash != null) {
@ -403,6 +416,20 @@ public class WalletProtobufSerializer {
wallet.addKey(ecKey); 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. // Read all transactions and insert into the txMap.
for (Protos.Transaction txProto : walletProto.getTransactionList()) { for (Protos.Transaction txProto : walletProto.getTransactionList()) {
readTransaction(txProto, wallet.getParams()); 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; return 1;
} }
@Override
public boolean isRequiringUpdateAllBloomFilter() {
return false;
}
@Override public BloomFilter getBloomFilter(int size, double falsePositiveRate, long nTweak) { @Override public BloomFilter getBloomFilter(int size, double falsePositiveRate, long nTweak) {
BloomFilter filter = new BloomFilter(1, 0.99, 0); BloomFilter filter = new BloomFilter(1, 0.99, 0);
filter.setMatchAll(); filter.setMatchAll();

View File

@ -648,7 +648,7 @@ public class WalletTest extends TestWithWallet {
assertEquals(send1, eventDead[0]); assertEquals(send1, eventDead[0]);
assertEquals(send2, eventReplacement[0]); assertEquals(send2, eventReplacement[0]);
assertEquals(TransactionConfidence.ConfidenceType.DEAD, assertEquals(TransactionConfidence.ConfidenceType.DEAD,
send1.getConfidence().getConfidenceType()); send1.getConfidence().getConfidenceType());
assertEquals(send2, received.getOutput(0).getSpentBy().getParentTransaction()); assertEquals(send2, received.getOutput(0).getSpentBy().getParentTransaction());
TestUtils.DoubleSpends doubleSpends = TestUtils.createFakeDoubleSpendTxns(params, myAddress); TestUtils.DoubleSpends doubleSpends = TestUtils.createFakeDoubleSpendTxns(params, myAddress);
@ -659,7 +659,7 @@ public class WalletTest extends TestWithWallet {
sendMoneyToWallet(doubleSpends.t2, AbstractBlockChain.NewBlockType.BEST_CHAIN); sendMoneyToWallet(doubleSpends.t2, AbstractBlockChain.NewBlockType.BEST_CHAIN);
Threading.waitForUserCode(); Threading.waitForUserCode();
assertEquals(TransactionConfidence.ConfidenceType.DEAD, assertEquals(TransactionConfidence.ConfidenceType.DEAD,
doubleSpends.t1.getConfidence().getConfidenceType()); doubleSpends.t1.getConfidence().getConfidenceType());
assertEquals(doubleSpends.t2, doubleSpends.t1.getConfidence().getOverridingTransaction()); assertEquals(doubleSpends.t2, doubleSpends.t1.getConfidence().getOverridingTransaction());
assertEquals(5, eventWalletChanged[0]); assertEquals(5, eventWalletChanged[0]);
} }
@ -831,7 +831,7 @@ public class WalletTest extends TestWithWallet {
// Check we got them back in order. // Check we got them back in order.
List<Transaction> transactions = wallet.getTransactionsByTime(); List<Transaction> transactions = wallet.getTransactionsByTime();
assertEquals(tx2, transactions.get(0)); assertEquals(tx2, transactions.get(0));
assertEquals(tx1, transactions.get(1)); assertEquals(tx1, transactions.get(1));
assertEquals(2, transactions.size()); assertEquals(2, transactions.size());
// Check we get only the last transaction if we request a subrage. // Check we get only the last transaction if we request a subrage.
transactions = wallet.getRecentTransactions(1, false); transactions = wallet.getRecentTransactions(1, false);
@ -873,6 +873,20 @@ public class WalletTest extends TestWithWallet {
assertEquals(now + 60, wallet.getEarliestKeyCreationTime()); 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 @Test
public void spendToSameWallet() throws Exception { public void spendToSameWallet() throws Exception {
// Test that a spend to the same wallet is dealt with correctly. // 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)); 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 @Test
public void autosaveImmediate() throws Exception { public void autosaveImmediate() throws Exception {
// Test that the wallet will save itself automatically when it changes. // 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.core.TransactionConfidence.ConfidenceType;
import com.google.bitcoin.params.MainNetParams; import com.google.bitcoin.params.MainNetParams;
import com.google.bitcoin.params.UnitTestParams; import com.google.bitcoin.params.UnitTestParams;
import com.google.bitcoin.script.ScriptBuilder;
import com.google.bitcoin.utils.BriefLogFormatter; import com.google.bitcoin.utils.BriefLogFormatter;
import com.google.bitcoin.utils.TestUtils; import com.google.bitcoin.utils.TestUtils;
import com.google.bitcoin.utils.Threading; import com.google.bitcoin.utils.Threading;
@ -18,6 +19,7 @@ import java.io.ByteArrayOutputStream;
import java.math.BigInteger; import java.math.BigInteger;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.Set; import java.util.Set;
@ -27,19 +29,24 @@ import static org.junit.Assert.*;
public class WalletProtobufSerializerTest { public class WalletProtobufSerializerTest {
static final NetworkParameters params = UnitTestParams.get(); static final NetworkParameters params = UnitTestParams.get();
private ECKey myKey; private ECKey myKey;
private ECKey myWatchedKey;
private Address myAddress; private Address myAddress;
private Wallet myWallet; private Wallet myWallet;
public static String WALLET_DESCRIPTION = "The quick brown fox lives in \u4f26\u6566"; // Beijing in Chinese public static String WALLET_DESCRIPTION = "The quick brown fox lives in \u4f26\u6566"; // Beijing in Chinese
private long mScriptCreationTime;
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
BriefLogFormatter.initVerbose(); BriefLogFormatter.initVerbose();
myWatchedKey = new ECKey();
myKey = new ECKey(); myKey = new ECKey();
myKey.setCreationTimeSeconds(123456789L); myKey.setCreationTimeSeconds(123456789L);
myAddress = myKey.toAddress(params); myAddress = myKey.toAddress(params);
myWallet = new Wallet(params); myWallet = new Wallet(params);
myWallet.addKey(myKey); myWallet.addKey(myKey);
mScriptCreationTime = new Date().getTime() / 1000 - 1234;
myWallet.addWatchedAddress(myWatchedKey.toAddress(params), mScriptCreationTime);
myWallet.setDescription(WALLET_DESCRIPTION); myWallet.setDescription(WALLET_DESCRIPTION);
} }
@ -55,6 +62,11 @@ public class WalletProtobufSerializerTest {
wallet1.findKeyFromPubHash(myKey.getPubKeyHash()).getPrivKeyBytes()); wallet1.findKeyFromPubHash(myKey.getPubKeyHash()).getPrivKeyBytes());
assertEquals(myKey.getCreationTimeSeconds(), assertEquals(myKey.getCreationTimeSeconds(),
wallet1.findKeyFromPubHash(myKey.getPubKeyHash()).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()); assertEquals(WALLET_DESCRIPTION, wallet1.getDescription());
} }