mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-01-31 15:22:16 +00:00
Wallet: support for key rotation.
Key rotation allows you to specify a timestamp, and any money controlled by any keys created before that time will be automatically respent to keys created after it.
This commit is contained in:
parent
3ca2cd0345
commit
6dd907614c
@ -190,6 +190,18 @@ message Transaction {
|
||||
|
||||
// Data describing where the transaction is in the chain.
|
||||
optional TransactionConfidence confidence = 9;
|
||||
|
||||
// For what purpose the transaction was created.
|
||||
enum Purpose {
|
||||
// Old wallets or the purpose genuinely is a mystery (e.g. imported from some external source).
|
||||
UNKNOWN = 0;
|
||||
// Created in response to a user request for payment. This is the normal case.
|
||||
USER_PAYMENT = 1;
|
||||
// Created automatically to move money from rotated keys.
|
||||
KEY_ROTATION = 2;
|
||||
// In future: de/refragmentation, privacy boosting/mixing, child-pays-for-parent fees, etc.
|
||||
}
|
||||
optional Purpose purpose = 10 [default = UNKNOWN];
|
||||
}
|
||||
|
||||
/** The parameters used in the scrypt key derivation function.
|
||||
@ -257,4 +269,9 @@ message Wallet {
|
||||
optional string description = 11;
|
||||
|
||||
// (The field number 12 is used by last_seen_block_height)
|
||||
} // end of Wallet
|
||||
|
||||
// UNIX time in seconds since the epoch. If set, then any keys created before this date are assumed to be no longer
|
||||
// wanted. Money sent to them will be re-spent automatically to the first key that was created after this time. It
|
||||
// can be used to recover a compromised wallet, or just as part of preventative defence-in-depth measures.
|
||||
optional uint64 key_rotation_time = 13;
|
||||
}
|
||||
|
@ -105,6 +105,23 @@ public class Transaction extends ChildMessage implements Serializable {
|
||||
// can properly keep track of optimal encoded size
|
||||
private transient int optimalEncodingMessageSize;
|
||||
|
||||
/**
|
||||
* This enum describes the underlying reason the transaction was created. It's useful for rendering wallet GUIs
|
||||
* more appropriately.
|
||||
*/
|
||||
public enum Purpose {
|
||||
/** Used when the purpose of a transaction is genuinely unknown. */
|
||||
UNKNOWN,
|
||||
/** Transaction created to satisfy a user payment request. */
|
||||
USER_PAYMENT,
|
||||
/** Transaction automatically created and broadcast in order to reallocate money from old to new keys. */
|
||||
KEY_ROTATION,
|
||||
|
||||
// In future: de/refragmentation, privacy boosting/mixing, child-pays-for-parent fees, etc.
|
||||
}
|
||||
|
||||
private Purpose purpose = Purpose.UNKNOWN;
|
||||
|
||||
public Transaction(NetworkParameters params) {
|
||||
super(params);
|
||||
version = 1;
|
||||
@ -1212,4 +1229,20 @@ public class Transaction extends ChildMessage implements Serializable {
|
||||
else
|
||||
return new Date(getLockTime()*1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the purpose for which this transaction was created. See the javadoc for {@link Purpose} for more
|
||||
* information on the point of this field and what it can be.
|
||||
*/
|
||||
public Purpose getPurpose() {
|
||||
return purpose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the transaction as being created for the given purpose. See the javadoc for {@link Purpose} for more
|
||||
* information on the point of this field and what it can be.
|
||||
*/
|
||||
public void setPurpose(Purpose purpose) {
|
||||
this.purpose = purpose;
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import com.google.bitcoin.script.ScriptBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.OutputStream;
|
||||
@ -283,6 +284,14 @@ public class TransactionOutput extends ChildMessage implements Serializable {
|
||||
return spentBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the transaction that owns this output, or null if this is a free standing object.
|
||||
*/
|
||||
@Nullable
|
||||
public Transaction getParentTransaction() {
|
||||
return parentTransaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure object is fully parsed before invoking java serialization. The backing byte array
|
||||
* is transient so if the object has parseLazy = true and hasn't invoked checkParse yet
|
||||
|
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright 2011 Google Inc.
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -25,9 +25,12 @@ import com.google.bitcoin.store.UnreadableWalletException;
|
||||
import com.google.bitcoin.store.WalletProtobufSerializer;
|
||||
import com.google.bitcoin.utils.ListenerRegistration;
|
||||
import com.google.bitcoin.utils.Threading;
|
||||
import com.google.bitcoin.wallet.KeyTimeCoinSelector;
|
||||
import com.google.bitcoin.wallet.WalletFiles;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.*;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
|
||||
@ -141,8 +144,12 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
||||
private boolean insideReorg;
|
||||
private Map<Transaction, TransactionConfidence.Listener.ChangeReason> confidenceChanged;
|
||||
private volatile WalletFiles vFileManager;
|
||||
|
||||
private TransactionBroadcaster vTransactionBroadcaster;
|
||||
// Object that is used to send transactions asynchronously when the wallet requires it.
|
||||
private volatile TransactionBroadcaster vTransactionBroadcaster;
|
||||
// UNIX time in seconds. Money controlled by keys created before this time will be automatically respent to a key
|
||||
// that was created after it. Useful when you believe some keys have been compromised.
|
||||
private volatile long vKeyRotationTimestamp;
|
||||
private volatile boolean vKeyRotationEnabled;
|
||||
|
||||
/** Represents the results of a {@link CoinSelector#select(java.math.BigInteger, java.util.LinkedList)} operation */
|
||||
public static class CoinSelection {
|
||||
@ -480,8 +487,8 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
||||
files.saveLater();
|
||||
}
|
||||
|
||||
/** If auto saving is enabled, do an immediate sync write to disk ignoring any delays. */
|
||||
private void saveNow() {
|
||||
// If auto saving is enabled, do an immediate sync write to disk ignoring any delays.
|
||||
WalletFiles files = vFileManager;
|
||||
if (files != null) {
|
||||
try {
|
||||
@ -621,6 +628,11 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
if (blockType == AbstractBlockChain.NewBlockType.BEST_CHAIN) {
|
||||
// If some keys are considered to be bad, possibly move money assigned to them now.
|
||||
// This has to run outside the wallet lock as it may trigger broadcasting of new transactions.
|
||||
maybeRotateKeys();
|
||||
}
|
||||
}
|
||||
|
||||
/** The results of examining the dependency graph of a pending transaction for protocol abuse. */
|
||||
@ -686,6 +698,8 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
// maybeRotateKeys() will ignore pending transactions so we don't bother calling it here (see the comments
|
||||
// in that function for an explanation of why).
|
||||
}
|
||||
|
||||
/**
|
||||
@ -832,6 +846,11 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
if (blockType == AbstractBlockChain.NewBlockType.BEST_CHAIN) {
|
||||
// If some keys are considered to be bad, possibly move money assigned to them now.
|
||||
// This has to run outside the wallet lock as it may trigger broadcasting of new transactions.
|
||||
maybeRotateKeys();
|
||||
}
|
||||
}
|
||||
|
||||
private void receive(Transaction tx, StoredBlock block, BlockChain.NewBlockType blockType) throws VerificationException {
|
||||
@ -1909,17 +1928,10 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
||||
req.tx.addInput(output);
|
||||
|
||||
if (req.ensureMinRequiredFee && req.emptyWallet) {
|
||||
TransactionOutput output = req.tx.getOutput(0);
|
||||
// Check if we need additional fee due to the transaction's size
|
||||
int size = req.tx.bitcoinSerialize().length;
|
||||
size += estimateBytesForSigning(bestCoinSelection);
|
||||
BigInteger fee = (req.fee == null ? BigInteger.ZERO : req.fee)
|
||||
.add(BigInteger.valueOf((size / 1000) + 1).multiply(req.feePerKb == null ? BigInteger.ZERO : req.feePerKb));
|
||||
output.setValue(output.getValue().subtract(fee));
|
||||
// Check if we need additional fee due to the output's value
|
||||
if (output.getValue().compareTo(Utils.CENT) < 0 && fee.compareTo(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE) < 0)
|
||||
output.setValue(output.getValue().subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.subtract(fee)));
|
||||
if (output.getMinNonDustValue().compareTo(output.getValue()) > 0)
|
||||
final BigInteger baseFee = req.fee == null ? BigInteger.ZERO : req.fee;
|
||||
final BigInteger feePerKb = req.feePerKb == null ? BigInteger.ZERO : req.feePerKb;
|
||||
Transaction tx = req.tx;
|
||||
if (!adjustOutputDownwardsForFee(tx, bestCoinSelection, baseFee, feePerKb))
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1958,6 +1970,10 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
||||
// the transaction is confirmed. We deliberately won't bother notifying listeners here as there's not much
|
||||
// point - the user isn't interested in a confidence transition they made themselves.
|
||||
req.tx.getConfidence().setSource(TransactionConfidence.Source.SELF);
|
||||
// Label the transaction as being a user requested payment. This can be used to render GUI wallet
|
||||
// transaction lists more appropriately, especially when the wallet starts to generate transactions itself
|
||||
// for internal purposes.
|
||||
req.tx.setPurpose(Transaction.Purpose.USER_PAYMENT);
|
||||
req.completed = true;
|
||||
req.fee = calculatedFee;
|
||||
log.info(" completed: {}", req.tx);
|
||||
@ -1967,6 +1983,20 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
||||
}
|
||||
}
|
||||
|
||||
/** Reduce the value of the first output of a transaction to pay the given feePerKb as appropriate for its size. */
|
||||
private boolean adjustOutputDownwardsForFee(Transaction tx, CoinSelection coinSelection, BigInteger baseFee, BigInteger feePerKb) {
|
||||
TransactionOutput output = tx.getOutput(0);
|
||||
// Check if we need additional fee due to the transaction's size
|
||||
int size = tx.bitcoinSerialize().length;
|
||||
size += estimateBytesForSigning(coinSelection);
|
||||
BigInteger fee = baseFee.add(BigInteger.valueOf((size / 1000) + 1).multiply(feePerKb));
|
||||
output.setValue(output.getValue().subtract(fee));
|
||||
// Check if we need additional fee due to the output's value
|
||||
if (output.getValue().compareTo(Utils.CENT) < 0 && fee.compareTo(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE) < 0)
|
||||
output.setValue(output.getValue().subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.subtract(fee)));
|
||||
return output.getMinNonDustValue().compareTo(output.getValue()) <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all possible outputs we could possibly spend, potentially even including immature coinbases
|
||||
* (which the protocol may forbid us from spending). In other words, return all outputs that this wallet holds
|
||||
@ -3336,7 +3366,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Managing wallet-triggered transaction broadcast.
|
||||
// Managing wallet-triggered transaction broadcast and key rotation.
|
||||
|
||||
/**
|
||||
* <p>Specifies that the given {@link TransactionBroadcaster}, typically a {@link PeerGroup}, should be used for
|
||||
@ -3350,7 +3380,160 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
||||
* re-organisation of the wallet contents on the block chain. For instance, in future the wallet may choose to
|
||||
* optimise itself to reduce fees or improve privacy.</p>
|
||||
*/
|
||||
public void setTransactionBroadcaster(@Nullable TransactionBroadcaster broadcaster) {
|
||||
public void setTransactionBroadcaster(@Nullable com.google.bitcoin.core.TransactionBroadcaster broadcaster) {
|
||||
vTransactionBroadcaster = broadcaster;
|
||||
}
|
||||
|
||||
/**
|
||||
* When a key rotation time is set, and money controlled by keys created before the given timestamp T will be
|
||||
* automatically respent to any key that was created after T. This can be used to recover from a situation where
|
||||
* a set of keys is believed to be compromised. Once the time is set transactions will be created and broadcast
|
||||
* immediately. New coins that come in after calling this method will be automatically respent immediately. The
|
||||
* rotation time is persisted to the wallet. You can stop key rotation by calling this method again with zero
|
||||
* as the argument.
|
||||
*/
|
||||
public void setKeyRotationTime(Date time) {
|
||||
setKeyRotationTime(time.getTime() / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a UNIX time since the epoch in seconds, or zero if unconfigured.
|
||||
*/
|
||||
public Date getKeyRotationTime() {
|
||||
return new Date(vKeyRotationTimestamp * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>When a key rotation time is set, and money controlled by keys created before the given timestamp T will be
|
||||
* automatically respent to any key that was created after T. This can be used to recover from a situation where
|
||||
* a set of keys is believed to be compromised. Once the time is set transactions will be created and broadcast
|
||||
* immediately. New coins that come in after calling this method will be automatically respent immediately. The
|
||||
* rotation time is persisted to the wallet. You can stop key rotation by calling this method again with zero
|
||||
* as the argument, or by using {@link #setKeyRotationEnabled(boolean)}.</p>
|
||||
*
|
||||
* <p>Note that this method won't do anything unless you call {@link #setKeyRotationEnabled(boolean)} first.</p>
|
||||
*/
|
||||
public void setKeyRotationTime(long unixTimeSeconds) {
|
||||
vKeyRotationTimestamp = unixTimeSeconds;
|
||||
if (unixTimeSeconds > 0) {
|
||||
log.info("Key rotation time set: {}", unixTimeSeconds);
|
||||
maybeRotateKeys();
|
||||
}
|
||||
saveNow();
|
||||
}
|
||||
|
||||
/** Toggles key rotation on and off. Note that this state is not serialized. Activating it can trigger tx sends. */
|
||||
public void setKeyRotationEnabled(boolean enabled) {
|
||||
vKeyRotationEnabled = enabled;
|
||||
if (enabled)
|
||||
maybeRotateKeys();
|
||||
}
|
||||
|
||||
/** Returns whether the keys creation time is before the key rotation time, if one was set. */
|
||||
public boolean isKeyRotating(ECKey key) {
|
||||
long time = vKeyRotationTimestamp;
|
||||
return time != 0 && key.getCreationTimeSeconds() < time;
|
||||
}
|
||||
|
||||
// Checks to see if any coins are controlled by rotating keys and if so, spends them.
|
||||
private void maybeRotateKeys() {
|
||||
checkState(!lock.isHeldByCurrentThread());
|
||||
// TODO: Handle chain replays and encrypted wallets here.
|
||||
if (!vKeyRotationEnabled) return;
|
||||
// Snapshot volatiles so this method has an atomic view.
|
||||
long keyRotationTimestamp = vKeyRotationTimestamp;
|
||||
if (keyRotationTimestamp == 0) return; // Nothing to do.
|
||||
TransactionBroadcaster broadcaster = vTransactionBroadcaster;
|
||||
|
||||
// Because transactions are size limited, we might not be able to re-key the entire wallet in one go. So
|
||||
// loop around here until we no longer produce transactions with the max number of inputs. That means we're
|
||||
// fully done, at least for now (we may still get more transactions later and this method will be reinvoked).
|
||||
Transaction tx;
|
||||
do {
|
||||
tx = rekeyOneBatch(keyRotationTimestamp, broadcaster);
|
||||
} while (tx != null && tx.getInputs().size() == KeyTimeCoinSelector.MAX_SIMULTANEOUS_INPUTS);
|
||||
}
|
||||
|
||||
private Transaction rekeyOneBatch(long keyRotationTimestamp, final TransactionBroadcaster broadcaster) {
|
||||
final Transaction rekeyTx;
|
||||
|
||||
lock.lock();
|
||||
try {
|
||||
// Firstly, see if we have any keys that are beyond the rotation time, and any before.
|
||||
ECKey safeKey = null;
|
||||
boolean haveRotatingKeys = false;
|
||||
for (ECKey key : keychain) {
|
||||
final long t = key.getCreationTimeSeconds();
|
||||
if (t < keyRotationTimestamp) {
|
||||
haveRotatingKeys = true;
|
||||
} else {
|
||||
safeKey = key;
|
||||
}
|
||||
}
|
||||
if (!haveRotatingKeys) return null;
|
||||
if (safeKey == null) {
|
||||
log.warn("Key rotation requested but no keys newer than the timestamp are available.");
|
||||
return null;
|
||||
}
|
||||
// Build the transaction using some custom logic for our special needs. Last parameter to
|
||||
// KeyTimeCoinSelector is whether to ignore pending transactions or not.
|
||||
//
|
||||
// We ignore pending outputs because trying to rotate these is basically racing an attacker, and
|
||||
// we're quite likely to lose and create stuck double spends. Also, some users who have 0.9 wallets
|
||||
// have already got stuck double spends in their wallet due to the Bloom-filtering block reordering
|
||||
// bug that was fixed in 0.10, thus, making a re-key transaction depend on those would cause it to
|
||||
// never confirm at all.
|
||||
CoinSelector selector = new KeyTimeCoinSelector(this, keyRotationTimestamp, true);
|
||||
CoinSelection toMove = selector.select(BigInteger.ZERO, calculateAllSpendCandidates(true));
|
||||
if (toMove.valueGathered.equals(BigInteger.ZERO)) return null; // Nothing to do.
|
||||
rekeyTx = new Transaction(params);
|
||||
for (TransactionOutput output : toMove.gathered) {
|
||||
rekeyTx.addInput(output);
|
||||
}
|
||||
rekeyTx.addOutput(toMove.valueGathered, safeKey);
|
||||
if (!adjustOutputDownwardsForFee(rekeyTx, toMove, BigInteger.ZERO, Transaction.REFERENCE_DEFAULT_MIN_TX_FEE)) {
|
||||
log.error("Failed to adjust rekey tx for fees.");
|
||||
return null;
|
||||
}
|
||||
rekeyTx.getConfidence().setSource(TransactionConfidence.Source.SELF);
|
||||
rekeyTx.setPurpose(Transaction.Purpose.KEY_ROTATION);
|
||||
rekeyTx.signInputs(Transaction.SigHash.ALL, this);
|
||||
// KeyTimeCoinSelector should never select enough inputs to push us oversize.
|
||||
checkState(rekeyTx.bitcoinSerialize().length < Transaction.MAX_STANDARD_TX_SIZE);
|
||||
commitTx(rekeyTx);
|
||||
} catch (VerificationException e) {
|
||||
throw new RuntimeException(e); // Cannot happen.
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
if (broadcaster == null)
|
||||
return rekeyTx;
|
||||
|
||||
log.info("Attempting to send key rotation tx: {}", rekeyTx);
|
||||
// We must broadcast the tx in a separate thread to avoid inverting any locks. Otherwise we may be running
|
||||
// with the blockchain lock held (whilst receiving a block) and thus re-entering the peerGroup would invert
|
||||
// blockchain <-> peergroup.
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Handle the future results just for logging.
|
||||
try {
|
||||
Futures.addCallback(broadcaster.broadcastTransaction(rekeyTx), new FutureCallback<Transaction>() {
|
||||
@Override
|
||||
public void onSuccess(Transaction transaction) {
|
||||
log.info("Successfully broadcast key rotation tx: {}", transaction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable throwable) {
|
||||
log.error("Failed to broadcast key rotation tx", throwable);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to broadcast rekey tx, will try again later", e);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
return rekeyTx;
|
||||
}
|
||||
}
|
||||
|
@ -167,6 +167,11 @@ public class WalletProtobufSerializer {
|
||||
}
|
||||
}
|
||||
|
||||
if (wallet.getKeyRotationTime() != null) {
|
||||
long timeSecs = wallet.getKeyRotationTime().getTime() / 1000;
|
||||
walletBuilder.setKeyRotationTime(timeSecs);
|
||||
}
|
||||
|
||||
populateExtensions(wallet, walletBuilder);
|
||||
|
||||
// Populate the wallet version.
|
||||
@ -240,6 +245,16 @@ public class WalletProtobufSerializer {
|
||||
Protos.TransactionConfidence.Builder confidenceBuilder = Protos.TransactionConfidence.newBuilder();
|
||||
writeConfidence(txBuilder, confidence, confidenceBuilder);
|
||||
}
|
||||
|
||||
Protos.Transaction.Purpose purpose;
|
||||
switch (tx.getPurpose()) {
|
||||
case UNKNOWN: purpose = Protos.Transaction.Purpose.UNKNOWN; break;
|
||||
case USER_PAYMENT: purpose = Protos.Transaction.Purpose.USER_PAYMENT; break;
|
||||
case KEY_ROTATION: purpose = Protos.Transaction.Purpose.KEY_ROTATION; break;
|
||||
default:
|
||||
throw new RuntimeException("New tx purpose serialization not implemented.");
|
||||
}
|
||||
txBuilder.setPurpose(purpose);
|
||||
|
||||
return txBuilder.build();
|
||||
}
|
||||
@ -396,6 +411,10 @@ public class WalletProtobufSerializer {
|
||||
wallet.setLastBlockSeenHeight(walletProto.getLastSeenBlockHeight());
|
||||
}
|
||||
|
||||
if (walletProto.hasKeyRotationTime()) {
|
||||
wallet.setKeyRotationTime(new Date(walletProto.getKeyRotationTime() * 1000));
|
||||
}
|
||||
|
||||
loadExtensions(wallet, walletProto);
|
||||
|
||||
if (walletProto.hasVersion()) {
|
||||
@ -470,6 +489,18 @@ public class WalletProtobufSerializer {
|
||||
tx.setLockTime(0xffffffffL & txProto.getLockTime());
|
||||
}
|
||||
|
||||
if (txProto.hasPurpose()) {
|
||||
switch (txProto.getPurpose()) {
|
||||
case UNKNOWN: tx.setPurpose(Transaction.Purpose.UNKNOWN); break;
|
||||
case USER_PAYMENT: tx.setPurpose(Transaction.Purpose.USER_PAYMENT); break;
|
||||
case KEY_ROTATION: tx.setPurpose(Transaction.Purpose.KEY_ROTATION); break;
|
||||
default: throw new RuntimeException("New purpose serialization not implemented");
|
||||
}
|
||||
} else {
|
||||
// Old wallet: assume a user payment as that's the only reason a new tx would have been created back then.
|
||||
tx.setPurpose(Transaction.Purpose.USER_PAYMENT);
|
||||
}
|
||||
|
||||
// Transaction should now be complete.
|
||||
Sha256Hash protoHash = byteStringToHash(txProto.getHash());
|
||||
if (!tx.getHash().equals(protoHash))
|
||||
|
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.bitcoin.wallet;
|
||||
|
||||
import com.google.bitcoin.core.*;
|
||||
import com.google.bitcoin.script.Script;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* A coin selector that takes all coins assigned to keys created before the given timestamp.
|
||||
* Used as part of the implementation of {@link Wallet#setKeyRotationTime(java.util.Date)}.
|
||||
*/
|
||||
public class KeyTimeCoinSelector implements Wallet.CoinSelector {
|
||||
private static final Logger log = LoggerFactory.getLogger(KeyTimeCoinSelector.class);
|
||||
|
||||
/** A number of inputs chosen to avoid hitting {@link com.google.bitcoin.core.Transaction.MAX_STANDARD_TX_SIZE} */
|
||||
public static final int MAX_SIMULTANEOUS_INPUTS = 600;
|
||||
|
||||
private final long unixTimeSeconds;
|
||||
private final Wallet wallet;
|
||||
private final boolean ignorePending;
|
||||
|
||||
public KeyTimeCoinSelector(Wallet wallet, long unixTimeSeconds, boolean ignorePending) {
|
||||
this.unixTimeSeconds = unixTimeSeconds;
|
||||
this.wallet = wallet;
|
||||
this.ignorePending = ignorePending;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wallet.CoinSelection select(BigInteger target, LinkedList<TransactionOutput> candidates) {
|
||||
try {
|
||||
LinkedList<TransactionOutput> gathered = Lists.newLinkedList();
|
||||
BigInteger valueGathered = BigInteger.ZERO;
|
||||
for (TransactionOutput output : candidates) {
|
||||
if (ignorePending && !isConfirmed(output))
|
||||
continue;
|
||||
// Find the key that controls output, assuming it's a regular pay-to-pubkey or pay-to-address output.
|
||||
// We ignore any other kind of exotic output on the assumption we can't spend it ourselves.
|
||||
final Script scriptPubKey = output.getScriptPubKey();
|
||||
ECKey controllingKey;
|
||||
if (scriptPubKey.isSentToRawPubKey()) {
|
||||
controllingKey = wallet.findKeyFromPubKey(scriptPubKey.getPubKey());
|
||||
} else if (scriptPubKey.isSentToAddress()) {
|
||||
controllingKey = wallet.findKeyFromPubHash(scriptPubKey.getPubKeyHash());
|
||||
} else {
|
||||
log.info("Skipping tx output {} because it's not of simple form.", output);
|
||||
continue;
|
||||
}
|
||||
if (controllingKey.getCreationTimeSeconds() >= unixTimeSeconds) continue;
|
||||
// It's older than the cutoff time so select.
|
||||
valueGathered = valueGathered.add(output.getValue());
|
||||
gathered.push(output);
|
||||
if (gathered.size() >= MAX_SIMULTANEOUS_INPUTS) {
|
||||
log.warn("Reached {} inputs, going further would yield a tx that is too large, stopping here.", gathered.size());
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new Wallet.CoinSelection(valueGathered, gathered);
|
||||
} catch (ScriptException e) {
|
||||
throw new RuntimeException(e); // We should never have problems understanding scripts in our wallet.
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isConfirmed(TransactionOutput output) {
|
||||
return output.getParentTransaction().getConfidence().getConfidenceType().equals(TransactionConfidence.ConfidenceType.BUILDING);
|
||||
}
|
||||
}
|
@ -3955,6 +3955,10 @@ public final class Protos {
|
||||
boolean hasConfidence();
|
||||
org.bitcoinj.wallet.Protos.TransactionConfidence getConfidence();
|
||||
org.bitcoinj.wallet.Protos.TransactionConfidenceOrBuilder getConfidenceOrBuilder();
|
||||
|
||||
// optional .wallet.Transaction.Purpose purpose = 10 [default = UNKNOWN];
|
||||
boolean hasPurpose();
|
||||
org.bitcoinj.wallet.Protos.Transaction.Purpose getPurpose();
|
||||
}
|
||||
public static final class Transaction extends
|
||||
com.google.protobuf.GeneratedMessage
|
||||
@ -4065,6 +4069,78 @@ public final class Protos {
|
||||
// @@protoc_insertion_point(enum_scope:wallet.Transaction.Pool)
|
||||
}
|
||||
|
||||
public enum Purpose
|
||||
implements com.google.protobuf.ProtocolMessageEnum {
|
||||
UNKNOWN(0, 0),
|
||||
USER_PAYMENT(1, 1),
|
||||
KEY_ROTATION(2, 2),
|
||||
;
|
||||
|
||||
public static final int UNKNOWN_VALUE = 0;
|
||||
public static final int USER_PAYMENT_VALUE = 1;
|
||||
public static final int KEY_ROTATION_VALUE = 2;
|
||||
|
||||
|
||||
public final int getNumber() { return value; }
|
||||
|
||||
public static Purpose valueOf(int value) {
|
||||
switch (value) {
|
||||
case 0: return UNKNOWN;
|
||||
case 1: return USER_PAYMENT;
|
||||
case 2: return KEY_ROTATION;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static com.google.protobuf.Internal.EnumLiteMap<Purpose>
|
||||
internalGetValueMap() {
|
||||
return internalValueMap;
|
||||
}
|
||||
private static com.google.protobuf.Internal.EnumLiteMap<Purpose>
|
||||
internalValueMap =
|
||||
new com.google.protobuf.Internal.EnumLiteMap<Purpose>() {
|
||||
public Purpose findValueByNumber(int number) {
|
||||
return Purpose.valueOf(number);
|
||||
}
|
||||
};
|
||||
|
||||
public final com.google.protobuf.Descriptors.EnumValueDescriptor
|
||||
getValueDescriptor() {
|
||||
return getDescriptor().getValues().get(index);
|
||||
}
|
||||
public final com.google.protobuf.Descriptors.EnumDescriptor
|
||||
getDescriptorForType() {
|
||||
return getDescriptor();
|
||||
}
|
||||
public static final com.google.protobuf.Descriptors.EnumDescriptor
|
||||
getDescriptor() {
|
||||
return org.bitcoinj.wallet.Protos.Transaction.getDescriptor().getEnumTypes().get(1);
|
||||
}
|
||||
|
||||
private static final Purpose[] VALUES = {
|
||||
UNKNOWN, USER_PAYMENT, KEY_ROTATION,
|
||||
};
|
||||
|
||||
public static Purpose valueOf(
|
||||
com.google.protobuf.Descriptors.EnumValueDescriptor desc) {
|
||||
if (desc.getType() != getDescriptor()) {
|
||||
throw new java.lang.IllegalArgumentException(
|
||||
"EnumValueDescriptor is not for this type.");
|
||||
}
|
||||
return VALUES[desc.getIndex()];
|
||||
}
|
||||
|
||||
private final int index;
|
||||
private final int value;
|
||||
|
||||
private Purpose(int index, int value) {
|
||||
this.index = index;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
// @@protoc_insertion_point(enum_scope:wallet.Transaction.Purpose)
|
||||
}
|
||||
|
||||
private int bitField0_;
|
||||
// required int32 version = 1;
|
||||
public static final int VERSION_FIELD_NUMBER = 1;
|
||||
@ -4185,6 +4261,16 @@ public final class Protos {
|
||||
return confidence_;
|
||||
}
|
||||
|
||||
// optional .wallet.Transaction.Purpose purpose = 10 [default = UNKNOWN];
|
||||
public static final int PURPOSE_FIELD_NUMBER = 10;
|
||||
private org.bitcoinj.wallet.Protos.Transaction.Purpose purpose_;
|
||||
public boolean hasPurpose() {
|
||||
return ((bitField0_ & 0x00000040) == 0x00000040);
|
||||
}
|
||||
public org.bitcoinj.wallet.Protos.Transaction.Purpose getPurpose() {
|
||||
return purpose_;
|
||||
}
|
||||
|
||||
private void initFields() {
|
||||
version_ = 0;
|
||||
hash_ = com.google.protobuf.ByteString.EMPTY;
|
||||
@ -4195,6 +4281,7 @@ public final class Protos {
|
||||
transactionOutput_ = java.util.Collections.emptyList();
|
||||
blockHash_ = java.util.Collections.emptyList();;
|
||||
confidence_ = org.bitcoinj.wallet.Protos.TransactionConfidence.getDefaultInstance();
|
||||
purpose_ = org.bitcoinj.wallet.Protos.Transaction.Purpose.UNKNOWN;
|
||||
}
|
||||
private byte memoizedIsInitialized = -1;
|
||||
public final boolean isInitialized() {
|
||||
@ -4261,6 +4348,9 @@ public final class Protos {
|
||||
if (((bitField0_ & 0x00000020) == 0x00000020)) {
|
||||
output.writeMessage(9, confidence_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000040) == 0x00000040)) {
|
||||
output.writeEnum(10, purpose_.getNumber());
|
||||
}
|
||||
getUnknownFields().writeTo(output);
|
||||
}
|
||||
|
||||
@ -4311,6 +4401,10 @@ public final class Protos {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeMessageSize(9, confidence_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000040) == 0x00000040)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeEnumSize(10, purpose_.getNumber());
|
||||
}
|
||||
size += getUnknownFields().getSerializedSize();
|
||||
memoizedSerializedSize = size;
|
||||
return size;
|
||||
@ -4468,6 +4562,8 @@ public final class Protos {
|
||||
confidenceBuilder_.clear();
|
||||
}
|
||||
bitField0_ = (bitField0_ & ~0x00000100);
|
||||
purpose_ = org.bitcoinj.wallet.Protos.Transaction.Purpose.UNKNOWN;
|
||||
bitField0_ = (bitField0_ & ~0x00000200);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -4557,6 +4653,10 @@ public final class Protos {
|
||||
} else {
|
||||
result.confidence_ = confidenceBuilder_.build();
|
||||
}
|
||||
if (((from_bitField0_ & 0x00000200) == 0x00000200)) {
|
||||
to_bitField0_ |= 0x00000040;
|
||||
}
|
||||
result.purpose_ = purpose_;
|
||||
result.bitField0_ = to_bitField0_;
|
||||
onBuilt();
|
||||
return result;
|
||||
@ -4653,6 +4753,9 @@ public final class Protos {
|
||||
if (other.hasConfidence()) {
|
||||
mergeConfidence(other.getConfidence());
|
||||
}
|
||||
if (other.hasPurpose()) {
|
||||
setPurpose(other.getPurpose());
|
||||
}
|
||||
this.mergeUnknownFields(other.getUnknownFields());
|
||||
return this;
|
||||
}
|
||||
@ -4767,6 +4870,17 @@ public final class Protos {
|
||||
setConfidence(subBuilder.buildPartial());
|
||||
break;
|
||||
}
|
||||
case 80: {
|
||||
int rawValue = input.readEnum();
|
||||
org.bitcoinj.wallet.Protos.Transaction.Purpose value = org.bitcoinj.wallet.Protos.Transaction.Purpose.valueOf(rawValue);
|
||||
if (value == null) {
|
||||
unknownFields.mergeVarintField(10, rawValue);
|
||||
} else {
|
||||
bitField0_ |= 0x00000200;
|
||||
purpose_ = value;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5397,6 +5511,30 @@ public final class Protos {
|
||||
return confidenceBuilder_;
|
||||
}
|
||||
|
||||
// optional .wallet.Transaction.Purpose purpose = 10 [default = UNKNOWN];
|
||||
private org.bitcoinj.wallet.Protos.Transaction.Purpose purpose_ = org.bitcoinj.wallet.Protos.Transaction.Purpose.UNKNOWN;
|
||||
public boolean hasPurpose() {
|
||||
return ((bitField0_ & 0x00000200) == 0x00000200);
|
||||
}
|
||||
public org.bitcoinj.wallet.Protos.Transaction.Purpose getPurpose() {
|
||||
return purpose_;
|
||||
}
|
||||
public Builder setPurpose(org.bitcoinj.wallet.Protos.Transaction.Purpose value) {
|
||||
if (value == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
bitField0_ |= 0x00000200;
|
||||
purpose_ = value;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
public Builder clearPurpose() {
|
||||
bitField0_ = (bitField0_ & ~0x00000200);
|
||||
purpose_ = org.bitcoinj.wallet.Protos.Transaction.Purpose.UNKNOWN;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
|
||||
// @@protoc_insertion_point(builder_scope:wallet.Transaction)
|
||||
}
|
||||
|
||||
@ -6505,6 +6643,10 @@ public final class Protos {
|
||||
// optional string description = 11;
|
||||
boolean hasDescription();
|
||||
String getDescription();
|
||||
|
||||
// optional uint64 key_rotation_time = 13;
|
||||
boolean hasKeyRotationTime();
|
||||
long getKeyRotationTime();
|
||||
}
|
||||
public static final class Wallet extends
|
||||
com.google.protobuf.GeneratedMessage
|
||||
@ -6784,6 +6926,16 @@ public final class Protos {
|
||||
}
|
||||
}
|
||||
|
||||
// optional uint64 key_rotation_time = 13;
|
||||
public static final int KEY_ROTATION_TIME_FIELD_NUMBER = 13;
|
||||
private long keyRotationTime_;
|
||||
public boolean hasKeyRotationTime() {
|
||||
return ((bitField0_ & 0x00000080) == 0x00000080);
|
||||
}
|
||||
public long getKeyRotationTime() {
|
||||
return keyRotationTime_;
|
||||
}
|
||||
|
||||
private void initFields() {
|
||||
networkIdentifier_ = "";
|
||||
lastSeenBlockHash_ = com.google.protobuf.ByteString.EMPTY;
|
||||
@ -6795,6 +6947,7 @@ public final class Protos {
|
||||
version_ = 0;
|
||||
extension_ = java.util.Collections.emptyList();
|
||||
description_ = "";
|
||||
keyRotationTime_ = 0L;
|
||||
}
|
||||
private byte memoizedIsInitialized = -1;
|
||||
public final boolean isInitialized() {
|
||||
@ -6866,6 +7019,9 @@ public final class Protos {
|
||||
if (((bitField0_ & 0x00000004) == 0x00000004)) {
|
||||
output.writeUInt32(12, lastSeenBlockHeight_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000080) == 0x00000080)) {
|
||||
output.writeUInt64(13, keyRotationTime_);
|
||||
}
|
||||
getUnknownFields().writeTo(output);
|
||||
}
|
||||
|
||||
@ -6915,6 +7071,10 @@ public final class Protos {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeUInt32Size(12, lastSeenBlockHeight_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000080) == 0x00000080)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeUInt64Size(13, keyRotationTime_);
|
||||
}
|
||||
size += getUnknownFields().getSerializedSize();
|
||||
memoizedSerializedSize = size;
|
||||
return size;
|
||||
@ -7079,6 +7239,8 @@ public final class Protos {
|
||||
}
|
||||
description_ = "";
|
||||
bitField0_ = (bitField0_ & ~0x00000200);
|
||||
keyRotationTime_ = 0L;
|
||||
bitField0_ = (bitField0_ & ~0x00000400);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -7176,6 +7338,10 @@ public final class Protos {
|
||||
to_bitField0_ |= 0x00000040;
|
||||
}
|
||||
result.description_ = description_;
|
||||
if (((from_bitField0_ & 0x00000400) == 0x00000400)) {
|
||||
to_bitField0_ |= 0x00000080;
|
||||
}
|
||||
result.keyRotationTime_ = keyRotationTime_;
|
||||
result.bitField0_ = to_bitField0_;
|
||||
onBuilt();
|
||||
return result;
|
||||
@ -7291,6 +7457,9 @@ public final class Protos {
|
||||
if (other.hasDescription()) {
|
||||
setDescription(other.getDescription());
|
||||
}
|
||||
if (other.hasKeyRotationTime()) {
|
||||
setKeyRotationTime(other.getKeyRotationTime());
|
||||
}
|
||||
this.mergeUnknownFields(other.getUnknownFields());
|
||||
return this;
|
||||
}
|
||||
@ -7413,6 +7582,11 @@ public final class Protos {
|
||||
lastSeenBlockHeight_ = input.readUInt32();
|
||||
break;
|
||||
}
|
||||
case 104: {
|
||||
bitField0_ |= 0x00000400;
|
||||
keyRotationTime_ = input.readUInt64();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8229,6 +8403,27 @@ public final class Protos {
|
||||
onChanged();
|
||||
}
|
||||
|
||||
// optional uint64 key_rotation_time = 13;
|
||||
private long keyRotationTime_ ;
|
||||
public boolean hasKeyRotationTime() {
|
||||
return ((bitField0_ & 0x00000400) == 0x00000400);
|
||||
}
|
||||
public long getKeyRotationTime() {
|
||||
return keyRotationTime_;
|
||||
}
|
||||
public Builder setKeyRotationTime(long value) {
|
||||
bitField0_ |= 0x00000400;
|
||||
keyRotationTime_ = value;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
public Builder clearKeyRotationTime() {
|
||||
bitField0_ = (bitField0_ & ~0x00000400);
|
||||
keyRotationTime_ = 0L;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
|
||||
// @@protoc_insertion_point(builder_scope:wallet.Wallet)
|
||||
}
|
||||
|
||||
@ -8326,32 +8521,35 @@ public final class Protos {
|
||||
"\n\010BUILDING\020\001\022\013\n\007PENDING\020\002\022\025\n\021NOT_IN_BEST" +
|
||||
"_CHAIN\020\003\022\010\n\004DEAD\020\004\"A\n\006Source\022\022\n\016SOURCE_U" +
|
||||
"NKNOWN\020\000\022\022\n\016SOURCE_NETWORK\020\001\022\017\n\013SOURCE_S" +
|
||||
"ELF\020\002\"\211\003\n\013Transaction\022\017\n\007version\030\001 \002(\005\022\014" +
|
||||
"ELF\020\002\"\374\003\n\013Transaction\022\017\n\007version\030\001 \002(\005\022\014" +
|
||||
"\n\004hash\030\002 \002(\014\022&\n\004pool\030\003 \001(\0162\030.wallet.Tran" +
|
||||
"saction.Pool\022\021\n\tlock_time\030\004 \001(\r\022\022\n\nupdat",
|
||||
"ed_at\030\005 \001(\003\0223\n\021transaction_input\030\006 \003(\0132\030" +
|
||||
".wallet.TransactionInput\0225\n\022transaction_" +
|
||||
"output\030\007 \003(\0132\031.wallet.TransactionOutput\022" +
|
||||
"\022\n\nblock_hash\030\010 \003(\014\0221\n\nconfidence\030\t \001(\0132" +
|
||||
"\035.wallet.TransactionConfidence\"Y\n\004Pool\022\013" +
|
||||
"\n\007UNSPENT\020\004\022\t\n\005SPENT\020\005\022\014\n\010INACTIVE\020\002\022\010\n\004" +
|
||||
"DEAD\020\n\022\013\n\007PENDING\020\020\022\024\n\020PENDING_INACTIVE\020" +
|
||||
"\022\"N\n\020ScryptParameters\022\014\n\004salt\030\001 \002(\014\022\020\n\001n" +
|
||||
"\030\002 \001(\003:\00516384\022\014\n\001r\030\003 \001(\005:\0018\022\014\n\001p\030\004 \001(\005:\001" +
|
||||
"1\"8\n\tExtension\022\n\n\002id\030\001 \002(\t\022\014\n\004data\030\002 \002(\014",
|
||||
"\022\021\n\tmandatory\030\003 \002(\010\"\255\003\n\006Wallet\022\032\n\022networ" +
|
||||
"k_identifier\030\001 \002(\t\022\034\n\024last_seen_block_ha" +
|
||||
"sh\030\002 \001(\014\022\036\n\026last_seen_block_height\030\014 \001(\r" +
|
||||
"\022\030\n\003key\030\003 \003(\0132\013.wallet.Key\022(\n\013transactio" +
|
||||
"n\030\004 \003(\0132\023.wallet.Transaction\022C\n\017encrypti" +
|
||||
"on_type\030\005 \001(\0162\035.wallet.Wallet.Encryption" +
|
||||
"Type:\013UNENCRYPTED\0227\n\025encryption_paramete" +
|
||||
"rs\030\006 \001(\0132\030.wallet.ScryptParameters\022\017\n\007ve" +
|
||||
"rsion\030\007 \001(\005\022$\n\textension\030\n \003(\0132\021.wallet." +
|
||||
"Extension\022\023\n\013description\030\013 \001(\t\";\n\016Encryp",
|
||||
"tionType\022\017\n\013UNENCRYPTED\020\001\022\030\n\024ENCRYPTED_S" +
|
||||
"CRYPT_AES\020\002B\035\n\023org.bitcoinj.walletB\006Prot" +
|
||||
"os"
|
||||
"\035.wallet.TransactionConfidence\0225\n\007purpos" +
|
||||
"e\030\n \001(\0162\033.wallet.Transaction.Purpose:\007UN" +
|
||||
"KNOWN\"Y\n\004Pool\022\013\n\007UNSPENT\020\004\022\t\n\005SPENT\020\005\022\014\n" +
|
||||
"\010INACTIVE\020\002\022\010\n\004DEAD\020\n\022\013\n\007PENDING\020\020\022\024\n\020PE" +
|
||||
"NDING_INACTIVE\020\022\":\n\007Purpose\022\013\n\007UNKNOWN\020\000" +
|
||||
"\022\020\n\014USER_PAYMENT\020\001\022\020\n\014KEY_ROTATION\020\002\"N\n\020",
|
||||
"ScryptParameters\022\014\n\004salt\030\001 \002(\014\022\020\n\001n\030\002 \001(" +
|
||||
"\003:\00516384\022\014\n\001r\030\003 \001(\005:\0018\022\014\n\001p\030\004 \001(\005:\0011\"8\n\t" +
|
||||
"Extension\022\n\n\002id\030\001 \002(\t\022\014\n\004data\030\002 \002(\014\022\021\n\tm" +
|
||||
"andatory\030\003 \002(\010\"\310\003\n\006Wallet\022\032\n\022network_ide" +
|
||||
"ntifier\030\001 \002(\t\022\034\n\024last_seen_block_hash\030\002 " +
|
||||
"\001(\014\022\036\n\026last_seen_block_height\030\014 \001(\r\022\030\n\003k" +
|
||||
"ey\030\003 \003(\0132\013.wallet.Key\022(\n\013transaction\030\004 \003" +
|
||||
"(\0132\023.wallet.Transaction\022C\n\017encryption_ty" +
|
||||
"pe\030\005 \001(\0162\035.wallet.Wallet.EncryptionType:" +
|
||||
"\013UNENCRYPTED\0227\n\025encryption_parameters\030\006 ",
|
||||
"\001(\0132\030.wallet.ScryptParameters\022\017\n\007version" +
|
||||
"\030\007 \001(\005\022$\n\textension\030\n \003(\0132\021.wallet.Exten" +
|
||||
"sion\022\023\n\013description\030\013 \001(\t\022\031\n\021key_rotatio" +
|
||||
"n_time\030\r \001(\004\";\n\016EncryptionType\022\017\n\013UNENCR" +
|
||||
"YPTED\020\001\022\030\n\024ENCRYPTED_SCRYPT_AES\020\002B\035\n\023org" +
|
||||
".bitcoinj.walletB\006Protos"
|
||||
};
|
||||
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
|
||||
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
|
||||
@ -8411,7 +8609,7 @@ public final class Protos {
|
||||
internal_static_wallet_Transaction_fieldAccessorTable = new
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
|
||||
internal_static_wallet_Transaction_descriptor,
|
||||
new java.lang.String[] { "Version", "Hash", "Pool", "LockTime", "UpdatedAt", "TransactionInput", "TransactionOutput", "BlockHash", "Confidence", },
|
||||
new java.lang.String[] { "Version", "Hash", "Pool", "LockTime", "UpdatedAt", "TransactionInput", "TransactionOutput", "BlockHash", "Confidence", "Purpose", },
|
||||
org.bitcoinj.wallet.Protos.Transaction.class,
|
||||
org.bitcoinj.wallet.Protos.Transaction.Builder.class);
|
||||
internal_static_wallet_ScryptParameters_descriptor =
|
||||
@ -8435,7 +8633,7 @@ public final class Protos {
|
||||
internal_static_wallet_Wallet_fieldAccessorTable = new
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
|
||||
internal_static_wallet_Wallet_descriptor,
|
||||
new java.lang.String[] { "NetworkIdentifier", "LastSeenBlockHash", "LastSeenBlockHeight", "Key", "Transaction", "EncryptionType", "EncryptionParameters", "Version", "Extension", "Description", },
|
||||
new java.lang.String[] { "NetworkIdentifier", "LastSeenBlockHash", "LastSeenBlockHeight", "Key", "Transaction", "EncryptionType", "EncryptionParameters", "Version", "Extension", "Description", "KeyRotationTime", },
|
||||
org.bitcoinj.wallet.Protos.Wallet.class,
|
||||
org.bitcoinj.wallet.Protos.Wallet.Builder.class);
|
||||
return null;
|
||||
|
@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.bitcoin.core;
|
||||
|
||||
import com.google.bitcoin.utils.Threading;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public class MockTransactionBroadcaster implements TransactionBroadcaster {
|
||||
private ReentrantLock lock = Threading.lock("mock tx broadcaster");
|
||||
|
||||
public LinkedBlockingQueue<Transaction> broadcasts = new LinkedBlockingQueue<Transaction>();
|
||||
|
||||
public MockTransactionBroadcaster(Wallet wallet) {
|
||||
// This code achieves nothing directly, but it sets up the broadcaster/peergroup > wallet lock ordering
|
||||
// so inversions can be caught.
|
||||
lock.lock();
|
||||
try {
|
||||
wallet.getPendingTransactions();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SettableFuture<Transaction> broadcastTransaction(Transaction tx) {
|
||||
// Use a lock just to catch lock ordering inversions.
|
||||
lock.lock();
|
||||
try {
|
||||
SettableFuture<Transaction> result = SettableFuture.create();
|
||||
broadcasts.put(tx);
|
||||
return result;
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
@ -22,7 +22,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.store.WalletProtobufSerializer;
|
||||
import com.google.bitcoin.utils.Threading;
|
||||
import com.google.bitcoin.wallet.KeyTimeCoinSelector;
|
||||
import com.google.bitcoin.wallet.WalletFiles;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
@ -47,6 +49,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static com.google.bitcoin.core.TestUtils.*;
|
||||
import static com.google.bitcoin.core.TestUtils.makeSolvedTestBlock;
|
||||
import static com.google.bitcoin.core.Utils.CENT;
|
||||
import static com.google.bitcoin.core.Utils.bitcoinValueToFriendlyString;
|
||||
import static com.google.bitcoin.core.Utils.toNanoCoins;
|
||||
import static org.junit.Assert.*;
|
||||
@ -163,6 +166,7 @@ public class WalletTest extends TestWithWallet {
|
||||
assertEquals("Wrong number of UNSPENT.3", 1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
|
||||
assertEquals("Wrong number of ALL.3", 1, wallet.getPoolSize(WalletTransaction.Pool.ALL));
|
||||
assertEquals(TransactionConfidence.Source.SELF, t2.getConfidence().getSource());
|
||||
assertEquals(Transaction.Purpose.USER_PAYMENT, t2.getPurpose());
|
||||
assertEquals(wallet.getChangeAddress(), t2.getOutput(1).getScriptPubKey().getToAddress(params));
|
||||
|
||||
// Do some basic sanity checks.
|
||||
@ -1230,7 +1234,7 @@ public class WalletTest extends TestWithWallet {
|
||||
wallet.receiveFromBlock(tx4, block, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
|
||||
// Simple test to make sure if we have an ouput < 0.01 we get a fee
|
||||
Transaction spend1 = wallet.createSend(notMyAddr, Utils.CENT.subtract(BigInteger.ONE));
|
||||
Transaction spend1 = wallet.createSend(notMyAddr, CENT.subtract(BigInteger.ONE));
|
||||
assertEquals(2, spend1.getOutputs().size());
|
||||
// We optimize for priority, so the output selected should be the largest one.
|
||||
// We should have paid the default minfee.
|
||||
@ -1238,13 +1242,13 @@ public class WalletTest extends TestWithWallet {
|
||||
Utils.COIN.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE));
|
||||
|
||||
// But not at exactly 0.01
|
||||
Transaction spend2 = wallet.createSend(notMyAddr, Utils.CENT);
|
||||
Transaction spend2 = wallet.createSend(notMyAddr, CENT);
|
||||
assertEquals(2, spend2.getOutputs().size());
|
||||
// We optimize for priority, so the output selected should be the largest one
|
||||
assertEquals(Utils.COIN, spend2.getOutput(0).getValue().add(spend2.getOutput(1).getValue()));
|
||||
|
||||
// ...but not more fee than what we request
|
||||
SendRequest request3 = SendRequest.to(notMyAddr, Utils.CENT.subtract(BigInteger.ONE));
|
||||
SendRequest request3 = SendRequest.to(notMyAddr, CENT.subtract(BigInteger.ONE));
|
||||
request3.fee = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(BigInteger.ONE);
|
||||
assertTrue(wallet.completeTx(request3));
|
||||
assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(BigInteger.ONE), request3.fee);
|
||||
@ -1255,7 +1259,7 @@ public class WalletTest extends TestWithWallet {
|
||||
Utils.COIN.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(BigInteger.ONE)));
|
||||
|
||||
// ...unless we need it
|
||||
SendRequest request4 = SendRequest.to(notMyAddr, Utils.CENT.subtract(BigInteger.ONE));
|
||||
SendRequest request4 = SendRequest.to(notMyAddr, CENT.subtract(BigInteger.ONE));
|
||||
request4.fee = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.subtract(BigInteger.ONE);
|
||||
assertTrue(wallet.completeTx(request4));
|
||||
assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, request4.fee);
|
||||
@ -1265,7 +1269,7 @@ public class WalletTest extends TestWithWallet {
|
||||
assertEquals(spend4.getOutput(0).getValue().add(spend4.getOutput(1).getValue()),
|
||||
Utils.COIN.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE));
|
||||
|
||||
SendRequest request5 = SendRequest.to(notMyAddr, Utils.COIN.subtract(Utils.CENT.subtract(BigInteger.ONE)));
|
||||
SendRequest request5 = SendRequest.to(notMyAddr, Utils.COIN.subtract(CENT.subtract(BigInteger.ONE)));
|
||||
assertTrue(wallet.completeTx(request5));
|
||||
assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, request5.fee);
|
||||
Transaction spend5 = request5.tx;
|
||||
@ -1275,7 +1279,7 @@ public class WalletTest extends TestWithWallet {
|
||||
assertEquals(spend5.getOutput(0).getValue().add(spend5.getOutput(1).getValue()),
|
||||
Utils.COIN.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE));
|
||||
|
||||
SendRequest request6 = SendRequest.to(notMyAddr, Utils.COIN.subtract(Utils.CENT));
|
||||
SendRequest request6 = SendRequest.to(notMyAddr, Utils.COIN.subtract(CENT));
|
||||
assertTrue(wallet.completeTx(request6));
|
||||
assertEquals(BigInteger.ZERO, request6.fee);
|
||||
Transaction spend6 = request6.tx;
|
||||
@ -1284,8 +1288,8 @@ public class WalletTest extends TestWithWallet {
|
||||
// We optimize for priority, so the output selected should be the largest one
|
||||
assertEquals(Utils.COIN, spend6.getOutput(0).getValue().add(spend6.getOutput(1).getValue()));
|
||||
|
||||
SendRequest request7 = SendRequest.to(notMyAddr, Utils.COIN.subtract(Utils.CENT.subtract(BigInteger.valueOf(2)).multiply(BigInteger.valueOf(2))));
|
||||
request7.tx.addOutput(Utils.CENT.subtract(BigInteger.ONE), notMyAddr);
|
||||
SendRequest request7 = SendRequest.to(notMyAddr, Utils.COIN.subtract(CENT.subtract(BigInteger.valueOf(2)).multiply(BigInteger.valueOf(2))));
|
||||
request7.tx.addOutput(CENT.subtract(BigInteger.ONE), notMyAddr);
|
||||
assertTrue(wallet.completeTx(request7));
|
||||
assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, request7.fee);
|
||||
Transaction spend7 = request7.tx;
|
||||
@ -1340,9 +1344,9 @@ public class WalletTest extends TestWithWallet {
|
||||
|
||||
// Remove the coin from our wallet
|
||||
wallet.commitTx(spend11);
|
||||
Transaction tx5 = createFakeTx(params, Utils.CENT, myAddress);
|
||||
Transaction tx5 = createFakeTx(params, CENT, myAddress);
|
||||
wallet.receiveFromBlock(tx5, block, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertEquals(Utils.CENT, wallet.getBalance());
|
||||
assertEquals(CENT, wallet.getBalance());
|
||||
|
||||
// Now test coin selection properly selects coin*depth
|
||||
for (int i = 0; i < 100; i++) {
|
||||
@ -1357,28 +1361,28 @@ public class WalletTest extends TestWithWallet {
|
||||
assertTrue(tx6.getOutput(0).isMine(wallet) && tx6.getOutput(0).isAvailableForSpending() && tx6.getConfidence().getDepthInBlocks() == 1);
|
||||
|
||||
// tx5 and tx6 have exactly the same coin*depth, so the larger should be selected...
|
||||
Transaction spend12 = wallet.createSend(notMyAddr, Utils.CENT);
|
||||
Transaction spend12 = wallet.createSend(notMyAddr, CENT);
|
||||
assertTrue(spend12.getOutputs().size() == 2 && spend12.getOutput(0).getValue().add(spend12.getOutput(1).getValue()).equals(Utils.COIN));
|
||||
|
||||
wallet.notifyNewBestBlock(block);
|
||||
assertTrue(tx5.getOutput(0).isMine(wallet) && tx5.getOutput(0).isAvailableForSpending() && tx5.getConfidence().getDepthInBlocks() == 101);
|
||||
assertTrue(tx6.getOutput(0).isMine(wallet) && tx6.getOutput(0).isAvailableForSpending() && tx6.getConfidence().getDepthInBlocks() == 1);
|
||||
// Now tx5 has slightly higher coin*depth than tx6...
|
||||
Transaction spend13 = wallet.createSend(notMyAddr, Utils.CENT);
|
||||
assertTrue(spend13.getOutputs().size() == 1 && spend13.getOutput(0).getValue().equals(Utils.CENT));
|
||||
Transaction spend13 = wallet.createSend(notMyAddr, CENT);
|
||||
assertTrue(spend13.getOutputs().size() == 1 && spend13.getOutput(0).getValue().equals(CENT));
|
||||
|
||||
block = new StoredBlock(makeSolvedTestBlock(blockStore, notMyAddr), BigInteger.ONE, 1);
|
||||
wallet.notifyNewBestBlock(block);
|
||||
assertTrue(tx5.getOutput(0).isMine(wallet) && tx5.getOutput(0).isAvailableForSpending() && tx5.getConfidence().getDepthInBlocks() == 102);
|
||||
assertTrue(tx6.getOutput(0).isMine(wallet) && tx6.getOutput(0).isAvailableForSpending() && tx6.getConfidence().getDepthInBlocks() == 2);
|
||||
// Now tx6 has higher coin*depth than tx5...
|
||||
Transaction spend14 = wallet.createSend(notMyAddr, Utils.CENT);
|
||||
Transaction spend14 = wallet.createSend(notMyAddr, CENT);
|
||||
assertTrue(spend14.getOutputs().size() == 2 && spend14.getOutput(0).getValue().add(spend14.getOutput(1).getValue()).equals(Utils.COIN));
|
||||
|
||||
// Now test feePerKb
|
||||
SendRequest request15 = SendRequest.to(notMyAddr, Utils.CENT);
|
||||
SendRequest request15 = SendRequest.to(notMyAddr, CENT);
|
||||
for (int i = 0; i < 29; i++)
|
||||
request15.tx.addOutput(Utils.CENT, notMyAddr);
|
||||
request15.tx.addOutput(CENT, notMyAddr);
|
||||
assertTrue(request15.tx.bitcoinSerialize().length > 1000);
|
||||
request15.feePerKb = BigInteger.ONE;
|
||||
assertTrue(wallet.completeTx(request15));
|
||||
@ -1392,10 +1396,10 @@ public class WalletTest extends TestWithWallet {
|
||||
outValue15 = outValue15.add(out.getValue());
|
||||
assertEquals(Utils.COIN.subtract(BigInteger.valueOf(2)), outValue15);
|
||||
|
||||
SendRequest request16 = SendRequest.to(notMyAddr, Utils.CENT);
|
||||
SendRequest request16 = SendRequest.to(notMyAddr, CENT);
|
||||
request16.feePerKb = BigInteger.ZERO;
|
||||
for (int i = 0; i < 29; i++)
|
||||
request16.tx.addOutput(Utils.CENT, notMyAddr);
|
||||
request16.tx.addOutput(CENT, notMyAddr);
|
||||
assertTrue(request16.tx.bitcoinSerialize().length > 1000);
|
||||
assertTrue(wallet.completeTx(request16));
|
||||
// Of course the fee shouldn't be added if feePerKb == 0
|
||||
@ -1409,10 +1413,10 @@ public class WalletTest extends TestWithWallet {
|
||||
assertEquals(Utils.COIN, outValue16);
|
||||
|
||||
// Create a transaction whose max size could be up to 999 (if signatures were maximum size)
|
||||
SendRequest request17 = SendRequest.to(notMyAddr, Utils.CENT);
|
||||
SendRequest request17 = SendRequest.to(notMyAddr, CENT);
|
||||
for (int i = 0; i < 22; i++)
|
||||
request17.tx.addOutput(Utils.CENT, notMyAddr);
|
||||
request17.tx.addOutput(new TransactionOutput(params, request17.tx, Utils.CENT, new byte[15]));
|
||||
request17.tx.addOutput(CENT, notMyAddr);
|
||||
request17.tx.addOutput(new TransactionOutput(params, request17.tx, CENT, new byte[15]));
|
||||
request17.feePerKb = BigInteger.ONE;
|
||||
assertTrue(wallet.completeTx(request17));
|
||||
assertEquals(BigInteger.ONE, request17.fee);
|
||||
@ -1437,10 +1441,10 @@ public class WalletTest extends TestWithWallet {
|
||||
assertEquals(Utils.COIN.subtract(BigInteger.ONE), outValue17);
|
||||
|
||||
// Create a transaction who's max size could be up to 1001 (if signatures were maximum size)
|
||||
SendRequest request18 = SendRequest.to(notMyAddr, Utils.CENT);
|
||||
SendRequest request18 = SendRequest.to(notMyAddr, CENT);
|
||||
for (int i = 0; i < 22; i++)
|
||||
request18.tx.addOutput(Utils.CENT, notMyAddr);
|
||||
request18.tx.addOutput(new TransactionOutput(params, request18.tx, Utils.CENT, new byte[17]));
|
||||
request18.tx.addOutput(CENT, notMyAddr);
|
||||
request18.tx.addOutput(new TransactionOutput(params, request18.tx, CENT, new byte[17]));
|
||||
request18.feePerKb = BigInteger.ONE;
|
||||
assertTrue(wallet.completeTx(request18));
|
||||
assertEquals(BigInteger.valueOf(2), request18.fee);
|
||||
@ -1463,11 +1467,11 @@ public class WalletTest extends TestWithWallet {
|
||||
assertEquals(outValue18, Utils.COIN.subtract(BigInteger.valueOf(2)));
|
||||
|
||||
// Now create a transaction that will spend COIN + fee, which makes it require both inputs
|
||||
assertEquals(wallet.getBalance(), Utils.CENT.add(Utils.COIN));
|
||||
SendRequest request19 = SendRequest.to(notMyAddr, Utils.CENT);
|
||||
assertEquals(wallet.getBalance(), CENT.add(Utils.COIN));
|
||||
SendRequest request19 = SendRequest.to(notMyAddr, CENT);
|
||||
request19.feePerKb = BigInteger.ZERO;
|
||||
for (int i = 0; i < 99; i++)
|
||||
request19.tx.addOutput(Utils.CENT, notMyAddr);
|
||||
request19.tx.addOutput(CENT, notMyAddr);
|
||||
// If we send now, we shouldn't need a fee and should only have to spend our COIN
|
||||
assertTrue(wallet.completeTx(request19));
|
||||
assertEquals(BigInteger.ZERO, request19.fee);
|
||||
@ -1485,14 +1489,14 @@ public class WalletTest extends TestWithWallet {
|
||||
outValue19 = outValue19.add(out.getValue());
|
||||
// But now our change output is CENT-minfee, so we have to pay min fee
|
||||
// Change this assert when we eventually randomize output order
|
||||
assertEquals(request19.tx.getOutput(request19.tx.getOutputs().size() - 1).getValue(), Utils.CENT.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE));
|
||||
assertEquals(outValue19, Utils.COIN.add(Utils.CENT).subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE));
|
||||
assertEquals(request19.tx.getOutput(request19.tx.getOutputs().size() - 1).getValue(), CENT.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE));
|
||||
assertEquals(outValue19, Utils.COIN.add(CENT).subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE));
|
||||
|
||||
// Create another transaction that will spend COIN + fee, which makes it require both inputs
|
||||
SendRequest request20 = SendRequest.to(notMyAddr, Utils.CENT);
|
||||
SendRequest request20 = SendRequest.to(notMyAddr, CENT);
|
||||
request20.feePerKb = BigInteger.ZERO;
|
||||
for (int i = 0; i < 99; i++)
|
||||
request20.tx.addOutput(Utils.CENT, notMyAddr);
|
||||
request20.tx.addOutput(CENT, notMyAddr);
|
||||
// If we send now, we shouldn't have a fee and should only have to spend our COIN
|
||||
assertTrue(wallet.completeTx(request20));
|
||||
assertEquals(BigInteger.ZERO, request20.fee);
|
||||
@ -1510,15 +1514,15 @@ public class WalletTest extends TestWithWallet {
|
||||
for (TransactionOutput out : request20.tx.getOutputs())
|
||||
outValue20 = outValue20.add(out.getValue());
|
||||
// This time the fee we wanted to pay was more, so that should be what we paid
|
||||
assertEquals(outValue20, Utils.COIN.add(Utils.CENT).subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(BigInteger.valueOf(4))));
|
||||
assertEquals(outValue20, Utils.COIN.add(CENT).subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(BigInteger.valueOf(4))));
|
||||
|
||||
// Same as request 19, but make the change 0 (so it doesnt force fee) and make us require min fee as a
|
||||
// result of an output < CENT.
|
||||
SendRequest request21 = SendRequest.to(notMyAddr, Utils.CENT);
|
||||
SendRequest request21 = SendRequest.to(notMyAddr, CENT);
|
||||
request21.feePerKb = BigInteger.ZERO;
|
||||
for (int i = 0; i < 99; i++)
|
||||
request21.tx.addOutput(Utils.CENT, notMyAddr);
|
||||
request21.tx.addOutput(Utils.CENT.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), notMyAddr);
|
||||
request21.tx.addOutput(CENT, notMyAddr);
|
||||
request21.tx.addOutput(CENT.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), notMyAddr);
|
||||
// If we send without a feePerKb, we should still require REFERENCE_DEFAULT_MIN_TX_FEE because we have an output < 0.01
|
||||
assertTrue(wallet.completeTx(request21));
|
||||
assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, request21.fee);
|
||||
@ -1526,14 +1530,14 @@ public class WalletTest extends TestWithWallet {
|
||||
BigInteger outValue21 = BigInteger.ZERO;
|
||||
for (TransactionOutput out : request21.tx.getOutputs())
|
||||
outValue21 = outValue21.add(out.getValue());
|
||||
assertEquals(outValue21, Utils.COIN.add(Utils.CENT).subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE));
|
||||
assertEquals(outValue21, Utils.COIN.add(CENT).subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE));
|
||||
|
||||
// Test feePerKb when we aren't using ensureMinRequiredFee
|
||||
// Same as request 19
|
||||
SendRequest request25 = SendRequest.to(notMyAddr, Utils.CENT);
|
||||
SendRequest request25 = SendRequest.to(notMyAddr, CENT);
|
||||
request25.feePerKb = BigInteger.ZERO;
|
||||
for (int i = 0; i < 70; i++)
|
||||
request25.tx.addOutput(Utils.CENT, notMyAddr);
|
||||
request25.tx.addOutput(CENT, notMyAddr);
|
||||
// If we send now, we shouldn't need a fee and should only have to spend our COIN
|
||||
assertTrue(wallet.completeTx(request25));
|
||||
assertEquals(BigInteger.ZERO, request25.fee);
|
||||
@ -1542,10 +1546,10 @@ public class WalletTest extends TestWithWallet {
|
||||
// Now reset request19 and give it a fee per kb
|
||||
request25.tx.clearInputs();
|
||||
request25 = SendRequest.forTx(request25.tx);
|
||||
request25.feePerKb = Utils.CENT.divide(BigInteger.valueOf(3));
|
||||
request25.feePerKb = CENT.divide(BigInteger.valueOf(3));
|
||||
request25.ensureMinRequiredFee = false;
|
||||
assertTrue(wallet.completeTx(request25));
|
||||
assertEquals(Utils.CENT.subtract(BigInteger.ONE), request25.fee);
|
||||
assertEquals(CENT.subtract(BigInteger.ONE), request25.fee);
|
||||
assertEquals(2, request25.tx.getInputs().size());
|
||||
BigInteger outValue25 = BigInteger.ZERO;
|
||||
for (TransactionOutput out : request25.tx.getOutputs())
|
||||
@ -1558,17 +1562,17 @@ public class WalletTest extends TestWithWallet {
|
||||
|
||||
// Spend our CENT output.
|
||||
Transaction spendTx5 = new Transaction(params);
|
||||
spendTx5.addOutput(Utils.CENT, notMyAddr);
|
||||
spendTx5.addOutput(CENT, notMyAddr);
|
||||
spendTx5.addInput(tx5.getOutput(0));
|
||||
spendTx5.signInputs(SigHash.ALL, wallet);
|
||||
wallet.receiveFromBlock(spendTx5, block, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertEquals(Utils.COIN, wallet.getBalance());
|
||||
|
||||
// Ensure change is discarded if it results in a fee larger than the chain (same as 8 and 9 but with feePerKb)
|
||||
SendRequest request26 = SendRequest.to(notMyAddr, Utils.CENT);
|
||||
SendRequest request26 = SendRequest.to(notMyAddr, CENT);
|
||||
for (int i = 0; i < 98; i++)
|
||||
request26.tx.addOutput(Utils.CENT, notMyAddr);
|
||||
request26.tx.addOutput(Utils.CENT.subtract(
|
||||
request26.tx.addOutput(CENT, notMyAddr);
|
||||
request26.tx.addOutput(CENT.subtract(
|
||||
Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(Transaction.MIN_NONDUST_OUTPUT)), notMyAddr);
|
||||
assertTrue(request26.tx.bitcoinSerialize().length > 1000);
|
||||
request26.feePerKb = BigInteger.ONE;
|
||||
@ -1597,14 +1601,14 @@ public class WalletTest extends TestWithWallet {
|
||||
// Generate a ton of small outputs
|
||||
StoredBlock block = new StoredBlock(makeSolvedTestBlock(blockStore, notMyAddr), BigInteger.ONE, 1);
|
||||
int i = 0;
|
||||
while (i <= Utils.CENT.divide(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).longValue()) {
|
||||
while (i <= CENT.divide(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).longValue()) {
|
||||
Transaction tx = createFakeTxWithChangeAddress(params, Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, myAddress, notMyAddr);
|
||||
tx.getInput(0).setSequenceNumber(i++); // Keep every transaction unique
|
||||
wallet.receiveFromBlock(tx, block, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
}
|
||||
|
||||
// Create a spend that will throw away change (category 3 type 2 in which the change causes fee which is worth more than change)
|
||||
SendRequest request1 = SendRequest.to(notMyAddr, Utils.CENT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).subtract(BigInteger.ONE));
|
||||
SendRequest request1 = SendRequest.to(notMyAddr, CENT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).subtract(BigInteger.ONE));
|
||||
assertTrue(wallet.completeTx(request1));
|
||||
assertEquals(BigInteger.ONE, request1.fee);
|
||||
assertEquals(request1.tx.getInputs().size(), i); // We should have spent all inputs
|
||||
@ -1615,7 +1619,7 @@ public class WalletTest extends TestWithWallet {
|
||||
wallet.receiveFromBlock(tx1, block, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
|
||||
// ... and create a spend that will throw away change (category 3 type 1 in which the change causes dust output)
|
||||
SendRequest request2 = SendRequest.to(notMyAddr, Utils.CENT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).subtract(BigInteger.ONE));
|
||||
SendRequest request2 = SendRequest.to(notMyAddr, CENT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).subtract(BigInteger.ONE));
|
||||
assertTrue(wallet.completeTx(request2));
|
||||
assertEquals(BigInteger.ONE, request2.fee);
|
||||
assertEquals(request2.tx.getInputs().size(), i - 1); // We should have spent all inputs - 1
|
||||
@ -1627,27 +1631,27 @@ public class WalletTest extends TestWithWallet {
|
||||
|
||||
// ... and create a spend that will throw away change (category 3 type 1 in which the change causes dust output)
|
||||
// but that also could have been category 2 if it wanted
|
||||
SendRequest request3 = SendRequest.to(notMyAddr, Utils.CENT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).subtract(BigInteger.ONE));
|
||||
SendRequest request3 = SendRequest.to(notMyAddr, CENT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).subtract(BigInteger.ONE));
|
||||
assertTrue(wallet.completeTx(request3));
|
||||
assertEquals(BigInteger.ONE, request3.fee);
|
||||
assertEquals(request3.tx.getInputs().size(), i - 2); // We should have spent all inputs - 2
|
||||
|
||||
//
|
||||
SendRequest request4 = SendRequest.to(notMyAddr, Utils.CENT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).subtract(BigInteger.ONE));
|
||||
SendRequest request4 = SendRequest.to(notMyAddr, CENT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).subtract(BigInteger.ONE));
|
||||
request4.feePerKb = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.divide(BigInteger.valueOf(request3.tx.bitcoinSerialize().length));
|
||||
assertTrue(wallet.completeTx(request4));
|
||||
assertEquals(BigInteger.ONE, request4.fee);
|
||||
assertEquals(request4.tx.getInputs().size(), i - 2); // We should have spent all inputs - 2
|
||||
|
||||
// Give us a few more inputs...
|
||||
while (wallet.getBalance().compareTo(Utils.CENT.shiftLeft(1)) < 0) {
|
||||
while (wallet.getBalance().compareTo(CENT.shiftLeft(1)) < 0) {
|
||||
Transaction tx3 = createFakeTxWithChangeAddress(params, Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, myAddress, notMyAddr);
|
||||
tx3.getInput(0).setSequenceNumber(i++); // Keep every transaction unique
|
||||
wallet.receiveFromBlock(tx3, block, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
}
|
||||
|
||||
// ...that is just slightly less than is needed for category 1
|
||||
SendRequest request5 = SendRequest.to(notMyAddr, Utils.CENT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).subtract(BigInteger.ONE));
|
||||
SendRequest request5 = SendRequest.to(notMyAddr, CENT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).subtract(BigInteger.ONE));
|
||||
assertTrue(wallet.completeTx(request5));
|
||||
assertEquals(BigInteger.ONE, request5.fee);
|
||||
assertEquals(1, request5.tx.getOutputs().size()); // We should have no change output
|
||||
@ -1658,7 +1662,7 @@ public class WalletTest extends TestWithWallet {
|
||||
wallet.receiveFromBlock(tx4, block, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
|
||||
// ... that puts us in category 1 (no fee!)
|
||||
SendRequest request6 = SendRequest.to(notMyAddr, Utils.CENT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).subtract(BigInteger.ONE));
|
||||
SendRequest request6 = SendRequest.to(notMyAddr, CENT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).subtract(BigInteger.ONE));
|
||||
assertTrue(wallet.completeTx(request6));
|
||||
assertEquals(BigInteger.ZERO, request6.fee);
|
||||
assertEquals(2, request6.tx.getOutputs().size()); // We should have a change output
|
||||
@ -1678,14 +1682,14 @@ public class WalletTest extends TestWithWallet {
|
||||
// Generate a ton of small outputs
|
||||
StoredBlock block = new StoredBlock(makeSolvedTestBlock(blockStore, notMyAddr), BigInteger.ONE, 1);
|
||||
int i = 0;
|
||||
while (i <= Utils.CENT.divide(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(BigInteger.TEN)).longValue()) {
|
||||
while (i <= CENT.divide(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(BigInteger.TEN)).longValue()) {
|
||||
Transaction tx = createFakeTxWithChangeAddress(params, Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(BigInteger.TEN), myAddress, notMyAddr);
|
||||
tx.getInput(0).setSequenceNumber(i++); // Keep every transaction unique
|
||||
wallet.receiveFromBlock(tx, block, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
}
|
||||
|
||||
// The selector will choose 2 with MIN_TX_FEE fee
|
||||
SendRequest request1 = SendRequest.to(notMyAddr, Utils.CENT.add(BigInteger.ONE));
|
||||
SendRequest request1 = SendRequest.to(notMyAddr, CENT.add(BigInteger.ONE));
|
||||
assertTrue(wallet.completeTx(request1));
|
||||
assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, request1.fee);
|
||||
assertEquals(request1.tx.getInputs().size(), i); // We should have spent all inputs
|
||||
@ -1705,16 +1709,16 @@ public class WalletTest extends TestWithWallet {
|
||||
StoredBlock block = new StoredBlock(makeSolvedTestBlock(blockStore, notMyAddr), BigInteger.ONE, 1);
|
||||
Transaction tx = createFakeTx(params, Utils.COIN, myAddress);
|
||||
wallet.receiveFromBlock(tx, block, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
Transaction tx2 = createFakeTx(params, Utils.CENT, myAddress);
|
||||
Transaction tx2 = createFakeTx(params, CENT, myAddress);
|
||||
wallet.receiveFromBlock(tx2, block, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
Transaction tx3 = createFakeTx(params, BigInteger.ONE, myAddress);
|
||||
wallet.receiveFromBlock(tx3, block, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
|
||||
// Create a transaction who's max size could be up to 1000 (if signatures were maximum size)
|
||||
SendRequest request1 = SendRequest.to(notMyAddr, Utils.COIN.subtract(Utils.CENT.multiply(BigInteger.valueOf(17))));
|
||||
SendRequest request1 = SendRequest.to(notMyAddr, Utils.COIN.subtract(CENT.multiply(BigInteger.valueOf(17))));
|
||||
for (int i = 0; i < 16; i++)
|
||||
request1.tx.addOutput(Utils.CENT, notMyAddr);
|
||||
request1.tx.addOutput(new TransactionOutput(params, request1.tx, Utils.CENT, new byte[16]));
|
||||
request1.tx.addOutput(CENT, notMyAddr);
|
||||
request1.tx.addOutput(new TransactionOutput(params, request1.tx, CENT, new byte[16]));
|
||||
request1.fee = BigInteger.ONE;
|
||||
request1.feePerKb = BigInteger.ONE;
|
||||
// We get a category 2 using COIN+CENT
|
||||
@ -1730,10 +1734,10 @@ public class WalletTest extends TestWithWallet {
|
||||
wallet.receiveFromBlock(tx4, block, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
|
||||
// Create a transaction who's max size could be up to 1000 (if signatures were maximum size)
|
||||
SendRequest request2 = SendRequest.to(notMyAddr, Utils.COIN.subtract(Utils.CENT.multiply(BigInteger.valueOf(17))));
|
||||
SendRequest request2 = SendRequest.to(notMyAddr, Utils.COIN.subtract(CENT.multiply(BigInteger.valueOf(17))));
|
||||
for (int i = 0; i < 16; i++)
|
||||
request2.tx.addOutput(Utils.CENT, notMyAddr);
|
||||
request2.tx.addOutput(new TransactionOutput(params, request2.tx, Utils.CENT, new byte[16]));
|
||||
request2.tx.addOutput(CENT, notMyAddr);
|
||||
request2.tx.addOutput(new TransactionOutput(params, request2.tx, CENT, new byte[16]));
|
||||
request2.feePerKb = BigInteger.ONE;
|
||||
// The process is the same as above, but now we can complete category 1 with one more input, and pay a fee of 2
|
||||
assertTrue(wallet.completeTx(request2));
|
||||
@ -1755,41 +1759,41 @@ public class WalletTest extends TestWithWallet {
|
||||
wallet.receiveFromBlock(tx1, block, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
Transaction tx2 = createFakeTx(params, Utils.COIN, myAddress); assertTrue(!tx1.getHash().equals(tx2.getHash()));
|
||||
wallet.receiveFromBlock(tx2, block, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
Transaction tx3 = createFakeTx(params, Utils.CENT, myAddress);
|
||||
Transaction tx3 = createFakeTx(params, CENT, myAddress);
|
||||
wallet.receiveFromBlock(tx3, block, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
|
||||
SendRequest request1 = SendRequest.to(notMyAddr, Utils.CENT);
|
||||
SendRequest request1 = SendRequest.to(notMyAddr, CENT);
|
||||
// If we just complete as-is, we will use one of the COIN outputs to get higher priority,
|
||||
// resulting in a change output
|
||||
assertNotNull(wallet.completeTx(request1));
|
||||
assertEquals(1, request1.tx.getInputs().size());
|
||||
assertEquals(2, request1.tx.getOutputs().size());
|
||||
assertEquals(Utils.CENT, request1.tx.getOutput(0).getValue());
|
||||
assertEquals(Utils.COIN.subtract(Utils.CENT), request1.tx.getOutput(1).getValue());
|
||||
assertEquals(CENT, request1.tx.getOutput(0).getValue());
|
||||
assertEquals(Utils.COIN.subtract(CENT), request1.tx.getOutput(1).getValue());
|
||||
|
||||
// Now create an identical request2 and add an unsigned spend of the CENT output
|
||||
SendRequest request2 = SendRequest.to(notMyAddr, Utils.CENT);
|
||||
SendRequest request2 = SendRequest.to(notMyAddr, CENT);
|
||||
request2.tx.addInput(tx3.getOutput(0));
|
||||
// Now completeTx will result in one input, one output
|
||||
assertTrue(wallet.completeTx(request2));
|
||||
assertEquals(1, request2.tx.getInputs().size());
|
||||
assertEquals(1, request2.tx.getOutputs().size());
|
||||
assertEquals(Utils.CENT, request2.tx.getOutput(0).getValue());
|
||||
assertEquals(CENT, request2.tx.getOutput(0).getValue());
|
||||
// Make sure it was properly signed
|
||||
request2.tx.getInput(0).getScriptSig().correctlySpends(request2.tx, 0, tx3.getOutput(0).getScriptPubKey(), true);
|
||||
|
||||
// However, if there is no connected output, we will grab a COIN output anyway and add the CENT to fee
|
||||
SendRequest request3 = SendRequest.to(notMyAddr, Utils.CENT);
|
||||
SendRequest request3 = SendRequest.to(notMyAddr, CENT);
|
||||
request3.tx.addInput(new TransactionInput(params, request3.tx, new byte[]{}, new TransactionOutPoint(params, 0, tx3.getHash())));
|
||||
// Now completeTx will result in two inputs, two outputs and a fee of a CENT
|
||||
// Note that it is simply assumed that the inputs are correctly signed, though in fact the first is not
|
||||
assertTrue(wallet.completeTx(request3));
|
||||
assertEquals(2, request3.tx.getInputs().size());
|
||||
assertEquals(2, request3.tx.getOutputs().size());
|
||||
assertEquals(Utils.CENT, request3.tx.getOutput(0).getValue());
|
||||
assertEquals(Utils.COIN.subtract(Utils.CENT), request3.tx.getOutput(1).getValue());
|
||||
assertEquals(CENT, request3.tx.getOutput(0).getValue());
|
||||
assertEquals(Utils.COIN.subtract(CENT), request3.tx.getOutput(1).getValue());
|
||||
|
||||
SendRequest request4 = SendRequest.to(notMyAddr, Utils.CENT);
|
||||
SendRequest request4 = SendRequest.to(notMyAddr, CENT);
|
||||
request4.tx.addInput(tx3.getOutput(0));
|
||||
// Now if we manually sign it, completeTx will not replace our signature
|
||||
request4.tx.signInputs(SigHash.ALL, wallet);
|
||||
@ -1797,7 +1801,7 @@ public class WalletTest extends TestWithWallet {
|
||||
assertTrue(wallet.completeTx(request4));
|
||||
assertEquals(1, request4.tx.getInputs().size());
|
||||
assertEquals(1, request4.tx.getOutputs().size());
|
||||
assertEquals(Utils.CENT, request4.tx.getOutput(0).getValue());
|
||||
assertEquals(CENT, request4.tx.getOutput(0).getValue());
|
||||
assertArrayEquals(scriptSig, request4.tx.getInput(0).getScriptBytes());
|
||||
}
|
||||
|
||||
@ -1847,23 +1851,23 @@ public class WalletTest extends TestWithWallet {
|
||||
Address outputKey = new ECKey().toAddress(params);
|
||||
// Add exactly 0.01
|
||||
StoredBlock block = new StoredBlock(makeSolvedTestBlock(blockStore, outputKey), BigInteger.ONE, 1);
|
||||
Transaction tx = createFakeTx(params, Utils.CENT, myAddress);
|
||||
Transaction tx = createFakeTx(params, CENT, myAddress);
|
||||
wallet.receiveFromBlock(tx, block, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
SendRequest request = SendRequest.emptyWallet(outputKey);
|
||||
assertTrue(wallet.completeTx(request));
|
||||
wallet.commitTx(request.tx);
|
||||
assertEquals(BigInteger.ZERO, wallet.getBalance());
|
||||
assertEquals(Utils.CENT, request.tx.getOutput(0).getValue());
|
||||
assertEquals(CENT, request.tx.getOutput(0).getValue());
|
||||
|
||||
// Add just under 0.01
|
||||
StoredBlock block2 = new StoredBlock(block.getHeader().createNextBlock(outputKey), BigInteger.ONE, 2);
|
||||
tx = createFakeTx(params, Utils.CENT.subtract(BigInteger.ONE), myAddress);
|
||||
tx = createFakeTx(params, CENT.subtract(BigInteger.ONE), myAddress);
|
||||
wallet.receiveFromBlock(tx, block2, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
request = SendRequest.emptyWallet(outputKey);
|
||||
assertTrue(wallet.completeTx(request));
|
||||
wallet.commitTx(request.tx);
|
||||
assertEquals(BigInteger.ZERO, wallet.getBalance());
|
||||
assertEquals(Utils.CENT.subtract(BigInteger.ONE).subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), request.tx.getOutput(0).getValue());
|
||||
assertEquals(CENT.subtract(BigInteger.ONE).subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), request.tx.getOutput(0).getValue());
|
||||
|
||||
// Add an unsendable value
|
||||
StoredBlock block3 = new StoredBlock(block2.getHeader().createNextBlock(outputKey), BigInteger.ONE, 3);
|
||||
@ -1878,4 +1882,107 @@ public class WalletTest extends TestWithWallet {
|
||||
assertEquals(BigInteger.ZERO, wallet.getBalance());
|
||||
assertEquals(outputValue, request.tx.getOutput(0).getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void keyRotation() throws Exception {
|
||||
// Watch out for wallet-initiated broadcasts.
|
||||
MockTransactionBroadcaster broadcaster = new MockTransactionBroadcaster(wallet);
|
||||
wallet.setTransactionBroadcaster(broadcaster);
|
||||
wallet.setKeyRotationEnabled(true);
|
||||
// Send three cents to two different keys, then add a key and mark the initial keys as compromised.
|
||||
ECKey key1 = new ECKey();
|
||||
ECKey key2 = new ECKey();
|
||||
wallet.addKey(key1);
|
||||
wallet.addKey(key2);
|
||||
sendMoneyToWallet(wallet, CENT, key1.toAddress(params), AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
sendMoneyToWallet(wallet, CENT, key2.toAddress(params), AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
sendMoneyToWallet(wallet, CENT, key2.toAddress(params), AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
Utils.rollMockClock(86400);
|
||||
Date compromiseTime = Utils.now();
|
||||
assertEquals(0, broadcaster.broadcasts.size());
|
||||
assertFalse(wallet.isKeyRotating(key1));
|
||||
|
||||
// Rotate the wallet.
|
||||
ECKey key3 = new ECKey();
|
||||
wallet.addKey(key3);
|
||||
// We see a broadcast triggered by setting the rotation time.
|
||||
wallet.setKeyRotationTime(compromiseTime);
|
||||
assertTrue(wallet.isKeyRotating(key1));
|
||||
Transaction tx = broadcaster.broadcasts.take();
|
||||
final BigInteger THREE_CENTS = CENT.add(CENT).add(CENT);
|
||||
assertEquals(THREE_CENTS, tx.getValueSentFromMe(wallet));
|
||||
assertEquals(THREE_CENTS.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), tx.getValueSentToMe(wallet));
|
||||
// TX is a raw pay to pubkey.
|
||||
assertArrayEquals(key3.getPubKey(), tx.getOutput(0).getScriptPubKey().getPubKey());
|
||||
assertEquals(3, tx.getInputs().size());
|
||||
// It confirms.
|
||||
sendMoneyToWallet(tx, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
|
||||
// Now receive some more money to key3 (secure) via a new block and check that nothing happens.
|
||||
sendMoneyToWallet(wallet, CENT, key3.toAddress(params), AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertTrue(broadcaster.broadcasts.isEmpty());
|
||||
|
||||
// Receive money via a new block on key1 and ensure it's immediately moved.
|
||||
sendMoneyToWallet(wallet, CENT, key1.toAddress(params), AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
tx = broadcaster.broadcasts.take();
|
||||
assertArrayEquals(key3.getPubKey(), tx.getOutput(0).getScriptPubKey().getPubKey());
|
||||
assertEquals(1, tx.getInputs().size());
|
||||
assertEquals(1, tx.getOutputs().size());
|
||||
assertEquals(CENT.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), tx.getOutput(0).getValue());
|
||||
|
||||
assertEquals(Transaction.Purpose.KEY_ROTATION, tx.getPurpose());
|
||||
|
||||
// We don't attempt to race an attacker against unconfirmed transactions.
|
||||
|
||||
// Now round-trip the wallet and check the protobufs are storing the data correctly.
|
||||
Protos.Wallet protos = new WalletProtobufSerializer().walletToProto(wallet);
|
||||
wallet = new Wallet(params);
|
||||
new WalletProtobufSerializer().readWallet(protos, wallet);
|
||||
|
||||
tx = wallet.getTransaction(tx.getHash());
|
||||
assertEquals(Transaction.Purpose.KEY_ROTATION, tx.getPurpose());
|
||||
// Have to divide here to avoid mismatch due to second-level precision in serialisation.
|
||||
assertEquals(compromiseTime.getTime() / 1000, wallet.getKeyRotationTime().getTime() / 1000);
|
||||
|
||||
// Make a normal spend and check it's all ok.
|
||||
final Address address = new ECKey().toAddress(params);
|
||||
wallet.sendCoins(broadcaster, address, wallet.getBalance());
|
||||
tx = broadcaster.broadcasts.take();
|
||||
assertArrayEquals(address.getHash160(), tx.getOutput(0).getScriptPubKey().getPubKeyHash());
|
||||
// We have to race here because we're checking for the ABSENCE of a broadcast, and if there were to be one,
|
||||
// it'd be happening in parallel.
|
||||
assertEquals(null, broadcaster.broadcasts.poll(1, TimeUnit.SECONDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fragmentedReKeying() throws Exception {
|
||||
// Send lots of small coins and check the fee is correct.
|
||||
ECKey key = new ECKey();
|
||||
wallet.addKey(key);
|
||||
Address address = key.toAddress(params);
|
||||
Utils.rollMockClock(86400);
|
||||
for (int i = 0; i < 800; i++) {
|
||||
sendMoneyToWallet(wallet, Utils.CENT, address, AbstractBlockChain.NewBlockType.BEST_CHAIN);
|
||||
}
|
||||
|
||||
MockTransactionBroadcaster broadcaster = new MockTransactionBroadcaster(wallet);
|
||||
wallet.setTransactionBroadcaster(broadcaster);
|
||||
wallet.setKeyRotationEnabled(true);
|
||||
|
||||
Date compromise = Utils.now();
|
||||
Utils.rollMockClock(86400);
|
||||
wallet.addKey(new ECKey());
|
||||
wallet.setKeyRotationTime(compromise);
|
||||
|
||||
Transaction tx = broadcaster.broadcasts.take();
|
||||
final BigInteger valueSentToMe = tx.getValueSentToMe(wallet);
|
||||
BigInteger fee = tx.getValueSentFromMe(wallet).subtract(valueSentToMe);
|
||||
assertEquals(BigInteger.valueOf(900000), fee);
|
||||
assertEquals(KeyTimeCoinSelector.MAX_SIMULTANEOUS_INPUTS, tx.getInputs().size());
|
||||
assertEquals(BigInteger.valueOf(599100000), valueSentToMe);
|
||||
|
||||
tx = broadcaster.broadcasts.take();
|
||||
assertNotNull(tx);
|
||||
assertEquals(200, tx.getInputs().size());
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user