/** * Copyright 2011 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.bitcoin.core; import org.junit.Before; import org.junit.Test; import java.math.BigInteger; import static com.google.bitcoin.core.Utils.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class WalletTest { static final NetworkParameters params = NetworkParameters.unitTests(); private Address myAddress; private Wallet wallet; private BlockStore blockStore; @Before public void setUp() throws Exception { ECKey myKey = new ECKey(); myAddress = myKey.toAddress(params); wallet = new Wallet(params); wallet.addKey(myKey); blockStore = new MemoryBlockStore(params); } private Transaction createFakeTx(BigInteger nanocoins, Address to) { Transaction t = new Transaction(params); TransactionOutput o1 = new TransactionOutput(params, t, nanocoins, to); t.addOutput(o1); // Make a previous tx simply to send us sufficient coins. This prev tx is not really valid but it doesn't // matter for our purposes. Transaction prevTx = new Transaction(params); TransactionOutput prevOut = new TransactionOutput(params, prevTx, nanocoins, to); prevTx.addOutput(prevOut); // Connect it. t.addInput(prevOut); return t; } class BlockPair { StoredBlock storedBlock; Block block; } // Emulates receiving a valid block that builds on top of the chain. private BlockPair createFakeBlock(Transaction... transactions) { try { Block b = blockStore.getChainHead().getHeader().createNextBlock(new ECKey().toAddress(params)); for (Transaction tx : transactions) b.addTransaction(tx); b.solve(); BlockPair pair = new BlockPair(); pair.block = b; pair.storedBlock = blockStore.getChainHead().build(b); blockStore.put(pair.storedBlock); blockStore.setChainHead(pair.storedBlock); return pair; } catch (VerificationException e) { throw new RuntimeException(e); // Cannot happen. } catch (BlockStoreException e) { throw new RuntimeException(e); // Cannot happen. } } @Test public void testBasicSpending() throws Exception { // We'll set up a wallet that receives a coin, then sends a coin of lesser value and keeps the change. BigInteger v1 = Utils.toNanoCoins(1, 0); Transaction t1 = createFakeTx(v1, myAddress); wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(v1, wallet.getBalance()); ECKey k2 = new ECKey(); BigInteger v2 = toNanoCoins(0, 50); Transaction t2 = wallet.createSend(k2.toAddress(params), v2); // Do some basic sanity checks. assertEquals(1, t2.inputs.size()); assertEquals(myAddress, t2.inputs.get(0).getScriptSig().getFromAddress()); // We have NOT proven that the signature is correct! } @Test public void testSideChain() throws Exception { // The wallet receives a coin on the main chain, then on a side chain. Only main chain counts towards balance. BigInteger v1 = Utils.toNanoCoins(1, 0); Transaction t1 = createFakeTx(v1, myAddress); wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(v1, wallet.getBalance()); BigInteger v2 = toNanoCoins(0, 50); Transaction t2 = createFakeTx(v2, myAddress); wallet.receive(t2, null, BlockChain.NewBlockType.SIDE_CHAIN); assertEquals(v1, wallet.getBalance()); } @Test public void testListener() throws Exception { final Transaction fakeTx = createFakeTx(Utils.toNanoCoins(1, 0), myAddress); final boolean[] didRun = new boolean[1]; WalletEventListener listener = new WalletEventListener() { public void onCoinsReceived(Wallet w, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { assertTrue(prevBalance.equals(BigInteger.ZERO)); assertTrue(newBalance.equals(Utils.toNanoCoins(1, 0))); assertEquals(tx, fakeTx); // Same object. assertEquals(w, wallet); // Same object. didRun[0] = true; } }; wallet.addEventListener(listener); wallet.receive(fakeTx, null, BlockChain.NewBlockType.BEST_CHAIN); assertTrue(didRun[0]); } @Test public void testBalance() throws Exception { // Receive 5 coins then half a coin. BigInteger v1 = toNanoCoins(5, 0); BigInteger v2 = toNanoCoins(0, 50); Transaction t1 = createFakeTx(v1, myAddress); Transaction t2 = createFakeTx(v2, myAddress); StoredBlock b1 = createFakeBlock(t1).storedBlock; StoredBlock b2 = createFakeBlock(t2).storedBlock; BigInteger expected = toNanoCoins(5, 50); wallet.receive(t1, b1, BlockChain.NewBlockType.BEST_CHAIN); wallet.receive(t2, b2, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(expected, wallet.getBalance()); // Now spend one coin. BigInteger v3 = toNanoCoins(1, 0); Transaction spend = wallet.createSend(new ECKey().toAddress(params), v3); wallet.confirmSend(spend); // 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(toNanoCoins(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. StoredBlock b3 = createFakeBlock(spend).storedBlock; wallet.receive(spend, b3, BlockChain.NewBlockType.BEST_CHAIN); // Change is confirmed. We started with 5.50 so we should have 4.50 left. BigInteger v4 = toNanoCoins(4, 50); assertEquals(v4, wallet.getBalance(Wallet.BalanceType.AVAILABLE)); } // Intuitively you'd expect to be able to create a transaction with identical inputs and outputs and get an // identical result to the official client. 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 testBlockChainCatchup() throws Exception { Transaction tx1 = createFakeTx(Utils.toNanoCoins(1, 0), myAddress); StoredBlock b1 = createFakeBlock(tx1).storedBlock; wallet.receive(tx1, b1, BlockChain.NewBlockType.BEST_CHAIN); // Send 0.10 to somebody else. Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10), myAddress); // 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. StoredBlock b2 = createFakeBlock(send1).storedBlock; wallet.receive(send1, b2, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(bitcoinValueToFriendlyString(wallet.getBalance()), "0.90"); // And we do it again after the catchup. Transaction send2 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10), myAddress); // What we'd really like to do is prove the official client would accept it .... no such luck unfortunately. wallet.confirmSend(send2); StoredBlock b3 = createFakeBlock(send2).storedBlock; wallet.receive(send2, b3, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(bitcoinValueToFriendlyString(wallet.getBalance()), "0.80"); } @Test public void testBalances() throws Exception { BigInteger nanos = Utils.toNanoCoins(1, 0); Transaction tx1 = createFakeTx(nanos, myAddress); wallet.receive(tx1, null, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(nanos, tx1.getValueSentToMe(wallet, true)); // Send 0.10 to somebody else. Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10), myAddress); // Reserialize. Transaction send2 = new Transaction(params, send1.bitcoinSerialize()); assertEquals(nanos, send2.getValueSentFromMe(wallet)); } @Test public void testFinneyAttack() 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 ourselves performing the attack correctly: a double spend on the chain moves // transactions from pending to dead. // // Note that the other way around, where a pending transaction sending us coins becomes dead, // isn't tested because today BitCoinJ only learns about such transactions when they appear in the chain. final Transaction[] eventDead = new Transaction[1]; final Transaction[] eventReplacement = new Transaction[1]; wallet.addEventListener(new WalletEventListener() { @Override public void onDeadTransaction(Transaction deadTx, Transaction replacementTx) { eventDead[0] = deadTx; eventReplacement[0] = replacementTx; } }); // Receive 1 BTC. BigInteger nanos = Utils.toNanoCoins(1, 0); Transaction t1 = createFakeTx(nanos, myAddress); wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN); // Create a send to a merchant. Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 50)); // Create a double spend. Transaction send2 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 50)); // Broadcast send1. wallet.confirmSend(send1); // Receive a block that overrides it. wallet.receive(send2, null, BlockChain.NewBlockType.BEST_CHAIN); assertEquals(send1, eventDead[0]); assertEquals(send2, eventReplacement[0]); } }