3
0
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:
Mike Hearn 2013-08-08 14:40:52 +02:00
parent 3ca2cd0345
commit 6dd907614c
9 changed files with 835 additions and 116 deletions

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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.
@ -241,6 +246,16 @@ public class WalletProtobufSerializer {
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))

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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();
}
}
}

View File

@ -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());
}
}