/*
* Copyright 2011 Google Inc.
* Copyright 2014 Andreas Schildbach
*
* 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 org.bitcoinj.wallet;
import org.bitcoinj.core.listeners.TransactionConfidenceEventListener;
import org.bitcoinj.core.AbstractBlockChain;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Block;
import org.bitcoinj.core.BlockChain;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.PeerAddress;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.StoredBlock;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.Utils;
import org.bitcoinj.core.VerificationException;
import org.bitcoinj.core.TransactionConfidence.ConfidenceType;
import org.bitcoinj.crypto.*;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.signers.StatelessTransactionSigner;
import org.bitcoinj.signers.TransactionSigner;
import org.bitcoinj.store.BlockStoreException;
import org.bitcoinj.store.MemoryBlockStore;
import org.bitcoinj.testing.*;
import org.bitcoinj.utils.ExchangeRate;
import org.bitcoinj.utils.Fiat;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.Wallet.BalanceType;
import org.bitcoinj.wallet.WalletTransaction.Pool;
import org.bitcoinj.wallet.listeners.KeyChainEventListener;
import org.bitcoinj.wallet.listeners.WalletChangeEventListener;
import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener;
import org.bitcoinj.wallet.listeners.WalletCoinsSentEventListener;
import org.easymock.EasyMock;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.ByteString;
import org.bitcoinj.wallet.Protos.Wallet.EncryptionType;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import java.io.File;
import java.math.BigInteger;
import java.net.InetAddress;
import java.security.SecureRandom;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.bitcoinj.core.Coin.*;
import static org.bitcoinj.core.Utils.HEX;
import static org.bitcoinj.testing.FakeTxBuilder.*;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.replay;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.junit.Assert.*;
public class WalletTest extends TestWithWallet {
private static final Logger log = LoggerFactory.getLogger(WalletTest.class);
private static final CharSequence PASSWORD1 = "my helicopter contains eels";
private static final CharSequence WRONG_PASSWORD = "nothing noone nobody nowhere";
private final Address OTHER_ADDRESS = new ECKey().toAddress(PARAMS);
@Before
@Override
public void setUp() throws Exception {
super.setUp();
}
@After
@Override
public void tearDown() throws Exception {
super.tearDown();
}
private void createMarriedWallet(int threshold, int numKeys) throws BlockStoreException {
createMarriedWallet(threshold, numKeys, true);
}
private void createMarriedWallet(int threshold, int numKeys, boolean addSigners) throws BlockStoreException {
wallet = new Wallet(PARAMS);
blockStore = new MemoryBlockStore(PARAMS);
chain = new BlockChain(PARAMS, wallet, blockStore);
List<DeterministicKey> followingKeys = Lists.newArrayList();
for (int i = 0; i < numKeys - 1; i++) {
final DeterministicKeyChain keyChain = new DeterministicKeyChain(new SecureRandom());
DeterministicKey partnerKey = DeterministicKey.deserializeB58(null, keyChain.getWatchingKey().serializePubB58(PARAMS), PARAMS);
followingKeys.add(partnerKey);
if (addSigners && i < threshold - 1)
wallet.addTransactionSigner(new KeyChainTransactionSigner(keyChain));
}
MarriedKeyChain chain = MarriedKeyChain.builder()
.random(new SecureRandom())
.followingKeys(followingKeys)
.threshold(threshold).build();
wallet.addAndActivateHDChain(chain);
}
@Test
public void getSeedAsWords1() {
// Can't verify much here as the wallet is random each time. We could fix the RNG for the unit tests and solve.
assertEquals(12, wallet.getKeyChainSeed().getMnemonicCode().size());
}
@Test
public void checkSeed() throws MnemonicException {
wallet.getKeyChainSeed().check();
}
@Test
public void basicSpending() throws Exception {
basicSpendingCommon(wallet, myAddress, OTHER_ADDRESS, null);
}
@Test
public void basicSpendingToP2SH() throws Exception {
Address destination = new Address(PARAMS, PARAMS.getP2SHHeader(), HEX.decode("4a22c3c4cbb31e4d03b15550636762bda0baf85a"));
basicSpendingCommon(wallet, myAddress, destination, null);
}
@Test
public void basicSpendingWithEncryptedWallet() throws Exception {
Wallet encryptedWallet = new Wallet(PARAMS);
encryptedWallet.encrypt(PASSWORD1);
Address myEncryptedAddress = encryptedWallet.freshReceiveKey().toAddress(PARAMS);
basicSpendingCommon(encryptedWallet, myEncryptedAddress, OTHER_ADDRESS, encryptedWallet);
}
@Test
public void basicSpendingFromP2SH() throws Exception {
createMarriedWallet(2, 2);
myAddress = wallet.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
basicSpendingCommon(wallet, myAddress, OTHER_ADDRESS, null);
createMarriedWallet(2, 3);
myAddress = wallet.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
basicSpendingCommon(wallet, myAddress, OTHER_ADDRESS, null);
createMarriedWallet(3, 3);
myAddress = wallet.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
basicSpendingCommon(wallet, myAddress, OTHER_ADDRESS, null);
}
@Test (expected = IllegalArgumentException.class)
public void thresholdShouldNotExceedNumberOfKeys() throws Exception {
createMarriedWallet(3, 2);
}
@Test
public void spendingWithIncompatibleSigners() throws Exception {
wallet.addTransactionSigner(new NopTransactionSigner(true));
basicSpendingCommon(wallet, myAddress, OTHER_ADDRESS, null);
}
static class TestRiskAnalysis implements RiskAnalysis {
private final boolean risky;
public TestRiskAnalysis(boolean risky) {
this.risky = risky;
}
@Override
public Result analyze() {
return risky ? Result.NON_FINAL : Result.OK;
}
public static class Analyzer implements RiskAnalysis.Analyzer {
private final Transaction riskyTx;
Analyzer(Transaction riskyTx) {
this.riskyTx = riskyTx;
}
@Override
public RiskAnalysis create(Wallet wallet, Transaction tx, List<Transaction> dependencies) {
return new TestRiskAnalysis(tx == riskyTx);
}
}
}
static class TestCoinSelector extends DefaultCoinSelector {
@Override
protected boolean shouldSelect(Transaction tx) {
return true;
}
}
private Transaction cleanupCommon(Address destination) throws Exception {
receiveATransaction(wallet, myAddress);
Coin v2 = valueOf(0, 50);
SendRequest req = SendRequest.to(destination, v2);
wallet.completeTx(req);
Transaction t2 = req.tx;
// Broadcast the transaction and commit.
broadcastAndCommit(wallet, t2);
// At this point we have one pending and one spent
Coin v1 = valueOf(0, 10);
Transaction t = sendMoneyToWallet(null, v1, myAddress);
Threading.waitForUserCode();
sendMoneyToWallet(null, t);
assertEquals("Wrong number of PENDING", 2, wallet.getPoolSize(Pool.PENDING));
assertEquals("Wrong number of UNSPENT", 0, wallet.getPoolSize(Pool.UNSPENT));
assertEquals("Wrong number of ALL", 3, wallet.getTransactions(true).size());
assertEquals(valueOf(0, 60), wallet.getBalance(Wallet.BalanceType.ESTIMATED));
// Now we have another incoming pending
return t;
}
@Test
public void cleanup() throws Exception {
Transaction t = cleanupCommon(OTHER_ADDRESS);
// Consider the new pending as risky and remove it from the wallet
wallet.setRiskAnalyzer(new TestRiskAnalysis.Analyzer(t));
wallet.cleanup();
assertTrue(wallet.isConsistent());
assertEquals("Wrong number of PENDING", 1, wallet.getPoolSize(WalletTransaction.Pool.PENDING));
assertEquals("Wrong number of UNSPENT", 0, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals("Wrong number of ALL", 2, wallet.getTransactions(true).size());
assertEquals(valueOf(0, 50), wallet.getBalance(Wallet.BalanceType.ESTIMATED));
}
@Test
public void cleanupFailsDueToSpend() throws Exception {
Transaction t = cleanupCommon(OTHER_ADDRESS);
// Now we have another incoming pending. Spend everything.
Coin v3 = valueOf(0, 60);
SendRequest req = SendRequest.to(OTHER_ADDRESS, v3);
// Force selection of the incoming coin so that we can spend it
req.coinSelector = new TestCoinSelector();
wallet.completeTx(req);
wallet.commitTx(req.tx);
assertEquals("Wrong number of PENDING", 3, wallet.getPoolSize(WalletTransaction.Pool.PENDING));
assertEquals("Wrong number of UNSPENT", 0, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals("Wrong number of ALL", 4, wallet.getTransactions(true).size());
// Consider the new pending as risky and try to remove it from the wallet
wallet.setRiskAnalyzer(new TestRiskAnalysis.Analyzer(t));
wallet.cleanup();
assertTrue(wallet.isConsistent());
// The removal should have failed
assertEquals("Wrong number of PENDING", 3, wallet.getPoolSize(WalletTransaction.Pool.PENDING));
assertEquals("Wrong number of UNSPENT", 0, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals("Wrong number of ALL", 4, wallet.getTransactions(true).size());
assertEquals(ZERO, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
}
private void basicSpendingCommon(Wallet wallet, Address toAddress, Address destination, Wallet encryptedWallet) throws Exception {
// We'll set up a wallet that receives a coin, then sends a coin of lesser value and keeps the change. We
// will attach a small fee. Because the Bitcoin protocol makes it difficult to determine the fee of an
// arbitrary transaction in isolation, we'll check that the fee was set by examining the size of the change.
// Receive some money as a pending transaction.
receiveATransaction(wallet, toAddress);
// Try to send too much and fail.
Coin vHuge = valueOf(10, 0);
SendRequest req = SendRequest.to(destination, vHuge);
try {
wallet.completeTx(req);
fail();
} catch (InsufficientMoneyException e) {
assertEquals(valueOf(9, 0), e.missing);
}
// Prepare to send.
Coin v2 = valueOf(0, 50);
req = SendRequest.to(destination, v2);
if (encryptedWallet != null) {
KeyCrypter keyCrypter = encryptedWallet.getKeyCrypter();
KeyParameter aesKey = keyCrypter.deriveKey(PASSWORD1);
KeyParameter wrongAesKey = keyCrypter.deriveKey(WRONG_PASSWORD);
// Try to create a send with a fee but no password (this should fail).
try {
wallet.completeTx(req);
fail();
} catch (ECKey.MissingPrivateKeyException kce) {
}
assertEquals("Wrong number of UNSPENT", 1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals("Wrong number of ALL", 1, wallet.getTransactions(true).size());
// Try to create a send with a fee but the wrong password (this should fail).
req = SendRequest.to(destination, v2);
req.aesKey = wrongAesKey;
try {
wallet.completeTx(req);
fail("No exception was thrown trying to sign an encrypted key with the wrong password supplied.");
} catch (KeyCrypterException kce) {
assertEquals("Could not decrypt bytes", kce.getMessage());
}
assertEquals("Wrong number of UNSPENT", 1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals("Wrong number of ALL", 1, wallet.getTransactions(true).size());
// Create a send with a fee with the correct password (this should succeed).
req = SendRequest.to(destination, v2);
req.aesKey = aesKey;
}
// Complete the transaction successfully.
req.shuffleOutputs = false;
wallet.completeTx(req);
Transaction t2 = req.tx;
assertEquals("Wrong number of UNSPENT", 1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals("Wrong number of ALL", 1, wallet.getTransactions(true).size());
assertEquals(TransactionConfidence.Source.SELF, t2.getConfidence().getSource());
assertEquals(Transaction.Purpose.USER_PAYMENT, t2.getPurpose());
// Do some basic sanity checks.
basicSanityChecks(wallet, t2, destination);
// Broadcast the transaction and commit.
List<TransactionOutput> unspents1 = wallet.getUnspents();
assertEquals(1, unspents1.size());
broadcastAndCommit(wallet, t2);
List<TransactionOutput> unspents2 = wallet.getUnspents();
assertNotEquals(unspents1, unspents2.size());
// Now check that we can spend the unconfirmed change, with a new change address of our own selection.
// (req.aesKey is null for unencrypted / the correct aesKey for encrypted.)
wallet = spendUnconfirmedChange(wallet, t2, req.aesKey);
assertNotEquals(unspents2, wallet.getUnspents());
}
private void receiveATransaction(Wallet wallet, Address toAddress) throws Exception {
receiveATransactionAmount(wallet, toAddress, COIN);
}
private void receiveATransactionAmount(Wallet wallet, Address toAddress, Coin amount) {
final ListenableFuture<Coin> availFuture = wallet.getBalanceFuture(amount, Wallet.BalanceType.AVAILABLE);
final ListenableFuture<Coin> estimatedFuture = wallet.getBalanceFuture(amount, Wallet.BalanceType.ESTIMATED);
assertFalse(availFuture.isDone());
assertFalse(estimatedFuture.isDone());
// Send some pending coins to the wallet.
Transaction t1 = sendMoneyToWallet(wallet, null, amount, toAddress);
Threading.waitForUserCode();
final ListenableFuture<TransactionConfidence> depthFuture = t1.getConfidence().getDepthFuture(1);
assertFalse(depthFuture.isDone());
assertEquals(ZERO, wallet.getBalance());
assertEquals(amount, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
assertFalse(availFuture.isDone());
// Our estimated balance has reached the requested level.
assertTrue(estimatedFuture.isDone());
assertEquals(1, wallet.getPoolSize(Pool.PENDING));
assertEquals(0, wallet.getPoolSize(Pool.UNSPENT));
// Confirm the coins.
sendMoneyToWallet(wallet, AbstractBlockChain.NewBlockType.BEST_CHAIN, t1);
assertEquals("Incorrect confirmed tx balance", amount, wallet.getBalance());
assertEquals("Incorrect confirmed tx PENDING pool size", 0, wallet.getPoolSize(Pool.PENDING));
assertEquals("Incorrect confirmed tx UNSPENT pool size", 1, wallet.getPoolSize(Pool.UNSPENT));
assertEquals("Incorrect confirmed tx ALL pool size", 1, wallet.getTransactions(true).size());
Threading.waitForUserCode();
assertTrue(availFuture.isDone());
assertTrue(estimatedFuture.isDone());
assertTrue(depthFuture.isDone());
}
private void basicSanityChecks(Wallet wallet, Transaction t, Address destination) throws VerificationException {
assertEquals("Wrong number of tx inputs", 1, t.getInputs().size());
assertEquals("Wrong number of tx outputs",2, t.getOutputs().size());
assertEquals(destination, t.getOutput(0).getScriptPubKey().getToAddress(PARAMS));
assertEquals(wallet.currentChangeAddress(), t.getOutputs().get(1).getScriptPubKey().getToAddress(PARAMS));
assertEquals(valueOf(0, 50), t.getOutputs().get(1).getValue());
// Check the script runs and signatures verify.
t.getInputs().get(0).verify();
}
private static void broadcastAndCommit(Wallet wallet, Transaction t) throws Exception {
final LinkedList<Transaction> txns = Lists.newLinkedList();
wallet.addCoinsSentEventListener(new WalletCoinsSentEventListener() {
@Override
public void onCoinsSent(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
txns.add(tx);
}
});
t.getConfidence().markBroadcastBy(new PeerAddress(PARAMS, InetAddress.getByAddress(new byte[]{1,2,3,4})));
t.getConfidence().markBroadcastBy(new PeerAddress(PARAMS, InetAddress.getByAddress(new byte[]{10,2,3,4})));
wallet.commitTx(t);
Threading.waitForUserCode();
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.PENDING));
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.SPENT));
assertEquals(2, wallet.getTransactions(true).size());
assertEquals(t, txns.getFirst());
assertEquals(1, txns.size());
}
private Wallet spendUnconfirmedChange(Wallet wallet, Transaction t2, KeyParameter aesKey) throws Exception {
if (wallet.getTransactionSigners().size() == 1) // don't bother reconfiguring the p2sh wallet
wallet = roundTrip(wallet);
Coin v3 = valueOf(0, 50);
assertEquals(v3, wallet.getBalance());
SendRequest req = SendRequest.to(OTHER_ADDRESS, valueOf(0, 48));
req.aesKey = aesKey;
req.shuffleOutputs = false;
wallet.completeTx(req);
Transaction t3 = req.tx;
assertNotEquals(t2.getOutput(1).getScriptPubKey().getToAddress(PARAMS),
t3.getOutput(1).getScriptPubKey().getToAddress(PARAMS));
assertNotNull(t3);
wallet.commitTx(t3);
assertTrue(wallet.isConsistent());
// t2 and t3 gets confirmed in the same block.
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, t2, t3);
assertTrue(wallet.isConsistent());
return wallet;
}
@Test
@SuppressWarnings("deprecation")
// Having a test for deprecated method getFromAddress() is no evil so we suppress the warning here.
public void customTransactionSpending() throws Exception {
// We'll set up a wallet that receives a coin, then sends a coin of lesser value and keeps the change.
Coin v1 = valueOf(3, 0);
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, v1);
assertEquals(v1, wallet.getBalance());
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals(1, wallet.getTransactions(true).size());
Coin v2 = valueOf(0, 50);
Coin v3 = valueOf(0, 75);
Coin v4 = valueOf(1, 25);
Transaction t2 = new Transaction(PARAMS);
t2.addOutput(v2, OTHER_ADDRESS);
t2.addOutput(v3, OTHER_ADDRESS);
t2.addOutput(v4, OTHER_ADDRESS);
SendRequest req = SendRequest.forTx(t2);
wallet.completeTx(req);
// Do some basic sanity checks.
assertEquals(1, t2.getInputs().size());
assertEquals(myAddress, t2.getInput(0).getScriptSig().getFromAddress(PARAMS));
assertEquals(TransactionConfidence.ConfidenceType.UNKNOWN, t2.getConfidence().getConfidenceType());
// We have NOT proven that the signature is correct!
wallet.commitTx(t2);
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.PENDING));
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.SPENT));
assertEquals(2, wallet.getTransactions(true).size());
}
@Test
public void sideChain() throws Exception {
// The wallet receives a coin on the main chain, then on a side chain. Balance is equal to both added together
// as we assume the side chain tx is pending and will be included shortly.
Coin v1 = COIN;
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, v1);
assertEquals(v1, wallet.getBalance());
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals(1, wallet.getTransactions(true).size());
Coin v2 = valueOf(0, 50);
sendMoneyToWallet(AbstractBlockChain.NewBlockType.SIDE_CHAIN, v2);
assertEquals(2, wallet.getTransactions(true).size());
assertEquals(v1, wallet.getBalance());
assertEquals(v1.add(v2), wallet.getBalance(Wallet.BalanceType.ESTIMATED));
}
@Test
public void balance() throws Exception {
// Receive 5 coins then half a coin.
Coin v1 = valueOf(5, 0);
Coin v2 = valueOf(0, 50);
Coin expected = valueOf(5, 50);
assertEquals(0, wallet.getTransactions(true).size());
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, v1);
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, v2);
assertEquals(2, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals(expected, wallet.getBalance());
// Now spend one coin.
Coin v3 = COIN;
Transaction spend = wallet.createSend(OTHER_ADDRESS, v3);
wallet.commitTx(spend);
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.PENDING));
// Available and estimated balances should not be the same. We don't check the exact available balance here
// because it depends on the coin selection algorithm.
assertEquals(valueOf(4, 50), wallet.getBalance(Wallet.BalanceType.ESTIMATED));
assertFalse(wallet.getBalance(Wallet.BalanceType.AVAILABLE).equals(
wallet.getBalance(Wallet.BalanceType.ESTIMATED)));
// Now confirm the transaction by including it into a block.
sendMoneyToWallet(BlockChain.NewBlockType.BEST_CHAIN, spend);
// Change is confirmed. We started with 5.50 so we should have 4.50 left.
Coin v4 = valueOf(4, 50);
assertEquals(v4, wallet.getBalance(Wallet.BalanceType.AVAILABLE));
}
@Test
public void balanceWithIdenticalOutputs() {
assertEquals(Coin.ZERO, wallet.getBalance(BalanceType.ESTIMATED));
Transaction tx = new Transaction(PARAMS);
tx.addOutput(Coin.COIN, myAddress);
tx.addOutput(Coin.COIN, myAddress); // identical to the above
wallet.addWalletTransaction(new WalletTransaction(Pool.UNSPENT, tx));
assertEquals(Coin.COIN.plus(Coin.COIN), wallet.getBalance(BalanceType.ESTIMATED));
}
// Intuitively you'd expect to be able to create a transaction with identical inputs and outputs and get an
// identical result to Bitcoin Core. However the signatures are not deterministic - signing the same data
// with the same key twice gives two different outputs. So we cannot prove bit-for-bit compatibility in this test
// suite.
@Test
public void blockChainCatchup() throws Exception {
// Test that we correctly process transactions arriving from the chain, with callbacks for inbound and outbound.
final Coin[] bigints = new Coin[4];
final Transaction[] txn = new Transaction[2];
final LinkedList<Transaction> confTxns = new LinkedList<Transaction>();
wallet.addCoinsReceivedEventListener(new WalletCoinsReceivedEventListener() {
@Override
public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
bigints[0] = prevBalance;
bigints[1] = newBalance;
txn[0] = tx;
}
});
wallet.addCoinsSentEventListener(new WalletCoinsSentEventListener() {
@Override
public void onCoinsSent(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
bigints[2] = prevBalance;
bigints[3] = newBalance;
txn[1] = tx;
}
});
wallet.addTransactionConfidenceEventListener(new TransactionConfidenceEventListener() {
@Override
public void onTransactionConfidenceChanged(Wallet wallet, Transaction tx) {
confTxns.add(tx);
}
});
// Receive some money.
Coin oneCoin = COIN;
Transaction tx1 = sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, oneCoin);
Threading.waitForUserCode();
assertEquals(null, txn[1]); // onCoinsSent not called.
assertEquals(tx1, confTxns.getFirst()); // onTransactionConfidenceChanged called
assertEquals(txn[0].getHash(), tx1.getHash());
assertEquals(ZERO, bigints[0]);
assertEquals(oneCoin, bigints[1]);
assertEquals(TransactionConfidence.ConfidenceType.BUILDING, tx1.getConfidence().getConfidenceType());
assertEquals(1, tx1.getConfidence().getAppearedAtChainHeight());
// Send 0.10 to somebody else.
Transaction send1 = wallet.createSend(OTHER_ADDRESS, valueOf(0, 10));
// Pretend it makes it into the block chain, our wallet state is cleared but we still have the keys, and we
// want to get back to our previous state. We can do this by just not confirming the transaction as
// createSend is stateless.
txn[0] = txn[1] = null;
confTxns.clear();
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, send1);
Threading.waitForUserCode();
assertEquals(Coin.valueOf(0, 90), wallet.getBalance());
assertEquals(null, txn[0]);
assertEquals(2, confTxns.size());
assertEquals(txn[1].getHash(), send1.getHash());
assertEquals(Coin.COIN, bigints[2]);
assertEquals(Coin.valueOf(0, 90), bigints[3]);
// And we do it again after the catchup.
Transaction send2 = wallet.createSend(OTHER_ADDRESS, valueOf(0, 10));
// What we'd really like to do is prove Bitcoin Core would accept it .... no such luck unfortunately.
wallet.commitTx(send2);
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, send2);
assertEquals(Coin.valueOf(0, 80), wallet.getBalance());
Threading.waitForUserCode();
FakeTxBuilder.BlockPair b4 = createFakeBlock(blockStore, Block.BLOCK_HEIGHT_GENESIS);
confTxns.clear();
wallet.notifyNewBestBlock(b4.storedBlock);
Threading.waitForUserCode();
assertEquals(3, confTxns.size());
}
@Test
public void balances() throws Exception {
Coin nanos = COIN;
Transaction tx1 = sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, nanos);
assertEquals(nanos, tx1.getValueSentToMe(wallet));
assertTrue(tx1.getWalletOutputs(wallet).size() >= 1);
// Send 0.10 to somebody else.
Transaction send1 = wallet.createSend(OTHER_ADDRESS, valueOf(0, 10));
// Reserialize.
Transaction send2 = PARAMS.getDefaultSerializer().makeTransaction(send1.bitcoinSerialize());
assertEquals(nanos, send2.getValueSentFromMe(wallet));
assertEquals(ZERO.subtract(valueOf(0, 10)), send2.getValue(wallet));
}
@Test
public void isConsistent_duplicates() throws Exception {
// This test ensures that isConsistent catches duplicate transactions, eg, because we submitted the same block
// twice (this is not allowed).
Transaction tx = createFakeTx(PARAMS, COIN, myAddress);
TransactionOutput output = new TransactionOutput(PARAMS, tx, valueOf(0, 5), OTHER_ADDRESS);
tx.addOutput(output);
wallet.receiveFromBlock(tx, null, BlockChain.NewBlockType.BEST_CHAIN, 0);
assertTrue(wallet.isConsistent());
Transaction txClone = PARAMS.getDefaultSerializer().makeTransaction(tx.bitcoinSerialize());
try {
wallet.receiveFromBlock(txClone, null, BlockChain.NewBlockType.BEST_CHAIN, 0);
fail("Illegal argument not thrown when it should have been.");
} catch (IllegalStateException ex) {
// expected
}
}
@Test
public void isConsistent_pools() throws Exception {
// This test ensures that isConsistent catches transactions that are in incompatible pools.
Transaction tx = createFakeTx(PARAMS, COIN, myAddress);
TransactionOutput output = new TransactionOutput(PARAMS, tx, valueOf(0, 5), OTHER_ADDRESS);
tx.addOutput(output);
wallet.receiveFromBlock(tx, null, BlockChain.NewBlockType.BEST_CHAIN, 0);
assertTrue(wallet.isConsistent());
wallet.addWalletTransaction(new WalletTransaction(Pool.PENDING, tx));
assertFalse(wallet.isConsistent());
}
@Test
public void isConsistent_spent() throws Exception {
// This test ensures that isConsistent catches transactions that are marked spent when
// they aren't.
Transaction tx = createFakeTx(PARAMS, COIN, myAddress);
TransactionOutput output = new TransactionOutput(PARAMS, tx, valueOf(0, 5), OTHER_ADDRESS);
tx.addOutput(output);
assertTrue(wallet.isConsistent());
wallet.addWalletTransaction(new WalletTransaction(Pool.SPENT, tx));
assertFalse(wallet.isConsistent());
}
@Test
public void isTxConsistentReturnsFalseAsExpected() {
Wallet wallet = new Wallet(PARAMS);
TransactionOutput to = createMock(TransactionOutput.class);
EasyMock.expect(to.isAvailableForSpending()).andReturn(true);
EasyMock.expect(to.isMineOrWatched(wallet)).andReturn(true);
EasyMock.expect(to.getSpentBy()).andReturn(new TransactionInput(PARAMS, null, new byte[0]));
Transaction tx = FakeTxBuilder.createFakeTxWithoutChange(PARAMS, to);
replay(to);
boolean isConsistent = wallet.isTxConsistent(tx, false);
assertFalse(isConsistent);
}
@Test
public void isTxConsistentReturnsFalseAsExpected_WhenAvailableForSpendingEqualsFalse() {
Wallet wallet = new Wallet(PARAMS);
TransactionOutput to = createMock(TransactionOutput.class);
EasyMock.expect(to.isAvailableForSpending()).andReturn(false);
EasyMock.expect(to.getSpentBy()).andReturn(null);
Transaction tx = FakeTxBuilder.createFakeTxWithoutChange(PARAMS, to);
replay(to);
boolean isConsistent = wallet.isTxConsistent(tx, false);
assertFalse(isConsistent);
}
@Test
public void transactions() throws Exception {
// This test covers a bug in which Transaction.getValueSentFromMe was calculating incorrectly.
Transaction tx = createFakeTx(PARAMS, COIN, myAddress);
// Now add another output (ie, change) that goes to some other address.
TransactionOutput output = new TransactionOutput(PARAMS, tx, valueOf(0, 5), OTHER_ADDRESS);
tx.addOutput(output);
// Note that tx is no longer valid: it spends more than it imports. However checking transactions balance
// correctly isn't possible in SPV mode because value is a property of outputs not inputs. Without all
// transactions you can't check they add up.
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, tx);
// Now the other guy creates a transaction which spends that change.
Transaction tx2 = new Transaction(PARAMS);
tx2.addInput(output);
tx2.addOutput(new TransactionOutput(PARAMS, tx2, valueOf(0, 5), myAddress));
// tx2 doesn't send any coins from us, even though the output is in the wallet.
assertEquals(ZERO, tx2.getValueSentFromMe(wallet));
}
@Test
public void bounce() throws Exception {
// This test covers bug 64 (False double spends). Check that if we create a spend and it's immediately sent
// back to us, this isn't considered as a double spend.
Coin coin1 = COIN;
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, coin1);
// Send half to some other guy. Sending only half then waiting for a confirm is important to ensure the tx is
// in the unspent pool, not pending or spent.
Coin coinHalf = valueOf(0, 50);
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals(1, wallet.getTransactions(true).size());
Transaction outbound1 = wallet.createSend(OTHER_ADDRESS, coinHalf);
wallet.commitTx(outbound1);
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, outbound1);
assertTrue(outbound1.getWalletOutputs(wallet).size() <= 1); //the change address at most
// That other guy gives us the coins right back.
Transaction inbound2 = new Transaction(PARAMS);
inbound2.addOutput(new TransactionOutput(PARAMS, inbound2, coinHalf, myAddress));
assertTrue(outbound1.getWalletOutputs(wallet).size() >= 1);
inbound2.addInput(outbound1.getOutputs().get(0));
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, inbound2);
assertEquals(coin1, wallet.getBalance());
}
@Test
public void doubleSpendUnspendsOtherInputs() throws Exception {
// Test another Finney attack, but this time the killed transaction was also spending some other outputs in
// our wallet which were not themselves double spent. This test ensures the death of the pending transaction
// frees up the other outputs and makes them spendable again.
// Receive 1 coin and then 2 coins in separate transactions.
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, COIN);
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, valueOf(2, 0));
// Create a send to a merchant of all our coins.
Transaction send1 = wallet.createSend(OTHER_ADDRESS, valueOf(2, 90));
// Create a double spend of just the first one.
Address BAD_GUY = new ECKey().toAddress(PARAMS);
Transaction send2 = wallet.createSend(BAD_GUY, COIN);
send2 = PARAMS.getDefaultSerializer().makeTransaction(send2.bitcoinSerialize());
// Broadcast send1, it's now pending.
wallet.commitTx(send1);
assertEquals(ZERO, wallet.getBalance()); // change of 10 cents is not yet mined so not included in the balance.
// Receive a block that overrides the send1 using send2.
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, send2);
// send1 got rolled back and replaced with a smaller send that only used one of our received coins, thus ...
assertEquals(valueOf(2, 0), wallet.getBalance());
assertTrue(wallet.isConsistent());
}
@Test
public void doubleSpends() throws Exception {
// Test the case where two semantically identical but bitwise different transactions double spend each other.
// We call the second transaction a "mutant" of the first.
//
// This can (and has!) happened when a wallet is cloned between devices, and both devices decide to make the
// same spend simultaneously - for example due a re-keying operation. It can also happen if there are malicious
// nodes in the P2P network that are mutating transactions on the fly as occurred during Feb 2014.
final Coin value = COIN;
final Coin value2 = valueOf(2, 0);
// Give us three coins and make sure we have some change.
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, value.add(value2));
Transaction send1 = checkNotNull(wallet.createSend(OTHER_ADDRESS, value2));
Transaction send2 = checkNotNull(wallet.createSend(OTHER_ADDRESS, value2));
byte[] buf = send1.bitcoinSerialize();
buf[43] = 0; // Break the signature: bitcoinj won't check in SPV mode and this is easier than other mutations.
send1 = PARAMS.getDefaultSerializer().makeTransaction(buf);
wallet.commitTx(send2);
wallet.allowSpendingUnconfirmedTransactions();
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
// Now spend the change. This transaction should die permanently when the mutant appears in the chain.
Transaction send3 = checkNotNull(wallet.createSend(OTHER_ADDRESS, value));
wallet.commitTx(send3);
assertEquals(ZERO, wallet.getBalance());
final LinkedList<TransactionConfidence> dead = new LinkedList<TransactionConfidence>();
final TransactionConfidence.Listener listener = new TransactionConfidence.Listener() {
@Override
public void onConfidenceChanged(TransactionConfidence confidence, ChangeReason reason) {
final TransactionConfidence.ConfidenceType type = confidence.getConfidenceType();
if (reason == ChangeReason.TYPE && type == TransactionConfidence.ConfidenceType.DEAD)
dead.add(confidence);
}
};
send2.getConfidence().addEventListener(Threading.SAME_THREAD, listener);
send3.getConfidence().addEventListener(Threading.SAME_THREAD, listener);
// Double spend!
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, send1);
// Back to having one coin.
assertEquals(value, wallet.getBalance());
assertEquals(send2.getHash(), dead.poll().getTransactionHash());
assertEquals(send3.getHash(), dead.poll().getTransactionHash());
}
@Test
public void doubleSpendFinneyAttack() throws Exception {
// A Finney attack is where a miner includes a transaction spending coins to themselves but does not
// broadcast it. When they find a solved block, they hold it back temporarily whilst they buy something with
// those same coins. After purchasing, they broadcast the block thus reversing the transaction. It can be
// done by any miner for products that can be bought at a chosen time and very quickly (as every second you
// withold your block means somebody else might find it first, invalidating your work).
//
// Test that we handle the attack correctly: a double spend on the chain moves transactions from pending to dead.
// This needs to work both for transactions we create, and that we receive from others.
final Transaction[] eventDead = new Transaction[1];
final Transaction[] eventReplacement = new Transaction[1];
final int[] eventWalletChanged = new int[1];
wallet.addTransactionConfidenceEventListener(new TransactionConfidenceEventListener() {
@Override
public void onTransactionConfidenceChanged(Wallet wallet, Transaction tx) {
if (tx.getConfidence().getConfidenceType() ==
TransactionConfidence.ConfidenceType.DEAD) {
eventDead[0] = tx;
eventReplacement[0] = tx.getConfidence().getOverridingTransaction();
}
}
});
wallet.addChangeEventListener(new WalletChangeEventListener() {
@Override
public void onWalletChanged(Wallet wallet) {
eventWalletChanged[0]++;
}
});
// Receive 1 BTC.
Coin nanos = COIN;
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, nanos);
Transaction received = wallet.getTransactions(false).iterator().next();
// Create a send to a merchant.
Transaction send1 = wallet.createSend(OTHER_ADDRESS, valueOf(0, 50));
// Create a double spend.
Address BAD_GUY = new ECKey().toAddress(PARAMS);
Transaction send2 = wallet.createSend(BAD_GUY, valueOf(0, 50));
send2 = PARAMS.getDefaultSerializer().makeTransaction(send2.bitcoinSerialize());
// Broadcast send1.
wallet.commitTx(send1);
assertEquals(send1, received.getOutput(0).getSpentBy().getParentTransaction());
// Receive a block that overrides it.
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, send2);
Threading.waitForUserCode();
assertEquals(send1, eventDead[0]);
assertEquals(send2, eventReplacement[0]);
assertEquals(TransactionConfidence.ConfidenceType.DEAD,
send1.getConfidence().getConfidenceType());
assertEquals(send2, received.getOutput(0).getSpentBy().getParentTransaction());
FakeTxBuilder.DoubleSpends doubleSpends = FakeTxBuilder.createFakeDoubleSpendTxns(PARAMS, myAddress);
// t1 spends to our wallet. t2 double spends somewhere else.
wallet.receivePending(doubleSpends.t1, null);
assertEquals(TransactionConfidence.ConfidenceType.PENDING,
doubleSpends.t1.getConfidence().getConfidenceType());
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, doubleSpends.t2);
Threading.waitForUserCode();
assertEquals(TransactionConfidence.ConfidenceType.DEAD,
doubleSpends.t1.getConfidence().getConfidenceType());
assertEquals(doubleSpends.t2, doubleSpends.t1.getConfidence().getOverridingTransaction());
assertEquals(5, eventWalletChanged[0]);
}
@Test
public void doubleSpendWeCreate() throws Exception {
// Test we keep pending double spends in IN_CONFLICT until one of them is included in a block
// and we handle reorgs and dependency chains properly.
// The following graph shows the txns we use in this test and how they are related
// (Eg txA1 spends txARoot outputs, txC1 spends txA1 and txB1 outputs, etc).
// txARoot (10) -> txA1 (1) -+
// |--> txC1 (0.10) -> txD1 (0.01)
// txBRoot (100) -> txB1 (11) -+
//
// txARoot (10) -> txA2 (2) -+
// |--> txC2 (0.20) -> txD2 (0.02)
// txBRoot (100) -> txB2 (22) -+
//
// txARoot (10) -> txA3 (3)
//
// txA1 is in conflict with txA2 and txA3. txB1 is in conflict with txB2.
CoinSelector originalCoinSelector = wallet.getCoinSelector();
try {
wallet.allowSpendingUnconfirmedTransactions();
Transaction txARoot = sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, valueOf(10, 0));
SendRequest a1Req = SendRequest.to(OTHER_ADDRESS, valueOf(1, 0));
a1Req.tx.addInput(txARoot.getOutput(0));
a1Req.shuffleOutputs = false;
wallet.completeTx(a1Req);
Transaction txA1 = a1Req.tx;
SendRequest a2Req = SendRequest.to(OTHER_ADDRESS, valueOf(2, 0));
a2Req.tx.addInput(txARoot.getOutput(0));
a2Req.shuffleOutputs = false;
wallet.completeTx(a2Req);
Transaction txA2 = a2Req.tx;
SendRequest a3Req = SendRequest.to(OTHER_ADDRESS, valueOf(3, 0));
a3Req.tx.addInput(txARoot.getOutput(0));
a3Req.shuffleOutputs = false;
wallet.completeTx(a3Req);
Transaction txA3 = a3Req.tx;
wallet.commitTx(txA1);
wallet.commitTx(txA2);
wallet.commitTx(txA3);
Transaction txBRoot = sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, valueOf(100, 0));
SendRequest b1Req = SendRequest.to(OTHER_ADDRESS, valueOf(11, 0));
b1Req.tx.addInput(txBRoot.getOutput(0));
b1Req.shuffleOutputs = false;
wallet.completeTx(b1Req);
Transaction txB1 = b1Req.tx;
SendRequest b2Req = SendRequest.to(OTHER_ADDRESS, valueOf(22, 0));
b2Req.tx.addInput(txBRoot.getOutput(0));
b2Req.shuffleOutputs = false;
wallet.completeTx(b2Req);
Transaction txB2 = b2Req.tx;
wallet.commitTx(txB1);
wallet.commitTx(txB2);
SendRequest c1Req = SendRequest.to(OTHER_ADDRESS, valueOf(0, 10));
c1Req.tx.addInput(txA1.getOutput(1));
c1Req.tx.addInput(txB1.getOutput(1));
c1Req.shuffleOutputs = false;
wallet.completeTx(c1Req);
Transaction txC1 = c1Req.tx;
SendRequest c2Req = SendRequest.to(OTHER_ADDRESS, valueOf(0, 20));
c2Req.tx.addInput(txA2.getOutput(1));
c2Req.tx.addInput(txB2.getOutput(1));
c2Req.shuffleOutputs = false;
wallet.completeTx(c2Req);
Transaction txC2 = c2Req.tx;
wallet.commitTx(txC1);
wallet.commitTx(txC2);
SendRequest d1Req = SendRequest.to(OTHER_ADDRESS, valueOf(0, 1));
d1Req.tx.addInput(txC1.getOutput(1));
d1Req.shuffleOutputs = false;
wallet.completeTx(d1Req);
Transaction txD1 = d1Req.tx;
SendRequest d2Req = SendRequest.to(OTHER_ADDRESS, valueOf(0, 2));
d2Req.tx.addInput(txC2.getOutput(1));
d2Req.shuffleOutputs = false;
wallet.completeTx(d2Req);
Transaction txD2 = d2Req.tx;
wallet.commitTx(txD1);
wallet.commitTx(txD2);
assertInConflict(txA1);
assertInConflict(txA2);
assertInConflict(txA3);
assertInConflict(txB1);
assertInConflict(txB2);
assertInConflict(txC1);
assertInConflict(txC2);
assertInConflict(txD1);
assertInConflict(txD2);
// Add a block to the block store. The rest of the blocks in this test will be on top of this one.
FakeTxBuilder.BlockPair blockPair0 = createFakeBlock(blockStore, 1);
// A block was mined including txA1
FakeTxBuilder.BlockPair blockPair1 = createFakeBlock(blockStore, 2, txA1);
wallet.receiveFromBlock(txA1, blockPair1.storedBlock, AbstractBlockChain.NewBlockType.BEST_CHAIN, 0);
wallet.notifyNewBestBlock(blockPair1.storedBlock);
assertSpent(txA1);
assertDead(txA2);
assertDead(txA3);
assertInConflict(txB1);
assertInConflict(txB2);
assertInConflict(txC1);
assertDead(txC2);
assertInConflict(txD1);
assertDead(txD2);
// A reorg: previous block "replaced" by new block containing txA1 and txB1
FakeTxBuilder.BlockPair blockPair2 = createFakeBlock(blockStore, blockPair0.storedBlock, 2, txA1, txB1);
wallet.receiveFromBlock(txA1, blockPair2.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 0);
wallet.receiveFromBlock(txB1, blockPair2.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 1);
wallet.reorganize(blockPair0.storedBlock, Lists.newArrayList(blockPair1.storedBlock),
Lists.newArrayList(blockPair2.storedBlock));
assertSpent(txA1);
assertDead(txA2);
assertDead(txA3);
assertSpent(txB1);
assertDead(txB2);
assertPending(txC1);
assertDead(txC2);
assertPending(txD1);
assertDead(txD2);
// A reorg: previous block "replaced" by new block containing txA1, txB1 and txC1
FakeTxBuilder.BlockPair blockPair3 = createFakeBlock(blockStore, blockPair0.storedBlock, 2, txA1, txB1,
txC1);
wallet.receiveFromBlock(txA1, blockPair3.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 0);
wallet.receiveFromBlock(txB1, blockPair3.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 1);
wallet.receiveFromBlock(txC1, blockPair3.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 2);
wallet.reorganize(blockPair0.storedBlock, Lists.newArrayList(blockPair2.storedBlock),
Lists.newArrayList(blockPair3.storedBlock));
assertSpent(txA1);
assertDead(txA2);
assertDead(txA3);
assertSpent(txB1);
assertDead(txB2);
assertSpent(txC1);
assertDead(txC2);
assertPending(txD1);
assertDead(txD2);
// A reorg: previous block "replaced" by new block containing txB1
FakeTxBuilder.BlockPair blockPair4 = createFakeBlock(blockStore, blockPair0.storedBlock, 2, txB1);
wallet.receiveFromBlock(txB1, blockPair4.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 0);
wallet.reorganize(blockPair0.storedBlock, Lists.newArrayList(blockPair3.storedBlock),
Lists.newArrayList(blockPair4.storedBlock));
assertPending(txA1);
assertDead(txA2);
assertDead(txA3);
assertSpent(txB1);
assertDead(txB2);
assertPending(txC1);
assertDead(txC2);
assertPending(txD1);
assertDead(txD2);
// A reorg: previous block "replaced" by new block containing txA2
FakeTxBuilder.BlockPair blockPair5 = createFakeBlock(blockStore, blockPair0.storedBlock, 2, txA2);
wallet.receiveFromBlock(txA2, blockPair5.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 0);
wallet.reorganize(blockPair0.storedBlock, Lists.newArrayList(blockPair4.storedBlock),
Lists.newArrayList(blockPair5.storedBlock));
assertDead(txA1);
assertUnspent(txA2);
assertDead(txA3);
assertPending(txB1);
assertDead(txB2);
assertDead(txC1);
assertDead(txC2);
assertDead(txD1);
assertDead(txD2);
// A reorg: previous block "replaced" by new empty block
FakeTxBuilder.BlockPair blockPair6 = createFakeBlock(blockStore, blockPair0.storedBlock, 2);
wallet.reorganize(blockPair0.storedBlock, Lists.newArrayList(blockPair5.storedBlock),
Lists.newArrayList(blockPair6.storedBlock));
assertDead(txA1);
assertPending(txA2);
assertDead(txA3);
assertPending(txB1);
assertDead(txB2);
assertDead(txC1);
assertDead(txC2);
assertDead(txD1);
assertDead(txD2);
} finally {
wallet.setCoinSelector(originalCoinSelector);
}
}
@Test
public void doubleSpendWeReceive() throws Exception {
FakeTxBuilder.DoubleSpends doubleSpends = FakeTxBuilder.createFakeDoubleSpendTxns(PARAMS, myAddress);
// doubleSpends.t1 spends to our wallet. doubleSpends.t2 double spends somewhere else.
Transaction t1b = new Transaction(PARAMS);
TransactionOutput t1bo = new TransactionOutput(PARAMS, t1b, valueOf(0, 50), OTHER_ADDRESS);
t1b.addOutput(t1bo);
t1b.addInput(doubleSpends.t1.getOutput(0));
wallet.receivePending(doubleSpends.t1, null);
wallet.receivePending(doubleSpends.t2, null);
wallet.receivePending(t1b, null);
assertInConflict(doubleSpends.t1);
assertInConflict(doubleSpends.t1);
assertInConflict(t1b);
// Add a block to the block store. The rest of the blocks in this test will be on top of this one.
FakeTxBuilder.BlockPair blockPair0 = createFakeBlock(blockStore, 1);
// A block was mined including doubleSpends.t1
FakeTxBuilder.BlockPair blockPair1 = createFakeBlock(blockStore, 2, doubleSpends.t1);
wallet.receiveFromBlock(doubleSpends.t1, blockPair1.storedBlock, AbstractBlockChain.NewBlockType.BEST_CHAIN, 0);
wallet.notifyNewBestBlock(blockPair1.storedBlock);
assertSpent(doubleSpends.t1);
assertDead(doubleSpends.t2);
assertPending(t1b);
// A reorg: previous block "replaced" by new block containing doubleSpends.t2
FakeTxBuilder.BlockPair blockPair2 = createFakeBlock(blockStore, blockPair0.storedBlock, 2, doubleSpends.t2);
wallet.receiveFromBlock(doubleSpends.t2, blockPair2.storedBlock, AbstractBlockChain.NewBlockType.SIDE_CHAIN, 0);
wallet.reorganize(blockPair0.storedBlock, Lists.newArrayList(blockPair1.storedBlock),
Lists.newArrayList(blockPair2.storedBlock));
assertDead(doubleSpends.t1);
assertSpent(doubleSpends.t2);
assertDead(t1b);
}
@Test
public void doubleSpendForBuildingTx() throws Exception {
CoinSelector originalCoinSelector = wallet.getCoinSelector();
try {
wallet.allowSpendingUnconfirmedTransactions();
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, valueOf(2, 0));
Transaction send1 = checkNotNull(wallet.createSend(OTHER_ADDRESS, valueOf(1, 0)));
Transaction send2 = checkNotNull(wallet.createSend(OTHER_ADDRESS, valueOf(1, 20)));
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, send1);
assertUnspent(send1);
wallet.receivePending(send2, null);
assertUnspent(send1);
assertDead(send2);
} finally {
wallet.setCoinSelector(originalCoinSelector);
}
}
@Test
public void txSpendingDeadTx() throws Exception {
CoinSelector originalCoinSelector = wallet.getCoinSelector();
try {
wallet.allowSpendingUnconfirmedTransactions();
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, valueOf(2, 0));
Transaction send1 = checkNotNull(wallet.createSend(OTHER_ADDRESS, valueOf(1, 0)));
Transaction send2 = checkNotNull(wallet.createSend(OTHER_ADDRESS, valueOf(1, 20)));
wallet.commitTx(send1);
assertPending(send1);
Transaction send1b = checkNotNull(wallet.createSend(OTHER_ADDRESS, valueOf(0, 50)));
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, send2);
assertDead(send1);
assertUnspent(send2);
wallet.receivePending(send1b, null);
assertDead(send1);
assertUnspent(send2);
assertDead(send1b);
} finally {
wallet.setCoinSelector(originalCoinSelector);
}
}
private void assertInConflict(Transaction tx) {
assertEquals(ConfidenceType.IN_CONFLICT, tx.getConfidence().getConfidenceType());
assertTrue(wallet.poolContainsTxHash(WalletTransaction.Pool.PENDING, tx.getHash()));
}
private void assertPending(Transaction tx) {
assertEquals(ConfidenceType.PENDING, tx.getConfidence().getConfidenceType());
assertTrue(wallet.poolContainsTxHash(WalletTransaction.Pool.PENDING, tx.getHash()));
}
private void assertSpent(Transaction tx) {
assertEquals(ConfidenceType.BUILDING, tx.getConfidence().getConfidenceType());
assertTrue(wallet.poolContainsTxHash(WalletTransaction.Pool.SPENT, tx.getHash()));
}
private void assertUnspent(Transaction tx) {
assertEquals(ConfidenceType.BUILDING, tx.getConfidence().getConfidenceType());
assertTrue(wallet.poolContainsTxHash(WalletTransaction.Pool.UNSPENT, tx.getHash()));
}
private void assertDead(Transaction tx) {
assertEquals(ConfidenceType.DEAD, tx.getConfidence().getConfidenceType());
assertTrue(wallet.poolContainsTxHash(WalletTransaction.Pool.DEAD, tx.getHash()));
}
@Test
public void testAddTransactionsDependingOn() throws Exception {
CoinSelector originalCoinSelector = wallet.getCoinSelector();
try {
wallet.allowSpendingUnconfirmedTransactions();
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, valueOf(2, 0));
Transaction send1 = checkNotNull(wallet.createSend(OTHER_ADDRESS, valueOf(1, 0)));
Transaction send2 = checkNotNull(wallet.createSend(OTHER_ADDRESS, valueOf(1, 20)));
wallet.commitTx(send1);
Transaction send1b = checkNotNull(wallet.createSend(OTHER_ADDRESS, valueOf(0, 50)));
wallet.commitTx(send1b);
Transaction send1c = checkNotNull(wallet.createSend(OTHER_ADDRESS, valueOf(0, 25)));
wallet.commitTx(send1c);
wallet.commitTx(send2);
Set<Transaction> txns = new HashSet<Transaction>();
txns.add(send1);
wallet.addTransactionsDependingOn(txns, wallet.getTransactions(true));
assertEquals(3, txns.size());
assertTrue(txns.contains(send1));
assertTrue(txns.contains(send1b));
assertTrue(txns.contains(send1c));
} finally {
wallet.setCoinSelector(originalCoinSelector);
}
}
@Test
public void sortTxnsByDependency() throws Exception {
CoinSelector originalCoinSelector = wallet.getCoinSelector();
try {
wallet.allowSpendingUnconfirmedTransactions();
Transaction send1 = sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, valueOf(2, 0));
Transaction send1a = checkNotNull(wallet.createSend(OTHER_ADDRESS, valueOf(1, 0)));
wallet.commitTx(send1a);
Transaction send1b = checkNotNull(wallet.createSend(OTHER_ADDRESS, valueOf(0, 50)));
wallet.commitTx(send1b);
Transaction send1c = checkNotNull(wallet.createSend(OTHER_ADDRESS, valueOf(0, 25)));
wallet.commitTx(send1c);
Transaction send1d = checkNotNull(wallet.createSend(OTHER_ADDRESS, valueOf(0, 12)));
wallet.commitTx(send1d);
Transaction send1e = checkNotNull(wallet.createSend(OTHER_ADDRESS, valueOf(0, 06)));
wallet.commitTx(send1e);
Transaction send2 = sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, valueOf(200, 0));
SendRequest req2a = SendRequest.to(OTHER_ADDRESS, valueOf(100, 0));
req2a.tx.addInput(send2.getOutput(0));
req2a.shuffleOutputs = false;
wallet.completeTx(req2a);
Transaction send2a = req2a.tx;
SendRequest req2b = SendRequest.to(OTHER_ADDRESS, valueOf(50, 0));
req2b.tx.addInput(send2a.getOutput(1));
req2b.shuffleOutputs = false;
wallet.completeTx(req2b);
Transaction send2b = req2b.tx;
SendRequest req2c = SendRequest.to(OTHER_ADDRESS, valueOf(25, 0));
req2c.tx.addInput(send2b.getOutput(1));
req2c.shuffleOutputs = false;
wallet.completeTx(req2c);
Transaction send2c = req2c.tx;
Set<Transaction> unsortedTxns = new HashSet<Transaction>();
unsortedTxns.add(send1a);
unsortedTxns.add(send1b);
unsortedTxns.add(send1c);
unsortedTxns.add(send1d);
unsortedTxns.add(send1e);
unsortedTxns.add(send2a);
unsortedTxns.add(send2b);
unsortedTxns.add(send2c);
List<Transaction> sortedTxns = wallet.sortTxnsByDependency(unsortedTxns);
assertEquals(8, sortedTxns.size());
assertTrue(sortedTxns.indexOf(send1a) < sortedTxns.indexOf(send1b));
assertTrue(sortedTxns.indexOf(send1b) < sortedTxns.indexOf(send1c));
assertTrue(sortedTxns.indexOf(send1c) < sortedTxns.indexOf(send1d));
assertTrue(sortedTxns.indexOf(send1d) < sortedTxns.indexOf(send1e));
assertTrue(sortedTxns.indexOf(send2a) < sortedTxns.indexOf(send2b));
assertTrue(sortedTxns.indexOf(send2b) < sortedTxns.indexOf(send2c));
} finally {
wallet.setCoinSelector(originalCoinSelector);
}
}
@Test
public void pending1() throws Exception {
// Check that if we receive a pending transaction that is then confirmed, we are notified as appropriate.
final Coin nanos = COIN;
final Transaction t1 = createFakeTx(PARAMS, nanos, myAddress);
// First one is "called" second is "pending".
final boolean[] flags = new boolean[2];
final Transaction[] notifiedTx = new Transaction[1];
final int[] walletChanged = new int[1];
wallet.addCoinsReceivedEventListener(new WalletCoinsReceivedEventListener() {
@Override
public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
// Check we got the expected transaction.
assertEquals(tx, t1);
// Check that it's considered to be pending inclusion in the block chain.
assertEquals(prevBalance, ZERO);
assertEquals(newBalance, nanos);
flags[0] = true;
flags[1] = tx.isPending();
notifiedTx[0] = tx;
}
});
wallet.addChangeEventListener(new WalletChangeEventListener() {
@Override
public void onWalletChanged(Wallet wallet) {
walletChanged[0]++;
}
});
if (wallet.isPendingTransactionRelevant(t1))
wallet.receivePending(t1, null);
Threading.waitForUserCode();
assertTrue(flags[0]);
assertTrue(flags[1]); // is pending
flags[0] = false;
// Check we don't get notified if we receive it again.
assertFalse(wallet.isPendingTransactionRelevant(t1));
assertFalse(flags[0]);
// Now check again, that we should NOT be notified when we receive it via a block (we were already notified).
// However the confidence should be updated.
// Make a fresh copy of the tx to ensure we're testing realistically.
flags[0] = flags[1] = false;
final TransactionConfidence.Listener.ChangeReason[] reasons = new TransactionConfidence.Listener.ChangeReason[1];
notifiedTx[0].getConfidence().addEventListener(new TransactionConfidence.Listener() {
@Override
public void onConfidenceChanged(TransactionConfidence confidence, TransactionConfidence.Listener.ChangeReason reason) {
flags[1] = true;
reasons[0] = reason;
}
});
assertEquals(TransactionConfidence.ConfidenceType.PENDING,
notifiedTx[0].getConfidence().getConfidenceType());
// Send a block with nothing interesting. Verify we don't get a callback.
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN);
Threading.waitForUserCode();
assertNull(reasons[0]);
final Transaction t1Copy = PARAMS.getDefaultSerializer().makeTransaction(t1.bitcoinSerialize());
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, t1Copy);
Threading.waitForUserCode();
assertFalse(flags[0]);
assertTrue(flags[1]);
assertEquals(TransactionConfidence.ConfidenceType.BUILDING, notifiedTx[0].getConfidence().getConfidenceType());
// Check we don't get notified about an irrelevant transaction.
flags[0] = false;
flags[1] = false;
Transaction irrelevant = createFakeTx(PARAMS, nanos, OTHER_ADDRESS);
if (wallet.isPendingTransactionRelevant(irrelevant))
wallet.receivePending(irrelevant, null);
Threading.waitForUserCode();
assertFalse(flags[0]);
assertEquals(3, walletChanged[0]);
}
@Test
public void pending2() throws Exception {
// Check that if we receive a pending tx we did not send, it updates our spent flags correctly.
final Transaction[] txn = new Transaction[1];
final Coin[] bigints = new Coin[2];
wallet.addCoinsSentEventListener(new WalletCoinsSentEventListener() {
@Override
public void onCoinsSent(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
txn[0] = tx;
bigints[0] = prevBalance;
bigints[1] = newBalance;
}
});
// Receive some coins.
Coin nanos = COIN;
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, nanos);
// Create a spend with them, but don't commit it (ie it's from somewhere else but using our keys). This TX
// will have change as we don't spend our entire balance.
Coin halfNanos = valueOf(0, 50);
Transaction t2 = wallet.createSend(OTHER_ADDRESS, halfNanos);
// Now receive it as pending.
if (wallet.isPendingTransactionRelevant(t2))
wallet.receivePending(t2, null);
// We received an onCoinsSent() callback.
Threading.waitForUserCode();
assertEquals(t2, txn[0]);
assertEquals(nanos, bigints[0]);
assertEquals(halfNanos, bigints[1]);
// Our balance is now 0.50 BTC
assertEquals(halfNanos, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
}
@Test
public void pending3() throws Exception {
// Check that if we receive a pending tx, and it's overridden by a double spend from the main chain, we
// are notified that it's dead. This should work even if the pending tx inputs are NOT ours, ie, they don't
// connect to anything.
Coin nanos = COIN;
// Create two transactions that share the same input tx.
Address badGuy = new ECKey().toAddress(PARAMS);
Transaction doubleSpentTx = new Transaction(PARAMS);
TransactionOutput doubleSpentOut = new TransactionOutput(PARAMS, doubleSpentTx, nanos, badGuy);
doubleSpentTx.addOutput(doubleSpentOut);
Transaction t1 = new Transaction(PARAMS);
TransactionOutput o1 = new TransactionOutput(PARAMS, t1, nanos, myAddress);
t1.addOutput(o1);
t1.addInput(doubleSpentOut);
Transaction t2 = new Transaction(PARAMS);
TransactionOutput o2 = new TransactionOutput(PARAMS, t2, nanos, badGuy);
t2.addOutput(o2);
t2.addInput(doubleSpentOut);
final Transaction[] called = new Transaction[2];
wallet.addCoinsReceivedEventListener(new WalletCoinsReceivedEventListener() {
@Override
public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
called[0] = tx;
}
});
wallet.addTransactionConfidenceEventListener(new TransactionConfidenceEventListener() {
@Override
public void onTransactionConfidenceChanged(Wallet wallet, Transaction tx) {
if (tx.getConfidence().getConfidenceType() ==
TransactionConfidence.ConfidenceType.DEAD) {
called[0] = tx;
called[1] = tx.getConfidence().getOverridingTransaction();
}
}
});
assertEquals(ZERO, wallet.getBalance());
if (wallet.isPendingTransactionRelevant(t1))
wallet.receivePending(t1, null);
Threading.waitForUserCode();
assertEquals(t1, called[0]);
assertEquals(nanos, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
// Now receive a double spend on the main chain.
called[0] = called[1] = null;
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, t2);
Threading.waitForUserCode();
assertEquals(ZERO, wallet.getBalance());
assertEquals(t1, called[0]); // dead
assertEquals(t2, called[1]); // replacement
}
@Test
public void transactionsList() throws Exception {
// Check the wallet can give us an ordered list of all received transactions.
Utils.setMockClock();
Transaction tx1 = sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, COIN);
Utils.rollMockClock(60 * 10);
Transaction tx2 = sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, valueOf(0, 5));
// Check we got them back in order.
List<Transaction> transactions = wallet.getTransactionsByTime();
assertEquals(tx2, transactions.get(0));
assertEquals(tx1, transactions.get(1));
assertEquals(2, transactions.size());
// Check we get only the last transaction if we request a subrage.
transactions = wallet.getRecentTransactions(1, false);
assertEquals(1, transactions.size());
assertEquals(tx2, transactions.get(0));
// Create a spend five minutes later.
Utils.rollMockClock(60 * 5);
Transaction tx3 = wallet.createSend(OTHER_ADDRESS, valueOf(0, 5));
// Does not appear in list yet.
assertEquals(2, wallet.getTransactionsByTime().size());
wallet.commitTx(tx3);
// Now it does.
transactions = wallet.getTransactionsByTime();
assertEquals(3, transactions.size());
assertEquals(tx3, transactions.get(0));
// Verify we can handle the case of older wallets in which the timestamp is null (guessed from the
// block appearances list).
tx1.setUpdateTime(null);
tx3.setUpdateTime(null);
// Check we got them back in order.
transactions = wallet.getTransactionsByTime();
assertEquals(tx2, transactions.get(0));
assertEquals(3, transactions.size());
}
@Test
public void keyCreationTime() throws Exception {
Utils.setMockClock();
long now = Utils.currentTimeSeconds();
wallet = new Wallet(PARAMS);
assertEquals(now, wallet.getEarliestKeyCreationTime());
Utils.rollMockClock(60);
wallet.freshReceiveKey();
assertEquals(now, wallet.getEarliestKeyCreationTime());
}
@Test
public void scriptCreationTime() throws Exception {
Utils.setMockClock();
long now = Utils.currentTimeSeconds();
wallet = new Wallet(PARAMS);
assertEquals(now, wallet.getEarliestKeyCreationTime());
Utils.rollMockClock(-120);
wallet.addWatchedAddress(OTHER_ADDRESS);
wallet.freshReceiveKey();
assertEquals(now - 120, wallet.getEarliestKeyCreationTime());
}
@Test
public void spendToSameWallet() throws Exception {
// Test that a spend to the same wallet is dealt with correctly.
// It should appear in the wallet and confirm.
// This is a bit of a silly thing to do in the real world as all it does is burn a fee but it is perfectly valid.
Coin coin1 = COIN;
Coin coinHalf = valueOf(0, 50);
// Start by giving us 1 coin.
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, coin1);
// Send half to ourselves. We should then have a balance available to spend of zero.
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals(1, wallet.getTransactions(true).size());
Transaction outbound1 = wallet.createSend(myAddress, coinHalf);
wallet.commitTx(outbound1);
// We should have a zero available balance before the next block.
assertEquals(ZERO, wallet.getBalance());
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, outbound1);
// We should have a balance of 1 BTC after the block is received.
assertEquals(coin1, wallet.getBalance());
}
@Test
public void lastBlockSeen() throws Exception {
Coin v1 = valueOf(5, 0);
Coin v2 = valueOf(0, 50);
Coin v3 = valueOf(0, 25);
Transaction t1 = createFakeTx(PARAMS, v1, myAddress);
Transaction t2 = createFakeTx(PARAMS, v2, myAddress);
Transaction t3 = createFakeTx(PARAMS, v3, myAddress);
Block genesis = blockStore.getChainHead().getHeader();
Block b10 = makeSolvedTestBlock(genesis, t1);
Block b11 = makeSolvedTestBlock(genesis, t2);
Block b2 = makeSolvedTestBlock(b10, t3);
Block b3 = makeSolvedTestBlock(b2);
// Receive a block on the best chain - this should set the last block seen hash.
chain.add(b10);
assertEquals(b10.getHash(), wallet.getLastBlockSeenHash());
assertEquals(b10.getTimeSeconds(), wallet.getLastBlockSeenTimeSecs());
assertEquals(1, wallet.getLastBlockSeenHeight());
// Receive a block on the side chain - this should not change the last block seen hash.
chain.add(b11);
assertEquals(b10.getHash(), wallet.getLastBlockSeenHash());
// Receive block 2 on the best chain - this should change the last block seen hash.
chain.add(b2);
assertEquals(b2.getHash(), wallet.getLastBlockSeenHash());
// Receive block 3 on the best chain - this should change the last block seen hash despite having no txns.
chain.add(b3);
assertEquals(b3.getHash(), wallet.getLastBlockSeenHash());
}
@Test
public void pubkeyOnlyScripts() throws Exception {
// Verify that we support outputs like OP_PUBKEY and the corresponding inputs.
ECKey key1 = wallet.freshReceiveKey();
Coin value = valueOf(5, 0);
Transaction t1 = createFakeTx(PARAMS, value, key1);
if (wallet.isPendingTransactionRelevant(t1))
wallet.receivePending(t1, null);
// TX should have been seen as relevant.
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
assertEquals(ZERO, wallet.getBalance(Wallet.BalanceType.AVAILABLE));
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, t1);
// TX should have been seen as relevant, extracted and processed.
assertEquals(value, wallet.getBalance(Wallet.BalanceType.AVAILABLE));
// Spend it and ensure we can spend the <key> OP_CHECKSIG output correctly.
Transaction t2 = wallet.createSend(OTHER_ADDRESS, value);
assertNotNull(t2);
// TODO: This code is messy, improve the Script class and fixinate!
assertEquals(t2.toString(), 1, t2.getInputs().get(0).getScriptSig().getChunks().size());
assertTrue(t2.getInputs().get(0).getScriptSig().getChunks().get(0).data.length > 50);
}
@Test
public void isWatching() {
assertFalse(wallet.isWatching());
Wallet watchingWallet = Wallet.fromWatchingKey(PARAMS, wallet.getWatchingKey().dropPrivateBytes().dropParent());
assertTrue(watchingWallet.isWatching());
wallet.encrypt(PASSWORD1);
assertFalse(wallet.isWatching());
}
@Test
public void watchingWallet() throws Exception {
DeterministicKey watchKey = wallet.getWatchingKey();
String serialized = watchKey.serializePubB58(PARAMS);
// Construct watching wallet.
Wallet watchingWallet = Wallet.fromWatchingKey(PARAMS, DeterministicKey.deserializeB58(null, serialized, PARAMS));
DeterministicKey key2 = watchingWallet.freshReceiveKey();
assertEquals(myKey, key2);
ECKey key = wallet.freshKey(KeyChain.KeyPurpose.CHANGE);
key2 = watchingWallet.freshKey(KeyChain.KeyPurpose.CHANGE);
assertEquals(key, key2);
key.sign(Sha256Hash.ZERO_HASH);
try {
key2.sign(Sha256Hash.ZERO_HASH);
fail();
} catch (ECKey.MissingPrivateKeyException e) {
// Expected
}
receiveATransaction(watchingWallet, myKey.toAddress(PARAMS));
assertEquals(COIN, watchingWallet.getBalance());
assertEquals(COIN, watchingWallet.getBalance(Wallet.BalanceType.AVAILABLE));
assertEquals(ZERO, watchingWallet.getBalance(Wallet.BalanceType.AVAILABLE_SPENDABLE));
}
@Test(expected = ECKey.MissingPrivateKeyException.class)
public void watchingWalletWithCreationTime() throws Exception {
DeterministicKey watchKey = wallet.getWatchingKey();
String serialized = watchKey.serializePubB58(PARAMS);
Wallet watchingWallet = Wallet.fromWatchingKeyB58(PARAMS, serialized, 1415282801);
DeterministicKey key2 = watchingWallet.freshReceiveKey();
assertEquals(myKey, key2);
ECKey key = wallet.freshKey(KeyChain.KeyPurpose.CHANGE);
key2 = watchingWallet.freshKey(KeyChain.KeyPurpose.CHANGE);
assertEquals(key, key2);
key.sign(Sha256Hash.ZERO_HASH);
key2.sign(Sha256Hash.ZERO_HASH);
}
@Test
public void watchingScripts() throws Exception {
// Verify that pending transactions to watched addresses are relevant
Address watchedAddress = new ECKey().toAddress(PARAMS);
wallet.addWatchedAddress(watchedAddress);
Coin value = valueOf(5, 0);
Transaction t1 = createFakeTx(PARAMS, value, watchedAddress);
assertTrue(t1.getWalletOutputs(wallet).size() >= 1);
assertTrue(wallet.isPendingTransactionRelevant(t1));
}
@Test(expected = InsufficientMoneyException.class)
public void watchingScriptsConfirmed() throws Exception {
Address watchedAddress = new ECKey().toAddress(PARAMS);
wallet.addWatchedAddress(watchedAddress);
sendMoneyToWallet(BlockChain.NewBlockType.BEST_CHAIN, CENT, watchedAddress);
assertEquals(CENT, wallet.getBalance());
// We can't spend watched balances
wallet.createSend(OTHER_ADDRESS, CENT);
}
@Test
public void watchingScriptsSentFrom() throws Exception {
int baseElements = wallet.getBloomFilterElementCount();
Address watchedAddress = new ECKey().toAddress(PARAMS);
wallet.addWatchedAddress(watchedAddress);
assertEquals(baseElements + 1, wallet.getBloomFilterElementCount());
Transaction t1 = createFakeTx(PARAMS, CENT, watchedAddress);
Transaction t2 = createFakeTx(PARAMS, COIN, OTHER_ADDRESS);
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, t1);
assertEquals(baseElements + 2, wallet.getBloomFilterElementCount());
Transaction st2 = new Transaction(PARAMS);
st2.addOutput(CENT, OTHER_ADDRESS);
st2.addOutput(COIN, OTHER_ADDRESS);
st2.addInput(t1.getOutput(0));
st2.addInput(t2.getOutput(0));
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, st2);
assertEquals(baseElements + 2, wallet.getBloomFilterElementCount());
assertEquals(CENT, st2.getValueSentFromMe(wallet));
}
@Test
public void watchingScriptsBloomFilter() throws Exception {
assertFalse(wallet.isRequiringUpdateAllBloomFilter());
Address watchedAddress = new ECKey().toAddress(PARAMS);
Transaction t1 = createFakeTx(PARAMS, CENT, watchedAddress);
TransactionOutPoint outPoint = new TransactionOutPoint(PARAMS, 0, t1);
wallet.addWatchedAddress(watchedAddress);
assertTrue(wallet.isRequiringUpdateAllBloomFilter());
// Note that this has a 1e-12 chance of failing this unit test due to a false positive
assertFalse(wallet.getBloomFilter(1e-12).contains(outPoint.unsafeBitcoinSerialize()));
sendMoneyToWallet(BlockChain.NewBlockType.BEST_CHAIN, t1);
assertTrue(wallet.getBloomFilter(1e-12).contains(outPoint.unsafeBitcoinSerialize()));
}
@Test
public void getWatchedAddresses() throws Exception {
Address watchedAddress = new ECKey().toAddress(PARAMS);
wallet.addWatchedAddress(watchedAddress);
List<Address> watchedAddresses = wallet.getWatchedAddresses();
assertEquals(1, watchedAddresses.size());
assertEquals(watchedAddress, watchedAddresses.get(0));
}
@Test
public void removeWatchedAddresses() {
List<Address> addressesForRemoval = new ArrayList<Address>();
for (int i = 0; i < 10; i++) {
Address watchedAddress = new ECKey().toAddress(PARAMS);
addressesForRemoval.add(watchedAddress);
wallet.addWatchedAddress(watchedAddress);
}
wallet.removeWatchedAddresses(addressesForRemoval);
for (Address addr : addressesForRemoval)
assertFalse(wallet.isAddressWatched(addr));
assertFalse(wallet.isRequiringUpdateAllBloomFilter());
}
@Test
public void removeWatchedAddress() {
Address watchedAddress = new ECKey().toAddress(PARAMS);
wallet.addWatchedAddress(watchedAddress);
wallet.removeWatchedAddress(watchedAddress);
assertFalse(wallet.isAddressWatched(watchedAddress));
assertFalse(wallet.isRequiringUpdateAllBloomFilter());
}
@Test
public void removeScriptsBloomFilter() throws Exception {
List<Address> addressesForRemoval = new ArrayList<Address>();
for (int i = 0; i < 10; i++) {
Address watchedAddress = new ECKey().toAddress(PARAMS);
addressesForRemoval.add(watchedAddress);
wallet.addWatchedAddress(watchedAddress);
}
wallet.removeWatchedAddresses(addressesForRemoval);
for (Address addr : addressesForRemoval) {
Transaction t1 = createFakeTx(PARAMS, CENT, addr);
TransactionOutPoint outPoint = new TransactionOutPoint(PARAMS, 0, t1);
// Note that this has a 1e-12 chance of failing this unit test due to a false positive
assertFalse(wallet.getBloomFilter(1e-12).contains(outPoint.unsafeBitcoinSerialize()));
sendMoneyToWallet(BlockChain.NewBlockType.BEST_CHAIN, t1);
assertFalse(wallet.getBloomFilter(1e-12).contains(outPoint.unsafeBitcoinSerialize()));
}
}
@Test
public void marriedKeychainBloomFilter() throws Exception {
createMarriedWallet(2, 2);
Address address = wallet.currentReceiveAddress();
assertTrue(wallet.getBloomFilter(0.001).contains(address.getHash160()));
Transaction t1 = createFakeTx(PARAMS, CENT, address);
TransactionOutPoint outPoint = new TransactionOutPoint(PARAMS, 0, t1);
assertFalse(wallet.getBloomFilter(0.001).contains(outPoint.unsafeBitcoinSerialize()));
sendMoneyToWallet(BlockChain.NewBlockType.BEST_CHAIN, t1);
assertTrue(wallet.getBloomFilter(0.001).contains(outPoint.unsafeBitcoinSerialize()));
}
@Test
public void autosaveImmediate() throws Exception {
// Test that the wallet will save itself automatically when it changes.
File f = File.createTempFile("bitcoinj-unit-test", null);
Sha256Hash hash1 = Sha256Hash.of(f);
// Start with zero delay and ensure the wallet file changes after adding a key.
wallet.autosaveToFile(f, 0, TimeUnit.SECONDS, null);
ECKey key = wallet.freshReceiveKey();
Sha256Hash hash2 = Sha256Hash.of(f);
assertFalse("Wallet not saved after generating fresh key", hash1.equals(hash2)); // File has changed.
Transaction t1 = createFakeTx(PARAMS, valueOf(5, 0), key);
if (wallet.isPendingTransactionRelevant(t1))
wallet.receivePending(t1, null);
Sha256Hash hash3 = Sha256Hash.of(f);
assertFalse("Wallet not saved after receivePending", hash2.equals(hash3)); // File has changed again.
}
@Test
public void autosaveDelayed() throws Exception {
// Test that the wallet will save itself automatically when it changes, but not immediately and near-by
// updates are coalesced together. This test is a bit racy, it assumes we can complete the unit test within
// an auto-save cycle of 1 second.
final File[] results = new File[2];
final CountDownLatch latch = new CountDownLatch(3);
File f = File.createTempFile("bitcoinj-unit-test", null);
Sha256Hash hash1 = Sha256Hash.of(f);
wallet.autosaveToFile(f, 1, TimeUnit.SECONDS,
new WalletFiles.Listener() {
@Override
public void onBeforeAutoSave(File tempFile) {
results[0] = tempFile;
}
@Override
public void onAfterAutoSave(File newlySavedFile) {
results[1] = newlySavedFile;
latch.countDown();
}
}
);
ECKey key = wallet.freshReceiveKey();
Sha256Hash hash2 = Sha256Hash.of(f);
assertFalse(hash1.equals(hash2)); // File has changed immediately despite the delay, as keys are important.
assertNotNull(results[0]);
assertEquals(f, results[1]);
results[0] = results[1] = null;
sendMoneyToWallet(BlockChain.NewBlockType.BEST_CHAIN);
Sha256Hash hash3 = Sha256Hash.of(f);
assertEquals(hash2, hash3); // File has NOT changed yet. Just new blocks with no txns - delayed.
assertNull(results[0]);
assertNull(results[1]);
sendMoneyToWallet(BlockChain.NewBlockType.BEST_CHAIN, valueOf(5, 0), key);
Sha256Hash hash4 = Sha256Hash.of(f);
assertFalse(hash3.equals(hash4)); // File HAS changed.
results[0] = results[1] = null;
// A block that contains some random tx we don't care about.
sendMoneyToWallet(BlockChain.NewBlockType.BEST_CHAIN, Coin.COIN, OTHER_ADDRESS);
assertEquals(hash4, Sha256Hash.of(f)); // File has NOT changed.
assertNull(results[0]);
assertNull(results[1]);
// Wait for an auto-save to occur.
latch.await();
Sha256Hash hash5 = Sha256Hash.of(f);
assertFalse(hash4.equals(hash5)); // File has now changed.
assertNotNull(results[0]);
assertEquals(f, results[1]);
// Now we shutdown auto-saving and expect wallet changes to remain unsaved, even "important" changes.
wallet.shutdownAutosaveAndWait();
results[0] = results[1] = null;
ECKey key2 = new ECKey();
wallet.importKey(key2);
assertEquals(hash5, Sha256Hash.of(f)); // File has NOT changed.
sendMoneyToWallet(BlockChain.NewBlockType.BEST_CHAIN, valueOf(5, 0), key2);
Thread.sleep(2000); // Wait longer than autosave delay. TODO Fix the racyness.
assertEquals(hash5, Sha256Hash.of(f)); // File has still NOT changed.
assertNull(results[0]);
assertNull(results[1]);
}
@Test
public void spendOutputFromPendingTransaction() throws Exception {
// We'll set up a wallet that receives a coin, then sends a coin of lesser value and keeps the change.
Coin v1 = COIN;
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, v1);
// First create our current transaction
ECKey k2 = wallet.freshReceiveKey();
Coin v2 = valueOf(0, 50);
Transaction t2 = new Transaction(PARAMS);
TransactionOutput o2 = new TransactionOutput(PARAMS, t2, v2, k2.toAddress(PARAMS));
t2.addOutput(o2);
SendRequest req = SendRequest.forTx(t2);
wallet.completeTx(req);
// Commit t2, so it is placed in the pending pool
wallet.commitTx(t2);
assertEquals(0, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.PENDING));
assertEquals(2, wallet.getTransactions(true).size());
// Now try to the spend the output.
ECKey k3 = new ECKey();
Coin v3 = valueOf(0, 25);
Transaction t3 = new Transaction(PARAMS);
t3.addOutput(v3, k3.toAddress(PARAMS));
t3.addInput(o2);
wallet.signTransaction(SendRequest.forTx(t3));
// Commit t3, so the coins from the pending t2 are spent
wallet.commitTx(t3);
assertEquals(0, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT));
assertEquals(2, wallet.getPoolSize(WalletTransaction.Pool.PENDING));
assertEquals(3, wallet.getTransactions(true).size());
// Now the output of t2 must not be available for spending
assertFalse(o2.isAvailableForSpending());
}
@Test
public void replayWhilstPending() throws Exception {
// Check that if a pending transaction spends outputs of chain-included transactions, we mark them as spent.
// See bug 345. This can happen if there is a pending transaction floating around and then you replay the
// chain without emptying the memory pool (or refilling it from a peer).
Coin value = COIN;
Transaction tx1 = createFakeTx(PARAMS, value, myAddress);
Transaction tx2 = new Transaction(PARAMS);
tx2.addInput(tx1.getOutput(0));
tx2.addOutput(valueOf(0, 9), OTHER_ADDRESS);
// Add a change address to ensure this tx is relevant.
tx2.addOutput(CENT, wallet.currentChangeAddress());
wallet.receivePending(tx2, null);
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, tx1);
assertEquals(ZERO, wallet.getBalance());
assertEquals(1, wallet.getPoolSize(Pool.SPENT));
assertEquals(1, wallet.getPoolSize(Pool.PENDING));
assertEquals(0, wallet.getPoolSize(Pool.UNSPENT));
}
@Test
public void outOfOrderPendingTxns() throws Exception {
// Check that if there are two pending transactions which we receive out of order, they are marked as spent
// correctly. For instance, we are watching a wallet, someone pays us (A) and we then pay someone else (B)
// with a change address but the network delivers the transactions to us in order B then A.
Coin value = COIN;
Transaction a = createFakeTx(PARAMS, value, myAddress);
Transaction b = new Transaction(PARAMS);
b.addInput(a.getOutput(0));
b.addOutput(CENT, OTHER_ADDRESS);
Coin v = COIN.subtract(CENT);
b.addOutput(v, wallet.currentChangeAddress());
a = roundTripTransaction(PARAMS, a);
b = roundTripTransaction(PARAMS, b);
wallet.receivePending(b, null);
assertEquals(v, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
wallet.receivePending(a, null);
assertEquals(v, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
}
@Test
public void encryptionDecryptionAESBasic() throws Exception {
Wallet encryptedWallet = new Wallet(PARAMS);
encryptedWallet.encrypt(PASSWORD1);
KeyCrypter keyCrypter = encryptedWallet.getKeyCrypter();
KeyParameter aesKey = keyCrypter.deriveKey(PASSWORD1);
assertEquals(EncryptionType.ENCRYPTED_SCRYPT_AES, encryptedWallet.getEncryptionType());
assertTrue(encryptedWallet.checkPassword(PASSWORD1));
assertTrue(encryptedWallet.checkAESKey(aesKey));
assertFalse(encryptedWallet.checkPassword(WRONG_PASSWORD));
assertNotNull("The keyCrypter is missing but should not be", keyCrypter);
encryptedWallet.decrypt(aesKey);
// Wallet should now be unencrypted.
assertNull("Wallet is not an unencrypted wallet", encryptedWallet.getKeyCrypter());
try {
encryptedWallet.checkPassword(PASSWORD1);
fail();
} catch (IllegalStateException e) {
}
}
@Test
public void encryptionDecryptionPasswordBasic() throws Exception {
Wallet encryptedWallet = new Wallet(PARAMS);
encryptedWallet.encrypt(PASSWORD1);
assertTrue(encryptedWallet.isEncrypted());
encryptedWallet.decrypt(PASSWORD1);
assertFalse(encryptedWallet.isEncrypted());
// Wallet should now be unencrypted.
assertNull("Wallet is not an unencrypted wallet", encryptedWallet.getKeyCrypter());
try {
encryptedWallet.checkPassword(PASSWORD1);
fail();
} catch (IllegalStateException e) {
}
}
@Test
public void encryptionDecryptionBadPassword() throws Exception {
Wallet encryptedWallet = new Wallet(PARAMS);
encryptedWallet.encrypt(PASSWORD1);
KeyCrypter keyCrypter = encryptedWallet.getKeyCrypter();
KeyParameter wrongAesKey = keyCrypter.deriveKey(WRONG_PASSWORD);
// Check the wallet is currently encrypted
assertEquals("Wallet is not an encrypted wallet", EncryptionType.ENCRYPTED_SCRYPT_AES, encryptedWallet.getEncryptionType());
assertFalse(encryptedWallet.checkAESKey(wrongAesKey));
// Check that the wrong password does not decrypt the wallet.
try {
encryptedWallet.decrypt(wrongAesKey);
fail("Incorrectly decoded wallet with wrong password");
} catch (KeyCrypterException ede) {
// Expected.
}
}
@Test
public void changePasswordTest() {
Wallet encryptedWallet = new Wallet(PARAMS);
encryptedWallet.encrypt(PASSWORD1);
CharSequence newPassword = "My name is Tom";
encryptedWallet.changeEncryptionPassword(PASSWORD1, newPassword);
assertTrue(encryptedWallet.checkPassword(newPassword));
assertFalse(encryptedWallet.checkPassword(WRONG_PASSWORD));
}
@Test
public void changeAesKeyTest() {
Wallet encryptedWallet = new Wallet(PARAMS);
encryptedWallet.encrypt(PASSWORD1);
KeyCrypter keyCrypter = encryptedWallet.getKeyCrypter();
KeyParameter aesKey = keyCrypter.deriveKey(PASSWORD1);
CharSequence newPassword = "My name is Tom";
KeyParameter newAesKey = keyCrypter.deriveKey(newPassword);
encryptedWallet.changeEncryptionKey(keyCrypter, aesKey, newAesKey);
assertTrue(encryptedWallet.checkAESKey(newAesKey));
assertFalse(encryptedWallet.checkAESKey(aesKey));
}
@Test
public void encryptionDecryptionCheckExceptions() throws Exception {
Wallet encryptedWallet = new Wallet(PARAMS);
encryptedWallet.encrypt(PASSWORD1);
KeyCrypter keyCrypter = encryptedWallet.getKeyCrypter();
KeyParameter aesKey = keyCrypter.deriveKey(PASSWORD1);
// Check the wallet is currently encrypted
assertEquals("Wallet is not an encrypted wallet", EncryptionType.ENCRYPTED_SCRYPT_AES, encryptedWallet.getEncryptionType());
// Decrypt wallet.
assertNotNull("The keyCrypter is missing but should not be", keyCrypter);
encryptedWallet.decrypt(aesKey);
// Try decrypting it again
try {
assertNotNull("The keyCrypter is missing but should not be", keyCrypter);
encryptedWallet.decrypt(aesKey);
fail("Should not be able to decrypt a decrypted wallet");
} catch (IllegalStateException e) {
// expected
}
assertNull("Wallet is not an unencrypted wallet", encryptedWallet.getKeyCrypter());
// Encrypt wallet.
encryptedWallet.encrypt(keyCrypter, aesKey);
assertEquals("Wallet is not an encrypted wallet", EncryptionType.ENCRYPTED_SCRYPT_AES, encryptedWallet.getEncryptionType());
// Try encrypting it again
try {
encryptedWallet.encrypt(keyCrypter, aesKey);
fail("Should not be able to encrypt an encrypted wallet");
} catch (IllegalStateException e) {
// expected
}
assertEquals("Wallet is not an encrypted wallet", EncryptionType.ENCRYPTED_SCRYPT_AES, encryptedWallet.getEncryptionType());
}
@Test(expected = KeyCrypterException.class)
public void addUnencryptedKeyToEncryptedWallet() throws Exception {
Wallet encryptedWallet = new Wallet(PARAMS);
encryptedWallet.encrypt(PASSWORD1);
ECKey key1 = new ECKey();
encryptedWallet.importKey(key1);
}
@Test(expected = KeyCrypterException.class)
public void addEncryptedKeyToUnencryptedWallet() throws Exception {
Wallet encryptedWallet = new Wallet(PARAMS);
encryptedWallet.encrypt(PASSWORD1);
KeyCrypter keyCrypter = encryptedWallet.getKeyCrypter();
ECKey key1 = new ECKey();
key1 = key1.encrypt(keyCrypter, keyCrypter.deriveKey("PASSWORD!"));
wallet.importKey(key1);
}
@Test(expected = KeyCrypterException.class)
public void mismatchedCrypter() throws Exception {
Wallet encryptedWallet = new Wallet(PARAMS);
encryptedWallet.encrypt(PASSWORD1);
KeyCrypter keyCrypter = encryptedWallet.getKeyCrypter();
KeyParameter aesKey = keyCrypter.deriveKey(PASSWORD1);
// Try added an ECKey that was encrypted with a differenct ScryptParameters (i.e. a non-homogenous key).
// This is not allowed as the ScryptParameters is stored at the Wallet level.
Protos.ScryptParameters.Builder scryptParametersBuilder = Protos.ScryptParameters.newBuilder()
.setSalt(ByteString.copyFrom(KeyCrypterScrypt.randomSalt()));
Protos.ScryptParameters scryptParameters = scryptParametersBuilder.build();
KeyCrypter keyCrypterDifferent = new KeyCrypterScrypt(scryptParameters);
ECKey ecKeyDifferent = new ECKey();
ecKeyDifferent = ecKeyDifferent.encrypt(keyCrypterDifferent, aesKey);
encryptedWallet.importKey(ecKeyDifferent);
}
@Test
public void importAndEncrypt() throws InsufficientMoneyException {
Wallet encryptedWallet = new Wallet(PARAMS);
encryptedWallet.encrypt(PASSWORD1);
final ECKey key = new ECKey();
encryptedWallet.importKeysAndEncrypt(ImmutableList.of(key), PASSWORD1);
assertEquals(1, encryptedWallet.getImportedKeys().size());
assertEquals(key.getPubKeyPoint(), encryptedWallet.getImportedKeys().get(0).getPubKeyPoint());
sendMoneyToWallet(encryptedWallet, AbstractBlockChain.NewBlockType.BEST_CHAIN, Coin.COIN, key.toAddress(PARAMS));
assertEquals(Coin.COIN, encryptedWallet.getBalance());
SendRequest req = SendRequest.emptyWallet(OTHER_ADDRESS);
req.aesKey = checkNotNull(encryptedWallet.getKeyCrypter()).deriveKey(PASSWORD1);
encryptedWallet.sendCoinsOffline(req);
}
@Test
public void ageMattersDuringSelection() throws Exception {
// Test that we prefer older coins to newer coins when building spends. This reduces required fees and improves
// time to confirmation as the transaction will appear less spammy.
final int ITERATIONS = 10;
Transaction[] txns = new Transaction[ITERATIONS];
for (int i = 0; i < ITERATIONS; i++) {
txns[i] = sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, COIN);
}
// Check that we spend transactions in order of reception.
for (int i = 0; i < ITERATIONS; i++) {
Transaction spend = wallet.createSend(OTHER_ADDRESS, COIN);
assertEquals(spend.getInputs().size(), 1);
assertEquals("Failed on iteration " + i, spend.getInput(0).getOutpoint().getHash(), txns[i].getHash());
wallet.commitTx(spend);
}
}
@Test(expected = Wallet.ExceededMaxTransactionSize.class)
public void respectMaxStandardSize() throws Exception {
// Check that we won't create txns > 100kb. Average tx size is ~220 bytes so this would have to be enormous.
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, valueOf(100, 0));
Transaction tx = new Transaction(PARAMS);
byte[] bits = new byte[20];
new Random().nextBytes(bits);
Coin v = CENT;
// 3100 outputs to a random address.
for (int i = 0; i < 3100; i++) {
tx.addOutput(v, new Address(PARAMS, bits));
}
SendRequest req = SendRequest.forTx(tx);
wallet.completeTx(req);
}
@Test
public void opReturnOneOutputTest() throws Exception {
// Tests basic send of transaction with one output that doesn't transfer any value but just writes OP_RETURN.
receiveATransaction(wallet, myAddress);
Transaction tx = new Transaction(PARAMS);
Coin messagePrice = Coin.ZERO;
Script script = ScriptBuilder.createOpReturnScript("hello world!".getBytes());
tx.addOutput(messagePrice, script);
SendRequest request = SendRequest.forTx(tx);
request.ensureMinRequiredFee = true;
wallet.completeTx(request);
}
@Test
public void opReturnMaxBytes() throws Exception {
receiveATransaction(wallet, myAddress);
Transaction tx = new Transaction(PARAMS);
Script script = ScriptBuilder.createOpReturnScript(new byte[80]);
tx.addOutput(Coin.ZERO, script);
SendRequest request = SendRequest.forTx(tx);
request.ensureMinRequiredFee = true;
wallet.completeTx(request);
}
@Test
public void opReturnOneOutputWithValueTest() throws Exception {
// Tests basic send of transaction with one output that destroys coins and has an OP_RETURN.
receiveATransaction(wallet, myAddress);
Transaction tx = new Transaction(PARAMS);
Coin messagePrice = CENT;
Script script = ScriptBuilder.createOpReturnScript("hello world!".getBytes());
tx.addOutput(messagePrice, script);
SendRequest request = SendRequest.forTx(tx);
wallet.completeTx(request);
}
@Test
public void opReturnTwoOutputsTest() throws Exception {
// Tests sending transaction where one output transfers BTC, the other one writes OP_RETURN.
receiveATransaction(wallet, myAddress);
Transaction tx = new Transaction(PARAMS);
Coin messagePrice = Coin.ZERO;
Script script = ScriptBuilder.createOpReturnScript("hello world!".getBytes());
tx.addOutput(CENT, OTHER_ADDRESS);
tx.addOutput(messagePrice, script);
SendRequest request = SendRequest.forTx(tx);
wallet.completeTx(request);
}
@Test(expected = Wallet.MultipleOpReturnRequested.class)
public void twoOpReturnsPerTransactionTest() throws Exception {
// Tests sending transaction where there are 2 attempts to write OP_RETURN scripts - this should fail and throw MultipleOpReturnRequested.
receiveATransaction(wallet, myAddress);
Transaction tx = new Transaction(PARAMS);
Coin messagePrice = Coin.ZERO;
Script script1 = ScriptBuilder.createOpReturnScript("hello world 1!".getBytes());
Script script2 = ScriptBuilder.createOpReturnScript("hello world 2!".getBytes());
tx.addOutput(messagePrice, script1);
tx.addOutput(messagePrice, script2);
SendRequest request = SendRequest.forTx(tx);
request.ensureMinRequiredFee = true;
wallet.completeTx(request);
}
@Test(expected = Wallet.DustySendRequested.class)
public void sendDustTest() throws InsufficientMoneyException {
// Tests sending dust, should throw DustySendRequested.
Transaction tx = new Transaction(PARAMS);
tx.addOutput(Transaction.MIN_NONDUST_OUTPUT.subtract(SATOSHI), OTHER_ADDRESS);
SendRequest request = SendRequest.forTx(tx);
request.ensureMinRequiredFee = true;
wallet.completeTx(request);
}
@Test
public void sendMultipleCentsTest() throws Exception {
receiveATransactionAmount(wallet, myAddress, Coin.COIN);
Transaction tx = new Transaction(PARAMS);
Coin c = CENT.subtract(SATOSHI);
tx.addOutput(c, OTHER_ADDRESS);
tx.addOutput(c, OTHER_ADDRESS);
tx.addOutput(c, OTHER_ADDRESS);
tx.addOutput(c, OTHER_ADDRESS);
SendRequest request = SendRequest.forTx(tx);
wallet.completeTx(request);
}
@Test(expected = Wallet.DustySendRequested.class)
public void sendDustAndOpReturnWithoutValueTest() throws Exception {
// Tests sending dust and OP_RETURN without value, should throw DustySendRequested because sending sending dust is not allowed in any case.
receiveATransactionAmount(wallet, myAddress, Coin.COIN);
Transaction tx = new Transaction(PARAMS);
tx.addOutput(Coin.ZERO, ScriptBuilder.createOpReturnScript("hello world!".getBytes()));
tx.addOutput(Coin.SATOSHI, OTHER_ADDRESS);
SendRequest request = SendRequest.forTx(tx);
request.ensureMinRequiredFee = true;
wallet.completeTx(request);
}
@Test(expected = Wallet.DustySendRequested.class)
public void sendDustAndMessageWithValueTest() throws Exception {
// Tests sending dust and OP_RETURN with value, should throw DustySendRequested
receiveATransaction(wallet, myAddress);
Transaction tx = new Transaction(PARAMS);
tx.addOutput(Coin.CENT, ScriptBuilder.createOpReturnScript("hello world!".getBytes()));
tx.addOutput(Transaction.MIN_NONDUST_OUTPUT.subtract(SATOSHI), OTHER_ADDRESS);
SendRequest request = SendRequest.forTx(tx);
request.ensureMinRequiredFee = true;
wallet.completeTx(request);
}
@Test
public void sendRequestP2PKTest() {
ECKey key = new ECKey();
SendRequest req = SendRequest.to(PARAMS, key, SATOSHI.multiply(12));
assertArrayEquals(key.getPubKey(), req.tx.getOutputs().get(0).getScriptPubKey().getPubKey());
}
@Test
public void sendRequestP2PKHTest() {
SendRequest req = SendRequest.to(OTHER_ADDRESS, SATOSHI.multiply(12));
assertEquals(OTHER_ADDRESS, req.tx.getOutputs().get(0).getScriptPubKey().getToAddress(PARAMS));
}
@Test
public void feeSolverAndCoinSelectionTest_dustySendRequested() throws Exception {
// Generate a few outputs to us that are far too small to spend reasonably
Transaction tx1 = createFakeTx(PARAMS, SATOSHI, myAddress);
Transaction tx2 = createFakeTx(PARAMS, SATOSHI, myAddress);
assertNotEquals(tx1.getHash(), tx2.getHash());
Transaction tx3 = createFakeTx(PARAMS, SATOSHI.multiply(10), myAddress);
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, tx1, tx2, tx3);
// Not allowed to send dust.
try {
SendRequest request = SendRequest.to(OTHER_ADDRESS, SATOSHI);
request.ensureMinRequiredFee = true;
wallet.completeTx(request);
fail();
} catch (Wallet.DustySendRequested e) {
// Expected.
}
// Spend it all without fee enforcement
SendRequest req = SendRequest.to(OTHER_ADDRESS, SATOSHI.multiply(12));
assertNotNull(wallet.sendCoinsOffline(req));
assertEquals(ZERO, wallet.getBalance());
}
@Test
public void basicFeeSolverTests() throws Exception {
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, Coin.COIN);
// Simple test to make sure if we have an ouput < 0.01 we get a fee
SendRequest request1 = SendRequest.to(OTHER_ADDRESS, CENT.subtract(SATOSHI));
request1.ensureMinRequiredFee = true;
wallet.completeTx(request1);
Transaction spend1 = request1.tx;
assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, request1.tx.getFee());
assertEquals(2, spend1.getOutputs().size());
// ...but not more fee than what we request
SendRequest request3 = SendRequest.to(OTHER_ADDRESS, CENT.subtract(SATOSHI));
request3.feePerKb = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(SATOSHI);
request3.ensureMinRequiredFee = true;
wallet.completeTx(request3);
assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, request3.tx.getFee());
assertEquals(2, request3.tx.getOutputs().size());
// ...unless we need it
SendRequest request4 = SendRequest.to(OTHER_ADDRESS, CENT.subtract(SATOSHI));
request4.feePerKb = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.subtract(SATOSHI);
request4.ensureMinRequiredFee = true;
wallet.completeTx(request4);
assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, request4.tx.getFee());
assertEquals(2, request4.tx.getOutputs().size());
// If we would have a change output < 0.01, it should add the fee
SendRequest request5 = SendRequest.to(OTHER_ADDRESS, Coin.COIN.subtract(CENT.subtract(SATOSHI)));
request5.ensureMinRequiredFee = true;
wallet.completeTx(request5);
assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, request5.tx.getFee());
assertEquals(2, request5.tx.getOutputs().size());
// If change is 0.1-satoshi and we already have a 0.1-satoshi output, fee should be reference fee
SendRequest request7 = SendRequest.to(OTHER_ADDRESS, Coin.COIN.subtract(CENT.subtract(SATOSHI.multiply(2)).multiply(2)));
request7.ensureMinRequiredFee = true;
request7.tx.addOutput(CENT.subtract(SATOSHI), OTHER_ADDRESS);
wallet.completeTx(request7);
assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, request7.tx.getFee());
assertEquals(3, request7.tx.getOutputs().size());
// If we would have a change output == REFERENCE_DEFAULT_MIN_TX_FEE that would cause a fee, throw it away and make it fee
SendRequest request8 = SendRequest.to(OTHER_ADDRESS, COIN.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE));
request8.ensureMinRequiredFee = true;
wallet.completeTx(request8);
assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, request8.tx.getFee());
assertEquals(1, request8.tx.getOutputs().size());
// ...in fact, also add fee if we would get back less than MIN_NONDUST_OUTPUT
SendRequest request9 = SendRequest.to(OTHER_ADDRESS, COIN.subtract(
Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(Transaction.MIN_NONDUST_OUTPUT).subtract(SATOSHI)));
request9.ensureMinRequiredFee = true;
wallet.completeTx(request9);
assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(Transaction.MIN_NONDUST_OUTPUT).subtract(SATOSHI), request9.tx.getFee());
assertEquals(1, request9.tx.getOutputs().size());
// ...but if we get back any more than that, we should get a refund (but still pay fee)
SendRequest request10 = SendRequest.to(OTHER_ADDRESS, COIN.subtract(
Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(Transaction.MIN_NONDUST_OUTPUT)));
request10.ensureMinRequiredFee = true;
wallet.completeTx(request10);
assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, request10.tx.getFee());
assertEquals(2, request10.tx.getOutputs().size());
// ...of course fee should be min(request.fee, MIN_TX_FEE) so we should get MIN_TX_FEE.add(SATOSHI) here
SendRequest request11 = SendRequest.to(OTHER_ADDRESS, COIN.subtract(
Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(Transaction.MIN_NONDUST_OUTPUT).add(SATOSHI.multiply(2))));
request11.feePerKb = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(SATOSHI);
request11.ensureMinRequiredFee = true;
wallet.completeTx(request11);
assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, request11.tx.getFee());
assertEquals(2, request11.tx.getOutputs().size());
}
@Test
public void coinSelection_coinTimesDepth() throws Exception {
Transaction txCent = sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, CENT);
for (int i = 0; i < 197; i++)
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN);
Transaction txCoin = sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, COIN);
assertEquals(COIN.add(CENT), wallet.getBalance());
assertTrue(txCent.getOutput(0).isMine(wallet));
assertTrue(txCent.getOutput(0).isAvailableForSpending());
assertEquals(199, txCent.getConfidence().getDepthInBlocks());
assertTrue(txCoin.getOutput(0).isMine(wallet));
assertTrue(txCoin.getOutput(0).isAvailableForSpending());
assertEquals(1, txCoin.getConfidence().getDepthInBlocks());
// txCent has higher coin*depth than txCoin...
assertTrue(txCent.getOutput(0).getValue().multiply(txCent.getConfidence().getDepthInBlocks())
.isGreaterThan(txCoin.getOutput(0).getValue().multiply(txCoin.getConfidence().getDepthInBlocks())));
// ...so txCent should be selected
Transaction spend1 = wallet.createSend(OTHER_ADDRESS, CENT);
assertEquals(1, spend1.getInputs().size());
assertEquals(CENT, spend1.getInput(0).getValue());
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN);
assertTrue(txCent.getOutput(0).isMine(wallet));
assertTrue(txCent.getOutput(0).isAvailableForSpending());
assertEquals(200, txCent.getConfidence().getDepthInBlocks());
assertTrue(txCoin.getOutput(0).isMine(wallet));
assertTrue(txCoin.getOutput(0).isAvailableForSpending());
assertEquals(2, txCoin.getConfidence().getDepthInBlocks());
// Now txCent and txCoin have exactly the same coin*depth...
assertEquals(txCent.getOutput(0).getValue().multiply(txCent.getConfidence().getDepthInBlocks()),
txCoin.getOutput(0).getValue().multiply(txCoin.getConfidence().getDepthInBlocks()));
// ...so the larger txCoin should be selected
Transaction spend2 = wallet.createSend(OTHER_ADDRESS, COIN);
assertEquals(1, spend2.getInputs().size());
assertEquals(COIN, spend2.getInput(0).getValue());
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN);
assertTrue(txCent.getOutput(0).isMine(wallet));
assertTrue(txCent.getOutput(0).isAvailableForSpending());
assertEquals(201, txCent.getConfidence().getDepthInBlocks());
assertTrue(txCoin.getOutput(0).isMine(wallet));
assertTrue(txCoin.getOutput(0).isAvailableForSpending());
assertEquals(3, txCoin.getConfidence().getDepthInBlocks());
// Now txCent has lower coin*depth than txCoin...
assertTrue(txCent.getOutput(0).getValue().multiply(txCent.getConfidence().getDepthInBlocks())
.isLessThan(txCoin.getOutput(0).getValue().multiply(txCoin.getConfidence().getDepthInBlocks())));
// ...so txCoin should be selected
Transaction spend3 = wallet.createSend(OTHER_ADDRESS, COIN);
assertEquals(1, spend3.getInputs().size());
assertEquals(COIN, spend3.getInput(0).getValue());
}
@Test
public void feeSolverAndCoinSelectionTests2() throws Exception {
Transaction tx5 = sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, CENT);
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, COIN);
// Now test feePerKb
SendRequest request15 = SendRequest.to(OTHER_ADDRESS, CENT);
for (int i = 0; i < 29; i++)
request15.tx.addOutput(CENT, OTHER_ADDRESS);
assertTrue(request15.tx.unsafeBitcoinSerialize().length > 1000);
request15.feePerKb = Transaction.DEFAULT_TX_FEE;
request15.ensureMinRequiredFee = true;
wallet.completeTx(request15);
assertEquals(Coin.valueOf(60650), request15.tx.getFee());
Transaction spend15 = request15.tx;
// If a transaction is over 1kb, 2 satoshis should be added.
assertEquals(31, spend15.getOutputs().size());
// We optimize for priority, so the output selected should be the largest one
assertEquals(1, spend15.getInputs().size());
assertEquals(COIN, spend15.getInput(0).getValue());
// Test ensureMinRequiredFee
SendRequest request16 = SendRequest.to(OTHER_ADDRESS, CENT);
request16.feePerKb = ZERO;
request16.ensureMinRequiredFee = true;
for (int i = 0; i < 29; i++)
request16.tx.addOutput(CENT, OTHER_ADDRESS);
assertTrue(request16.tx.unsafeBitcoinSerialize().length > 1000);
wallet.completeTx(request16);
// Just the reference fee should be added if feePerKb == 0
assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, request16.tx.getFee());
Transaction spend16 = request16.tx;
assertEquals(31, spend16.getOutputs().size());
// We optimize for priority, so the output selected should be the largest one
assertEquals(1, spend16.getInputs().size());
assertEquals(COIN, spend16.getInput(0).getValue());
// Create a transaction whose max size could be up to 999 (if signatures were maximum size)
SendRequest request17 = SendRequest.to(OTHER_ADDRESS, CENT);
for (int i = 0; i < 22; i++)
request17.tx.addOutput(CENT, OTHER_ADDRESS);
request17.tx.addOutput(new TransactionOutput(PARAMS, request17.tx, CENT, new byte[15]));
request17.feePerKb = Transaction.DEFAULT_TX_FEE;
request17.ensureMinRequiredFee = true;
wallet.completeTx(request17);
assertEquals(Coin.valueOf(49950), request17.tx.getFee());
assertEquals(1, request17.tx.getInputs().size());
// Calculate its max length to make sure it is indeed 999
int theoreticalMaxLength17 = request17.tx.unsafeBitcoinSerialize().length + myKey.getPubKey().length + 75;
for (TransactionInput in : request17.tx.getInputs())
theoreticalMaxLength17 -= in.getScriptBytes().length;
assertEquals(999, theoreticalMaxLength17);
Transaction spend17 = request17.tx;
{
// Its actual size must be between 996 and 999 (inclusive) as signatures have a 3-byte size range (almost always)
final int length = spend17.unsafeBitcoinSerialize().length;
assertTrue(Integer.toString(length), length >= 996 && length <= 999);
}
// Now check that it got a fee of 1 since its max size is 999 (1kb).
assertEquals(25, spend17.getOutputs().size());
// We optimize for priority, so the output selected should be the largest one
assertEquals(1, spend17.getInputs().size());
assertEquals(COIN, spend17.getInput(0).getValue());
// Create a transaction who's max size could be up to 1001 (if signatures were maximum size)
SendRequest request18 = SendRequest.to(OTHER_ADDRESS, CENT);
for (int i = 0; i < 22; i++)
request18.tx.addOutput(CENT, OTHER_ADDRESS);
request18.tx.addOutput(new TransactionOutput(PARAMS, request18.tx, CENT, new byte[17]));
request18.feePerKb = Transaction.DEFAULT_TX_FEE;
request18.ensureMinRequiredFee = true;
wallet.completeTx(request18);
assertEquals(Coin.valueOf(50050), request18.tx.getFee());
assertEquals(1, request18.tx.getInputs().size());
// Calculate its max length to make sure it is indeed 1001
Transaction spend18 = request18.tx;
int theoreticalMaxLength18 = spend18.unsafeBitcoinSerialize().length + myKey.getPubKey().length + 75;
for (TransactionInput in : spend18.getInputs())
theoreticalMaxLength18 -= in.getScriptBytes().length;
assertEquals(1001, theoreticalMaxLength18);
// Its actual size must be between 998 and 1000 (inclusive) as signatures have a 3-byte size range (almost always)
assertTrue(spend18.unsafeBitcoinSerialize().length >= 998);
assertTrue(spend18.unsafeBitcoinSerialize().length <= 1001);
// Now check that it did get a fee since its max size is 1000
assertEquals(25, spend18.getOutputs().size());
// We optimize for priority, so the output selected should be the largest one
assertEquals(1, spend18.getInputs().size());
assertEquals(COIN, spend18.getInput(0).getValue());
// Now create a transaction that will spend COIN + fee, which makes it require both inputs
assertEquals(wallet.getBalance(), CENT.add(COIN));
SendRequest request19 = SendRequest.to(OTHER_ADDRESS, CENT);
request19.feePerKb = ZERO;
for (int i = 0; i < 99; i++)
request19.tx.addOutput(CENT, OTHER_ADDRESS);
// If we send now, we should only have to spend our COIN
wallet.completeTx(request19);
assertEquals(Coin.ZERO, request19.tx.getFee());
assertEquals(1, request19.tx.getInputs().size());
assertEquals(100, request19.tx.getOutputs().size());
// Now reset request19 and give it a fee per kb
request19.tx.clearInputs();
request19 = SendRequest.forTx(request19.tx);
request19.feePerKb = Transaction.DEFAULT_TX_FEE;
request19.shuffleOutputs = false;
wallet.completeTx(request19);
assertEquals(Coin.valueOf(187100), request19.tx.getFee());
assertEquals(2, request19.tx.getInputs().size());
assertEquals(COIN, request19.tx.getInput(0).getValue());
assertEquals(CENT, request19.tx.getInput(1).getValue());
// Create another transaction that will spend COIN + fee, which makes it require both inputs
SendRequest request20 = SendRequest.to(OTHER_ADDRESS, CENT);
request20.feePerKb = ZERO;
for (int i = 0; i < 99; i++)
request20.tx.addOutput(CENT, OTHER_ADDRESS);
// If we send now, we shouldn't have a fee and should only have to spend our COIN
wallet.completeTx(request20);
assertEquals(ZERO, request20.tx.getFee());
assertEquals(1, request20.tx.getInputs().size());
assertEquals(100, request20.tx.getOutputs().size());
// Now reset request19 and give it a fee per kb
request20.tx.clearInputs();
request20 = SendRequest.forTx(request20.tx);
request20.feePerKb = Transaction.DEFAULT_TX_FEE;
wallet.completeTx(request20);
// 4kb tx.
assertEquals(Coin.valueOf(187100), request20.tx.getFee());
assertEquals(2, request20.tx.getInputs().size());
assertEquals(COIN, request20.tx.getInput(0).getValue());
assertEquals(CENT, request20.tx.getInput(1).getValue());
// 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(OTHER_ADDRESS, CENT);
request21.feePerKb = ZERO;
request21.ensureMinRequiredFee = true;
for (int i = 0; i < 99; i++)
request21.tx.addOutput(CENT, OTHER_ADDRESS);
request21.tx.addOutput(CENT.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), OTHER_ADDRESS);
// If we send without a feePerKb, we should still require REFERENCE_DEFAULT_MIN_TX_FEE because we have an output < 0.01
wallet.completeTx(request21);
assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, request21.tx.getFee());
assertEquals(2, request21.tx.getInputs().size());
assertEquals(COIN, request21.tx.getInput(0).getValue());
assertEquals(CENT, request21.tx.getInput(1).getValue());
// Test feePerKb when we aren't using ensureMinRequiredFee
SendRequest request25 = SendRequest.to(OTHER_ADDRESS, CENT);
request25.feePerKb = ZERO;
for (int i = 0; i < 70; i++)
request25.tx.addOutput(CENT, OTHER_ADDRESS);
// If we send now, we shouldn't need a fee and should only have to spend our COIN
wallet.completeTx(request25);
assertEquals(ZERO, request25.tx.getFee());
assertEquals(1, request25.tx.getInputs().size());
assertEquals(72, request25.tx.getOutputs().size());
// Now reset request25 and give it a fee per kb
request25.tx.clearInputs();
request25 = SendRequest.forTx(request25.tx);
request25.feePerKb = Transaction.DEFAULT_TX_FEE;
request25.shuffleOutputs = false;
wallet.completeTx(request25);
assertEquals(Coin.valueOf(139500), request25.tx.getFee());
assertEquals(2, request25.tx.getInputs().size());
assertEquals(COIN, request25.tx.getInput(0).getValue());
assertEquals(CENT, request25.tx.getInput(1).getValue());
// Spend our CENT output.
Transaction spendTx5 = new Transaction(PARAMS);
spendTx5.addOutput(CENT, OTHER_ADDRESS);
spendTx5.addInput(tx5.getOutput(0));
wallet.signTransaction(SendRequest.forTx(spendTx5));
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, spendTx5);
assertEquals(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(OTHER_ADDRESS, CENT);
for (int i = 0; i < 98; i++)
request26.tx.addOutput(CENT, OTHER_ADDRESS);
request26.tx.addOutput(CENT.subtract(
Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(Transaction.MIN_NONDUST_OUTPUT).subtract(SATOSHI)),
OTHER_ADDRESS);
assertTrue(request26.tx.unsafeBitcoinSerialize().length > 1000);
request26.feePerKb = SATOSHI;
request26.ensureMinRequiredFee = true;
wallet.completeTx(request26);
assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(Transaction.MIN_NONDUST_OUTPUT).subtract(SATOSHI),
request26.tx.getFee());
Transaction spend26 = request26.tx;
// If a transaction is over 1kb, the set fee should be added
assertEquals(100, spend26.getOutputs().size());
// We optimize for priority, so the output selected should be the largest one
assertEquals(1, spend26.getInputs().size());
assertEquals(COIN, spend26.getInput(0).getValue());
}
@Test
@Ignore("disabled for now as this test is not maintainable")
public void basicCategoryStepTest() throws Exception {
// Creates spends that step through the possible fee solver categories
// Generate a ton of small outputs
StoredBlock block = new StoredBlock(makeSolvedTestBlock(blockStore, OTHER_ADDRESS), BigInteger.ONE, 1);
int i = 0;
Coin tenThousand = Coin.valueOf(10000);
while (i <= 100) {
Transaction tx = createFakeTxWithChangeAddress(PARAMS, tenThousand, myAddress, OTHER_ADDRESS);
tx.getInput(0).setSequenceNumber(i++); // Keep every transaction unique
wallet.receiveFromBlock(tx, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, i);
}
Coin balance = wallet.getBalance();
// 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(OTHER_ADDRESS, balance.subtract(SATOSHI));
request1.ensureMinRequiredFee = true;
wallet.completeTx(request1);
assertEquals(SATOSHI, request1.tx.getFee());
assertEquals(request1.tx.getInputs().size(), i); // We should have spent all inputs
// Give us one more input...
Transaction tx1 = createFakeTxWithChangeAddress(PARAMS, tenThousand, myAddress, OTHER_ADDRESS);
tx1.getInput(0).setSequenceNumber(i++); // Keep every transaction unique
wallet.receiveFromBlock(tx1, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, i);
// ... and create a spend that will throw away change (category 3 type 1 in which the change causes dust output)
SendRequest request2 = SendRequest.to(OTHER_ADDRESS, balance.subtract(SATOSHI));
request2.ensureMinRequiredFee = true;
wallet.completeTx(request2);
assertEquals(SATOSHI, request2.tx.getFee());
assertEquals(request2.tx.getInputs().size(), i - 1); // We should have spent all inputs - 1
// Give us one more input...
Transaction tx2 = createFakeTxWithChangeAddress(PARAMS, tenThousand, myAddress, OTHER_ADDRESS);
tx2.getInput(0).setSequenceNumber(i++); // Keep every transaction unique
wallet.receiveFromBlock(tx2, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, i);
// ... 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(OTHER_ADDRESS, CENT.add(tenThousand).subtract(SATOSHI));
request3.ensureMinRequiredFee = true;
wallet.completeTx(request3);
assertEquals(SATOSHI, request3.tx.getFee());
assertEquals(request3.tx.getInputs().size(), i - 2); // We should have spent all inputs - 2
//
SendRequest request4 = SendRequest.to(OTHER_ADDRESS, balance.subtract(SATOSHI));
request4.feePerKb = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.divide(request3.tx.unsafeBitcoinSerialize().length);
request4.ensureMinRequiredFee = true;
wallet.completeTx(request4);
assertEquals(SATOSHI, request4.tx.getFee());
assertEquals(request4.tx.getInputs().size(), i - 2); // We should have spent all inputs - 2
// Give us a few more inputs...
while (wallet.getBalance().compareTo(CENT.multiply(2)) < 0) {
Transaction tx3 = createFakeTxWithChangeAddress(PARAMS, tenThousand, myAddress, OTHER_ADDRESS);
tx3.getInput(0).setSequenceNumber(i++); // Keep every transaction unique
wallet.receiveFromBlock(tx3, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, i);
}
// ...that is just slightly less than is needed for category 1
SendRequest request5 = SendRequest.to(OTHER_ADDRESS, CENT.add(tenThousand).subtract(SATOSHI));
request5.ensureMinRequiredFee = true;
wallet.completeTx(request5);
assertEquals(SATOSHI, request5.tx.getFee());
assertEquals(1, request5.tx.getOutputs().size()); // We should have no change output
// Give us one more input...
Transaction tx4 = createFakeTxWithChangeAddress(PARAMS, tenThousand, myAddress, OTHER_ADDRESS);
tx4.getInput(0).setSequenceNumber(i); // Keep every transaction unique
wallet.receiveFromBlock(tx4, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, i);
// ... that puts us in category 1 (no fee!)
SendRequest request6 = SendRequest.to(OTHER_ADDRESS, CENT.add(tenThousand).subtract(SATOSHI));
request6.ensureMinRequiredFee = true;
wallet.completeTx(request6);
assertEquals(ZERO, request6.tx.getFee());
assertEquals(2, request6.tx.getOutputs().size()); // We should have a change output
}
@Test
public void testCategory2WithChange() throws Exception {
// Specifically target case 2 with significant change
// Generate a ton of small outputs
StoredBlock block = new StoredBlock(makeSolvedTestBlock(blockStore, OTHER_ADDRESS), BigInteger.ONE, 1);
int i = 0;
while (i <= CENT.divide(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(10))) {
Transaction tx = createFakeTxWithChangeAddress(PARAMS, Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(10), myAddress, OTHER_ADDRESS);
tx.getInput(0).setSequenceNumber(i++); // Keep every transaction unique
wallet.receiveFromBlock(tx, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, i);
}
// The selector will choose 2 with MIN_TX_FEE fee
SendRequest request1 = SendRequest.to(OTHER_ADDRESS, CENT.add(SATOSHI));
request1.ensureMinRequiredFee = true;
wallet.completeTx(request1);
assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, request1.tx.getFee());
assertEquals(request1.tx.getInputs().size(), i); // We should have spent all inputs
assertEquals(2, request1.tx.getOutputs().size()); // and gotten change back
}
@Test
public void transactionGetFeeTest() throws Exception {
// Prepare wallet to spend
StoredBlock block = new StoredBlock(makeSolvedTestBlock(blockStore, OTHER_ADDRESS), BigInteger.ONE, 1);
Transaction tx = createFakeTx(PARAMS, COIN, myAddress);
wallet.receiveFromBlock(tx, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, 0);
// Create a transaction
SendRequest request = SendRequest.to(OTHER_ADDRESS, CENT);
request.feePerKb = Transaction.DEFAULT_TX_FEE;
wallet.completeTx(request);
assertEquals(Coin.valueOf(11350), request.tx.getFee());
}
@Test
public void lowerThanDefaultFee() throws InsufficientMoneyException {
int feeFactor = 10;
Coin fee = Transaction.DEFAULT_TX_FEE.divide(feeFactor);
receiveATransactionAmount(wallet, myAddress, Coin.COIN);
SendRequest req = SendRequest.to(myAddress, Coin.CENT);
req.feePerKb = fee;
wallet.completeTx(req);
assertEquals(Coin.valueOf(11350).divide(feeFactor), req.tx.getFee());
wallet.commitTx(req.tx);
SendRequest emptyReq = SendRequest.emptyWallet(myAddress);
emptyReq.feePerKb = fee;
emptyReq.ensureMinRequiredFee = true;
emptyReq.emptyWallet = true;
emptyReq.coinSelector = AllowUnconfirmedCoinSelector.get();
wallet.completeTx(emptyReq);
assertEquals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE, emptyReq.tx.getFee());
wallet.commitTx(emptyReq.tx);
}
@Test
public void higherThanDefaultFee() throws InsufficientMoneyException {
int feeFactor = 10;
Coin fee = Transaction.DEFAULT_TX_FEE.multiply(feeFactor);
receiveATransactionAmount(wallet, myAddress, Coin.COIN);
SendRequest req = SendRequest.to(myAddress, Coin.CENT);
req.feePerKb = fee;
wallet.completeTx(req);
assertEquals(Coin.valueOf(11350).multiply(feeFactor), req.tx.getFee());
wallet.commitTx(req.tx);
SendRequest emptyReq = SendRequest.emptyWallet(myAddress);
emptyReq.feePerKb = fee;
emptyReq.emptyWallet = true;
emptyReq.coinSelector = AllowUnconfirmedCoinSelector.get();
wallet.completeTx(emptyReq);
assertEquals(Coin.valueOf(171000), emptyReq.tx.getFee());
wallet.commitTx(emptyReq.tx);
}
@Test
public void testCompleteTxWithExistingInputs() throws Exception {
// Tests calling completeTx with a SendRequest that already has a few inputs in it
// Generate a few outputs to us
StoredBlock block = new StoredBlock(makeSolvedTestBlock(blockStore, OTHER_ADDRESS), BigInteger.ONE, 1);
Transaction tx1 = createFakeTx(PARAMS, COIN, myAddress);
wallet.receiveFromBlock(tx1, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, 0);
Transaction tx2 = createFakeTx(PARAMS, COIN, myAddress);
assertNotEquals(tx1.getHash(), tx2.getHash());
wallet.receiveFromBlock(tx2, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, 1);
Transaction tx3 = createFakeTx(PARAMS, CENT, myAddress);
wallet.receiveFromBlock(tx3, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, 2);
SendRequest request1 = SendRequest.to(OTHER_ADDRESS, CENT);
// If we just complete as-is, we will use one of the COIN outputs to get higher priority,
// resulting in a change output
request1.shuffleOutputs = false;
wallet.completeTx(request1);
assertEquals(1, request1.tx.getInputs().size());
assertEquals(2, request1.tx.getOutputs().size());
assertEquals(CENT, request1.tx.getOutput(0).getValue());
assertEquals(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(OTHER_ADDRESS, CENT);
request2.tx.addInput(tx3.getOutput(0));
// Now completeTx will result in one input, one output
wallet.completeTx(request2);
assertEquals(1, request2.tx.getInputs().size());
assertEquals(1, request2.tx.getOutputs().size());
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());
// However, if there is no connected output, we will grab a COIN output anyway and add the CENT to fee
SendRequest request3 = SendRequest.to(OTHER_ADDRESS, 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
request3.shuffleOutputs = false;
wallet.completeTx(request3);
assertEquals(2, request3.tx.getInputs().size());
assertEquals(2, request3.tx.getOutputs().size());
assertEquals(CENT, request3.tx.getOutput(0).getValue());
assertEquals(COIN.subtract(CENT), request3.tx.getOutput(1).getValue());
SendRequest request4 = SendRequest.to(OTHER_ADDRESS, CENT);
request4.tx.addInput(tx3.getOutput(0));
// Now if we manually sign it, completeTx will not replace our signature
wallet.signTransaction(request4);
byte[] scriptSig = request4.tx.getInput(0).getScriptBytes();
wallet.completeTx(request4);
assertEquals(1, request4.tx.getInputs().size());
assertEquals(1, request4.tx.getOutputs().size());
assertEquals(CENT, request4.tx.getOutput(0).getValue());
assertArrayEquals(scriptSig, request4.tx.getInput(0).getScriptBytes());
}
// There is a test for spending a coinbase transaction as it matures in BlockChainTest#coinbaseTransactionAvailability
// Support for offline spending is tested in PeerGroupTest
@Test
public void exceptionsDoNotBlockAllListeners() throws Exception {
// Check that if a wallet listener throws an exception, the others still run.
wallet.addCoinsReceivedEventListener(new WalletCoinsReceivedEventListener() {
@Override
public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
log.info("onCoinsReceived 1");
throw new RuntimeException("barf");
}
});
final AtomicInteger flag = new AtomicInteger();
wallet.addCoinsReceivedEventListener(new WalletCoinsReceivedEventListener() {
@Override
public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) {
log.info("onCoinsReceived 2");
flag.incrementAndGet();
}
});
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, COIN);
log.info("Wait for user thread");
Threading.waitForUserCode();
log.info("... and test flag.");
assertEquals(1, flag.get());
}
@Test
public void testEmptyRandomWallet() throws Exception {
// Add a random set of outputs
StoredBlock block = new StoredBlock(makeSolvedTestBlock(blockStore, OTHER_ADDRESS), BigInteger.ONE, 1);
Random rng = new Random();
for (int i = 0; i < rng.nextInt(100) + 1; i++) {
Transaction tx = createFakeTx(PARAMS, Coin.valueOf(rng.nextInt((int) COIN.value)), myAddress);
wallet.receiveFromBlock(tx, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, i);
}
SendRequest request = SendRequest.emptyWallet(OTHER_ADDRESS);
wallet.completeTx(request);
wallet.commitTx(request.tx);
assertEquals(ZERO, wallet.getBalance());
}
@Test
public void testEmptyWallet() throws Exception {
// Add exactly 0.01
StoredBlock block = new StoredBlock(makeSolvedTestBlock(blockStore, OTHER_ADDRESS), BigInteger.ONE, 1);
Transaction tx = createFakeTx(PARAMS, CENT, myAddress);
wallet.receiveFromBlock(tx, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, 0);
SendRequest request = SendRequest.emptyWallet(OTHER_ADDRESS);
wallet.completeTx(request);
assertEquals(ZERO, request.tx.getFee());
wallet.commitTx(request.tx);
assertEquals(ZERO, wallet.getBalance());
assertEquals(CENT, request.tx.getOutput(0).getValue());
// Add 1 confirmed cent and 1 unconfirmed cent. Verify only one cent is emptied because of the coin selection
// policies that are in use by default.
block = new StoredBlock(makeSolvedTestBlock(blockStore, OTHER_ADDRESS), BigInteger.ONE, 2);
tx = createFakeTx(PARAMS, CENT, myAddress);
wallet.receiveFromBlock(tx, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, 0);
tx = createFakeTx(PARAMS, CENT, myAddress);
wallet.receivePending(tx, null);
request = SendRequest.emptyWallet(OTHER_ADDRESS);
wallet.completeTx(request);
assertEquals(ZERO, request.tx.getFee());
wallet.commitTx(request.tx);
assertEquals(ZERO, wallet.getBalance());
assertEquals(CENT, request.tx.getOutput(0).getValue());
// Add an unsendable value
block = new StoredBlock(block.getHeader().createNextBlock(OTHER_ADDRESS), BigInteger.ONE, 3);
Coin outputValue = Transaction.MIN_NONDUST_OUTPUT.subtract(SATOSHI);
tx = createFakeTx(PARAMS, outputValue, myAddress);
wallet.receiveFromBlock(tx, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, 0);
try {
request = SendRequest.emptyWallet(OTHER_ADDRESS);
assertEquals(ZERO, request.tx.getFee());
wallet.completeTx(request);
fail();
} catch (Wallet.CouldNotAdjustDownwards e) {}
}
@Test
public void childPaysForParent() throws Exception {
// Receive confirmed balance to play with.
Transaction toMe = createFakeTxWithoutChangeAddress(PARAMS, COIN, myAddress);
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, toMe);
assertEquals(Coin.COIN, wallet.getBalance(BalanceType.ESTIMATED_SPENDABLE));
assertEquals(Coin.COIN, wallet.getBalance(BalanceType.AVAILABLE_SPENDABLE));
// Receive unconfirmed coin without fee.
Transaction toMeWithoutFee = createFakeTxWithoutChangeAddress(PARAMS, COIN, myAddress);
wallet.receivePending(toMeWithoutFee, null);
assertEquals(Coin.COIN.multiply(2), wallet.getBalance(BalanceType.ESTIMATED_SPENDABLE));
assertEquals(Coin.COIN, wallet.getBalance(BalanceType.AVAILABLE_SPENDABLE));
// Craft a child-pays-for-parent transaction.
final Coin feeRaise = MILLICOIN;
final SendRequest sendRequest = SendRequest.childPaysForParent(wallet, toMeWithoutFee, feeRaise);
wallet.signTransaction(sendRequest);
wallet.commitTx(sendRequest.tx);
assertEquals(Transaction.Purpose.RAISE_FEE, sendRequest.tx.getPurpose());
assertEquals(Coin.COIN.multiply(2).subtract(feeRaise), wallet.getBalance(BalanceType.ESTIMATED_SPENDABLE));
assertEquals(Coin.COIN, wallet.getBalance(BalanceType.AVAILABLE_SPENDABLE));
}
@Test
public void keyRotationRandom() throws Exception {
Utils.setMockClock();
// Start with an empty wallet (no HD chain).
wallet = new Wallet(PARAMS);
// Watch out for wallet-initiated broadcasts.
MockTransactionBroadcaster broadcaster = new MockTransactionBroadcaster(wallet);
// Send three cents to two different random keys, then add a key and mark the initial keys as compromised.
ECKey key1 = new ECKey();
key1.setCreationTimeSeconds(Utils.currentTimeSeconds() - (86400 * 2));
ECKey key2 = new ECKey();
key2.setCreationTimeSeconds(Utils.currentTimeSeconds() - 86400);
wallet.importKey(key1);
wallet.importKey(key2);
sendMoneyToWallet(wallet, AbstractBlockChain.NewBlockType.BEST_CHAIN, CENT, key1.toAddress(PARAMS));
sendMoneyToWallet(wallet, AbstractBlockChain.NewBlockType.BEST_CHAIN, CENT, key2.toAddress(PARAMS));
sendMoneyToWallet(wallet, AbstractBlockChain.NewBlockType.BEST_CHAIN, CENT, key2.toAddress(PARAMS));
Date compromiseTime = Utils.now();
assertEquals(0, broadcaster.size());
assertFalse(wallet.isKeyRotating(key1));
// We got compromised!
Utils.rollMockClock(1);
wallet.setKeyRotationTime(compromiseTime);
assertTrue(wallet.isKeyRotating(key1));
wallet.doMaintenance(null, true);
Transaction tx = broadcaster.waitForTransactionAndSucceed();
final Coin THREE_CENTS = CENT.add(CENT).add(CENT);
assertEquals(Coin.valueOf(24550), tx.getFee());
assertEquals(THREE_CENTS, tx.getValueSentFromMe(wallet));
assertEquals(THREE_CENTS.subtract(tx.getFee()), tx.getValueSentToMe(wallet));
// TX sends to one of our addresses (for now we ignore married wallets).
final Address toAddress = tx.getOutput(0).getScriptPubKey().getToAddress(PARAMS);
final ECKey rotatingToKey = wallet.findKeyFromPubHash(toAddress.getHash160());
assertNotNull(rotatingToKey);
assertFalse(wallet.isKeyRotating(rotatingToKey));
assertEquals(3, tx.getInputs().size());
// It confirms.
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, tx);
// Now receive some more money to the newly derived address via a new block and check that nothing happens.
sendMoneyToWallet(wallet, AbstractBlockChain.NewBlockType.BEST_CHAIN, CENT, toAddress);
assertTrue(wallet.doMaintenance(null, true).get().isEmpty());
assertEquals(0, broadcaster.size());
// Receive money via a new block on key1 and ensure it shows up as a maintenance task.
sendMoneyToWallet(wallet, AbstractBlockChain.NewBlockType.BEST_CHAIN, CENT, key1.toAddress(PARAMS));
wallet.doMaintenance(null, true);
tx = broadcaster.waitForTransactionAndSucceed();
assertNotNull(wallet.findKeyFromPubHash(tx.getOutput(0).getScriptPubKey().getPubKeyHash()));
log.info("Unexpected thing: {}", tx);
assertEquals(Coin.valueOf(9650), tx.getFee());
assertEquals(1, tx.getInputs().size());
assertEquals(1, tx.getOutputs().size());
assertEquals(CENT, tx.getValueSentFromMe(wallet));
assertEquals(CENT.subtract(tx.getFee()), tx.getValueSentToMe(wallet));
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.
wallet = roundTrip(wallet);
tx = wallet.getTransaction(tx.getHash());
checkNotNull(tx);
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.
wallet.sendCoins(broadcaster, OTHER_ADDRESS, wallet.getBalance());
tx = broadcaster.waitForTransaction();
assertArrayEquals(OTHER_ADDRESS.getHash160(), tx.getOutput(0).getScriptPubKey().getPubKeyHash());
}
private Wallet roundTrip(Wallet wallet) throws UnreadableWalletException {
Protos.Wallet protos = new WalletProtobufSerializer().walletToProto(wallet);
return new WalletProtobufSerializer().readWallet(PARAMS, null, protos);
}
@Test
public void keyRotationHD() throws Exception {
// Test that if we rotate an HD chain, a new one is created and all arrivals on the old keys are moved.
Utils.setMockClock();
wallet = new Wallet(PARAMS);
ECKey key1 = wallet.freshReceiveKey();
ECKey key2 = wallet.freshReceiveKey();
sendMoneyToWallet(wallet, AbstractBlockChain.NewBlockType.BEST_CHAIN, CENT, key1.toAddress(PARAMS));
sendMoneyToWallet(wallet, AbstractBlockChain.NewBlockType.BEST_CHAIN, CENT, key2.toAddress(PARAMS));
DeterministicKey watchKey1 = wallet.getWatchingKey();
// A day later, we get compromised.
Utils.rollMockClock(86400);
wallet.setKeyRotationTime(Utils.currentTimeSeconds());
List<Transaction> txns = wallet.doMaintenance(null, false).get();
assertEquals(1, txns.size());
DeterministicKey watchKey2 = wallet.getWatchingKey();
assertNotEquals(watchKey1, watchKey2);
}
@SuppressWarnings("ConstantConditions")
@Test
public void keyRotationHD2() throws Exception {
// Check we handle the following scenario: a weak random key is created, then some good random keys are created
// but the weakness of the first isn't known yet. The wallet is upgraded to HD based on the weak key. Later, we
// find out about the weakness and set the rotation time to after the bad key's creation date. A new HD chain
// should be created based on the oldest known good key and the old chain + bad random key should rotate to it.
// We fix the private keys just to make the test deterministic (last byte differs).
Utils.setMockClock();
ECKey badKey = ECKey.fromPrivate(Utils.HEX.decode("00905b93f990267f4104f316261fc10f9f983551f9ef160854f40102eb71cffdbb"));
badKey.setCreationTimeSeconds(Utils.currentTimeSeconds());
Utils.rollMockClock(86400);
ECKey goodKey = ECKey.fromPrivate(Utils.HEX.decode("00905b93f990267f4104f316261fc10f9f983551f9ef160854f40102eb71cffdcc"));
goodKey.setCreationTimeSeconds(Utils.currentTimeSeconds());
// Do an upgrade based on the bad key.
final AtomicReference<List<DeterministicKeyChain>> fChains = new AtomicReference<List<DeterministicKeyChain>>();
KeyChainGroup kcg = new KeyChainGroup(PARAMS) {
{
fChains.set(chains);
}
};
kcg.importKeys(badKey, goodKey);
Utils.rollMockClock(86400);
wallet = new Wallet(PARAMS, kcg); // This avoids the automatic HD initialisation
assertTrue(fChains.get().isEmpty());
wallet.upgradeToDeterministic(null);
DeterministicKey badWatchingKey = wallet.getWatchingKey();
assertEquals(badKey.getCreationTimeSeconds(), badWatchingKey.getCreationTimeSeconds());
sendMoneyToWallet(wallet, AbstractBlockChain.NewBlockType.BEST_CHAIN, CENT, badWatchingKey.toAddress(PARAMS));
// Now we set the rotation time to the time we started making good keys. This should create a new HD chain.
wallet.setKeyRotationTime(goodKey.getCreationTimeSeconds());
List<Transaction> txns = wallet.doMaintenance(null, false).get();
assertEquals(1, txns.size());
Address output = txns.get(0).getOutput(0).getAddressFromP2PKHScript(PARAMS);
ECKey usedKey = wallet.findKeyFromPubHash(output.getHash160());
assertEquals(goodKey.getCreationTimeSeconds(), usedKey.getCreationTimeSeconds());
assertEquals(goodKey.getCreationTimeSeconds(), wallet.freshReceiveKey().getCreationTimeSeconds());
assertEquals("mrM3TpCnav5YQuVA1xLercCGJH4DXujMtv", usedKey.toAddress(PARAMS).toString());
DeterministicKeyChain c = fChains.get().get(1);
assertEquals(c.getEarliestKeyCreationTime(), goodKey.getCreationTimeSeconds());
assertEquals(2, fChains.get().size());
// Commit the maint txns.
wallet.commitTx(txns.get(0));
// Check next maintenance does nothing.
assertTrue(wallet.doMaintenance(null, false).get().isEmpty());
assertEquals(c, fChains.get().get(1));
assertEquals(2, fChains.get().size());
}
@Test(expected = IllegalArgumentException.class)
public void importOfHDKeyForbidden() throws Exception {
wallet.importKey(wallet.freshReceiveKey());
}
//@Test //- this test is slow, disable for now.
public void fragmentedReKeying() throws Exception {
// Send lots of small coins and check the fee is correct.
ECKey key = wallet.freshReceiveKey();
Address address = key.toAddress(PARAMS);
Utils.setMockClock();
Utils.rollMockClock(86400);
for (int i = 0; i < 800; i++) {
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, CENT, address);
}
MockTransactionBroadcaster broadcaster = new MockTransactionBroadcaster(wallet);
Date compromise = Utils.now();
Utils.rollMockClock(86400);
wallet.freshReceiveKey();
wallet.setKeyRotationTime(compromise);
wallet.doMaintenance(null, true);
Transaction tx = broadcaster.waitForTransactionAndSucceed();
final Coin valueSentToMe = tx.getValueSentToMe(wallet);
Coin fee = tx.getValueSentFromMe(wallet).subtract(valueSentToMe);
assertEquals(Coin.valueOf(900000), fee);
assertEquals(KeyTimeCoinSelector.MAX_SIMULTANEOUS_INPUTS, tx.getInputs().size());
assertEquals(Coin.valueOf(599100000), valueSentToMe);
tx = broadcaster.waitForTransaction();
assertNotNull(tx);
assertEquals(200, tx.getInputs().size());
}
@Test
public void completeTxPartiallySignedWithDummySigs() throws Exception {
byte[] dummySig = TransactionSignature.dummy().encodeToBitcoin();
completeTxPartiallySigned(Wallet.MissingSigsMode.USE_DUMMY_SIG, dummySig);
}
@Test
public void completeTxPartiallySignedWithEmptySig() throws Exception {
byte[] emptySig = {};
completeTxPartiallySigned(Wallet.MissingSigsMode.USE_OP_ZERO, emptySig);
}
@Test (expected = ECKey.MissingPrivateKeyException.class)
public void completeTxPartiallySignedThrows() throws Exception {
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, CENT, wallet.freshReceiveKey());
SendRequest req = SendRequest.emptyWallet(OTHER_ADDRESS);
wallet.completeTx(req);
// Delete the sigs
for (TransactionInput input : req.tx.getInputs())
input.clearScriptBytes();
Wallet watching = Wallet.fromWatchingKey(PARAMS, wallet.getWatchingKey().dropParent().dropPrivateBytes());
watching.completeTx(SendRequest.forTx(req.tx));
}
@Test
public void completeTxPartiallySignedMarriedWithDummySigs() throws Exception {
byte[] dummySig = TransactionSignature.dummy().encodeToBitcoin();
completeTxPartiallySignedMarried(Wallet.MissingSigsMode.USE_DUMMY_SIG, dummySig);
}
@Test
public void completeTxPartiallySignedMarriedWithEmptySig() throws Exception {
byte[] emptySig = {};
completeTxPartiallySignedMarried(Wallet.MissingSigsMode.USE_OP_ZERO, emptySig);
}
@Test (expected = TransactionSigner.MissingSignatureException.class)
public void completeTxPartiallySignedMarriedThrows() throws Exception {
byte[] emptySig = {};
completeTxPartiallySignedMarried(Wallet.MissingSigsMode.THROW, emptySig);
}
@Test (expected = TransactionSigner.MissingSignatureException.class)
public void completeTxPartiallySignedMarriedThrowsByDefault() throws Exception {
createMarriedWallet(2, 2, false);
myAddress = wallet.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, COIN, myAddress);
SendRequest req = SendRequest.emptyWallet(OTHER_ADDRESS);
wallet.completeTx(req);
}
public void completeTxPartiallySignedMarried(Wallet.MissingSigsMode missSigMode, byte[] expectedSig) throws Exception {
// create married wallet without signer
createMarriedWallet(2, 2, false);
myAddress = wallet.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, COIN, myAddress);
SendRequest req = SendRequest.emptyWallet(OTHER_ADDRESS);
req.missingSigsMode = missSigMode;
wallet.completeTx(req);
TransactionInput input = req.tx.getInput(0);
boolean firstSigIsMissing = Arrays.equals(expectedSig, input.getScriptSig().getChunks().get(1).data);
boolean secondSigIsMissing = Arrays.equals(expectedSig, input.getScriptSig().getChunks().get(2).data);
assertTrue("Only one of the signatures should be missing/dummy", firstSigIsMissing ^ secondSigIsMissing);
int localSigIndex = firstSigIsMissing ? 2 : 1;
int length = input.getScriptSig().getChunks().get(localSigIndex).data.length;
assertTrue("Local sig should be present: " + length, length >= 70);
}
@SuppressWarnings("ConstantConditions")
public void completeTxPartiallySigned(Wallet.MissingSigsMode missSigMode, byte[] expectedSig) throws Exception {
// Check the wallet will write dummy scriptSigs for inputs that we have only pubkeys for without the privkey.
ECKey priv = new ECKey();
ECKey pub = ECKey.fromPublicOnly(priv.getPubKeyPoint());
wallet.importKey(pub);
ECKey priv2 = wallet.freshReceiveKey();
// Send three transactions, with one being an address type and the other being a raw CHECKSIG type pubkey only,
// and the final one being a key we do have. We expect the first two inputs to be dummy values and the last
// to be signed correctly.
Transaction t1 = sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, CENT, pub.toAddress(PARAMS));
Transaction t2 = sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, CENT, pub);
Transaction t3 = sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, CENT, priv2);
SendRequest req = SendRequest.emptyWallet(OTHER_ADDRESS);
req.missingSigsMode = missSigMode;
wallet.completeTx(req);
byte[] dummySig = TransactionSignature.dummy().encodeToBitcoin();
// Selected inputs can be in any order.
for (int i = 0; i < req.tx.getInputs().size(); i++) {
TransactionInput input = req.tx.getInput(i);
if (input.getConnectedOutput().getParentTransaction().equals(t1)) {
assertArrayEquals(expectedSig, input.getScriptSig().getChunks().get(0).data);
} else if (input.getConnectedOutput().getParentTransaction().equals(t2)) {
assertArrayEquals(expectedSig, input.getScriptSig().getChunks().get(0).data);
} else if (input.getConnectedOutput().getParentTransaction().equals(t3)) {
input.getScriptSig().correctlySpends(req.tx, i, t3.getOutput(0).getScriptPubKey());
}
}
assertTrue(TransactionSignature.isEncodingCanonical(dummySig));
}
@Test
public void riskAnalysis() throws Exception {
// Send a tx that is considered risky to the wallet, verify it doesn't show up in the balances.
final Transaction tx = createFakeTx(PARAMS, COIN, myAddress);
final AtomicBoolean bool = new AtomicBoolean();
wallet.setRiskAnalyzer(new RiskAnalysis.Analyzer() {
@Override
public RiskAnalysis create(Wallet wallet, Transaction wtx, List<Transaction> dependencies) {
RiskAnalysis.Result result = RiskAnalysis.Result.OK;
if (wtx.getHash().equals(tx.getHash()))
result = RiskAnalysis.Result.NON_STANDARD;
final RiskAnalysis.Result finalResult = result;
return new RiskAnalysis() {
@Override
public Result analyze() {
bool.set(true);
return finalResult;
}
};
}
});
assertTrue(wallet.isPendingTransactionRelevant(tx));
assertEquals(Coin.ZERO, wallet.getBalance());
assertEquals(Coin.ZERO, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
wallet.receivePending(tx, null);
assertEquals(Coin.ZERO, wallet.getBalance());
assertEquals(Coin.ZERO, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
assertTrue(bool.get());
// Confirm it in the same manner as how Bloom filtered blocks do. Verify it shows up.
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, tx);
assertEquals(COIN, wallet.getBalance());
}
@Test
public void transactionInBlockNotification() {
final Transaction tx = createFakeTx(PARAMS, COIN, myAddress);
StoredBlock block = createFakeBlock(blockStore, Block.BLOCK_HEIGHT_GENESIS, tx).storedBlock;
wallet.receivePending(tx, null);
boolean notification = wallet.notifyTransactionIsInBlock(tx.getHash(), block, AbstractBlockChain.NewBlockType.BEST_CHAIN, 1);
assertTrue(notification);
final Transaction tx2 = createFakeTx(PARAMS, COIN, OTHER_ADDRESS);
wallet.receivePending(tx2, null);
StoredBlock block2 = createFakeBlock(blockStore, Block.BLOCK_HEIGHT_GENESIS + 1, tx2).storedBlock;
boolean notification2 = wallet.notifyTransactionIsInBlock(tx2.getHash(), block2, AbstractBlockChain.NewBlockType.BEST_CHAIN, 1);
assertFalse(notification2);
}
@Test
public void duplicatedBlock() {
final Transaction tx = createFakeTx(PARAMS, COIN, myAddress);
StoredBlock block = createFakeBlock(blockStore, Block.BLOCK_HEIGHT_GENESIS, tx).storedBlock;
wallet.notifyNewBestBlock(block);
wallet.notifyNewBestBlock(block);
}
@Test
public void keyEvents() throws Exception {
// Check that we can register an event listener, generate some keys and the callbacks are invoked properly.
wallet = new Wallet(PARAMS);
final List<ECKey> keys = Lists.newLinkedList();
wallet.addKeyChainEventListener(Threading.SAME_THREAD, new KeyChainEventListener() {
@Override
public void onKeysAdded(List<ECKey> k) {
keys.addAll(k);
}
});
wallet.freshReceiveKey();
assertEquals(1, keys.size());
}
@Test
public void upgradeToHDUnencrypted() throws Exception {
// This isn't very deep because most of it is tested in KeyChainGroupTest and Wallet just forwards most logic
// there. We're mostly concerned with the slightly different auto upgrade logic: KeyChainGroup won't do an
// on-demand auto upgrade of the wallet to HD even in the unencrypted case, because the key rotation time is
// a property of the Wallet, not the KeyChainGroup (it should perhaps be moved at some point - it doesn't matter
// much where it goes). Wallet on the other hand will try to auto-upgrade you when possible.
// Create an old-style random wallet.
KeyChainGroup group = new KeyChainGroup(PARAMS);
group.importKeys(new ECKey(), new ECKey());
wallet = new Wallet(PARAMS, group);
assertTrue(wallet.isDeterministicUpgradeRequired());
// Use an HD feature.
wallet.freshReceiveKey();
assertFalse(wallet.isDeterministicUpgradeRequired());
}
@Test
public void upgradeToHDEncrypted() throws Exception {
// Create an old-style random wallet.
KeyChainGroup group = new KeyChainGroup(PARAMS);
group.importKeys(new ECKey(), new ECKey());
wallet = new Wallet(PARAMS, group);
assertTrue(wallet.isDeterministicUpgradeRequired());
KeyCrypter crypter = new KeyCrypterScrypt();
KeyParameter aesKey = crypter.deriveKey("abc");
wallet.encrypt(crypter, aesKey);
try {
wallet.freshReceiveKey();
} catch (DeterministicUpgradeRequiresPassword e) {
// Expected.
}
wallet.upgradeToDeterministic(aesKey);
assertFalse(wallet.isDeterministicUpgradeRequired());
wallet.freshReceiveKey(); // works.
}
@Test(expected = IllegalStateException.class)
public void shouldNotAddTransactionSignerThatIsNotReady() throws Exception {
wallet.addTransactionSigner(new NopTransactionSigner(false));
}
@Test
public void transactionSignersShouldBeSerializedAlongWithWallet() throws Exception {
TransactionSigner signer = new NopTransactionSigner(true);
wallet.addTransactionSigner(signer);
assertEquals(2, wallet.getTransactionSigners().size());
wallet = roundTrip(wallet);
assertEquals(2, wallet.getTransactionSigners().size());
assertTrue(wallet.getTransactionSigners().get(1).isReady());
}
@Test
public void watchingMarriedWallet() throws Exception {
DeterministicKey watchKey = wallet.getWatchingKey();
String serialized = watchKey.serializePubB58(PARAMS);
Wallet wallet = Wallet.fromWatchingKeyB58(PARAMS, serialized, 0);
blockStore = new MemoryBlockStore(PARAMS);
chain = new BlockChain(PARAMS, wallet, blockStore);
final DeterministicKeyChain keyChain = new DeterministicKeyChain(new SecureRandom());
DeterministicKey partnerKey = DeterministicKey.deserializeB58(null, keyChain.getWatchingKey().serializePubB58(PARAMS), PARAMS);
TransactionSigner signer = new StatelessTransactionSigner() {
@Override
public boolean isReady() {
return true;
}
@Override
public boolean signInputs(ProposedTransaction propTx, KeyBag keyBag) {
assertEquals(propTx.partialTx.getInputs().size(), propTx.keyPaths.size());
List<ChildNumber> externalZeroLeaf = ImmutableList.<ChildNumber>builder()
.addAll(DeterministicKeyChain.ACCOUNT_ZERO_PATH)
.addAll(DeterministicKeyChain.EXTERNAL_SUBPATH).add(ChildNumber.ZERO).build();
for (TransactionInput input : propTx.partialTx.getInputs()) {
List<ChildNumber> keypath = propTx.keyPaths.get(input.getConnectedOutput().getScriptPubKey());
assertNotNull(keypath);
assertEquals(externalZeroLeaf, keypath);
}
return true;
}
};
wallet.addTransactionSigner(signer);
MarriedKeyChain chain = MarriedKeyChain.builder()
.random(new SecureRandom())
.followingKeys(partnerKey)
.build();
wallet.addAndActivateHDChain(chain);
Address myAddress = wallet.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
sendMoneyToWallet(wallet, AbstractBlockChain.NewBlockType.BEST_CHAIN, COIN, myAddress);
SendRequest req = SendRequest.emptyWallet(OTHER_ADDRESS);
req.missingSigsMode = Wallet.MissingSigsMode.USE_DUMMY_SIG;
wallet.completeTx(req);
}
@Test
public void sendRequestExchangeRate() throws Exception {
receiveATransaction(wallet, myAddress);
SendRequest sendRequest = SendRequest.to(myAddress, Coin.COIN);
sendRequest.exchangeRate = new ExchangeRate(Fiat.parseFiat("EUR", "500"));
wallet.completeTx(sendRequest);
assertEquals(sendRequest.exchangeRate, sendRequest.tx.getExchangeRate());
}
@Test
public void sendRequestMemo() throws Exception {
receiveATransaction(wallet, myAddress);
SendRequest sendRequest = SendRequest.to(myAddress, Coin.COIN);
sendRequest.memo = "memo";
wallet.completeTx(sendRequest);
assertEquals(sendRequest.memo, sendRequest.tx.getMemo());
}
@Test(expected = java.lang.IllegalStateException.class)
public void sendCoinsNoBroadcasterTest() throws InsufficientMoneyException {
ECKey key = ECKey.fromPrivate(BigInteger.TEN);
SendRequest req = SendRequest.to(OTHER_ADDRESS.getParameters(), key, SATOSHI.multiply(12));
wallet.sendCoins(req);
}
@Test
public void sendCoinsWithBroadcasterTest() throws InsufficientMoneyException {
ECKey key = ECKey.fromPrivate(BigInteger.TEN);
receiveATransactionAmount(wallet, myAddress, Coin.COIN);
MockTransactionBroadcaster broadcaster = new MockTransactionBroadcaster(wallet);
wallet.setTransactionBroadcaster(broadcaster);
SendRequest req = SendRequest.to(OTHER_ADDRESS.getParameters(), key, Coin.CENT);
wallet.sendCoins(req);
}
@Test
public void fromKeys() {
ECKey key = ECKey.fromPrivate(Utils.HEX.decode("00905b93f990267f4104f316261fc10f9f983551f9ef160854f40102eb71cffdcc"));
Wallet wallet = Wallet.fromKeys(PARAMS, Arrays.asList(key));
assertEquals(1, wallet.getImportedKeys().size());
assertEquals(key, wallet.getImportedKeys().get(0));
wallet.upgradeToDeterministic(null);
String seed = wallet.getKeyChainSeed().toHexString();
assertEquals("5ca8cd6c01aa004d3c5396c628b78a4a89462f412f460a845b594ac42eceaa264b0e14dcd4fe73d4ed08ce06f4c28facfa85042d26d784ab2798a870bb7af556", seed);
}
@Test
public void reset() {
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, COIN, myAddress);
assertNotEquals(Coin.ZERO, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
assertNotEquals(0, wallet.getTransactions(false).size());
assertNotEquals(0, wallet.getUnspents().size());
wallet.reset();
assertEquals(Coin.ZERO, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
assertEquals(0, wallet.getTransactions(false).size());
assertEquals(0, wallet.getUnspents().size());
}
@Test
public void totalReceivedSent() throws Exception {
// Receive 4 BTC in 2 separate transactions
Transaction toMe1 = createFakeTxWithoutChangeAddress(PARAMS, COIN.multiply(2), myAddress);
Transaction toMe2 = createFakeTxWithoutChangeAddress(PARAMS, COIN.multiply(2), myAddress);
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, toMe1, toMe2);
// Check we calculate the total received correctly
assertEquals(Coin.COIN.multiply(4), wallet.getTotalReceived());
// Send 3 BTC in a single transaction
SendRequest req = SendRequest.to(OTHER_ADDRESS, Coin.COIN.multiply(3));
wallet.completeTx(req);
sendMoneyToWallet(AbstractBlockChain.NewBlockType.BEST_CHAIN, req.tx);
// Check that we still have the same totalReceived, since the above tx will have sent us change back
assertEquals(Coin.COIN.multiply(4),wallet.getTotalReceived());
assertEquals(Coin.COIN.multiply(3),wallet.getTotalSent());
// TODO: test shared wallet calculation here
}
}