mirror of
https://github.com/bitcoinj/bitcoinj.git
synced 2025-01-19 05:33:44 +01:00
Support watching of scripts/addresses in wallet
This commit is contained in:
parent
2271e7198e
commit
da2e3e6c98
@ -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
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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)) {
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
@ -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();
|
||||
|
@ -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.
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user