Skip to content

Commit

Permalink
Added ability to create new addresses and view previous ones
Browse files Browse the repository at this point in the history
  • Loading branch information
erasmospunk committed Feb 2, 2015
1 parent 0d9e51f commit 259b167
Show file tree
Hide file tree
Showing 28 changed files with 885 additions and 32 deletions.
33 changes: 31 additions & 2 deletions core/src/main/java/com/coinomi/core/wallet/SimpleHDKeyChain.java
Original file line number Diff line number Diff line change
Expand Up @@ -844,11 +844,36 @@ private List<DeterministicKey> maybeLookAhead(DeterministicKey parent, int issue
return result;
}

/**
* Returns keys used on external path. This may be fewer than the number that have been deserialized
* or held in memory, because of the lookahead zone.
*/
public ArrayList<DeterministicKey> getIssuedExternalKeys() {
lock.lock();
try {
maybeLookAhead();
int treeSize = externalKey.getPath().size();
ArrayList<DeterministicKey> issuedKeys = new ArrayList<DeterministicKey>();
for (ECKey key : simpleKeyChain.getKeys()) {
DeterministicKey detkey = (DeterministicKey) key;
DeterministicKey parent = detkey.getParent();
if (parent == null) continue;
if (detkey.getPath().size() <= treeSize) continue;
if (parent.equals(internalKey)) continue;
if (parent.equals(externalKey) && detkey.getChildNumber().num() >= issuedExternalKeys) continue;
issuedKeys.add(detkey);
}
return issuedKeys;
} finally {
lock.unlock();
}
}

/**
* Returns number of keys used on external path. This may be fewer than the number that have been deserialized
* or held in memory, because of the lookahead zone.
*/
public int getIssuedExternalKeys() {
public int getNumIssuedExternalKeys() {
lock.lock();
try {
return issuedExternalKeys;
Expand All @@ -861,7 +886,7 @@ public int getIssuedExternalKeys() {
* Returns number of keys used on internal path. This may be fewer than the number that have been deserialized
* or held in memory, because of the lookahead zone.
*/
public int getIssuedInternalKeys() {
public int getNumIssuedInternalKeys() {
lock.lock();
try {
return issuedInternalKeys;
Expand Down Expand Up @@ -908,5 +933,9 @@ public List<DeterministicKey> getLeafKeys() {
}
return keys.build();
}

public boolean isExternal(DeterministicKey key) {
return key.getParent() != null && key.getParent().equals(externalKey);
}
}

117 changes: 114 additions & 3 deletions core/src/main/java/com/coinomi/core/wallet/WalletPocket.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.coinomi.core.network.interfaces.ConnectionEventListener;
import com.coinomi.core.network.interfaces.TransactionEventListener;
import com.coinomi.core.protos.Protos;
import com.coinomi.core.wallet.exceptions.Bip44KeyLookAheadExceededException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
Expand Down Expand Up @@ -74,6 +75,8 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
Expand All @@ -87,6 +90,9 @@

import javax.annotation.Nullable;

import static org.bitcoinj.wallet.KeyChain.KeyPurpose.RECEIVE_FUNDS;
import static org.bitcoinj.wallet.KeyChain.KeyPurpose.CHANGE;

import static com.coinomi.core.Preconditions.checkArgument;
import static com.coinomi.core.Preconditions.checkNotNull;
import static com.coinomi.core.Preconditions.checkState;
Expand All @@ -101,6 +107,8 @@ public class WalletPocket implements TransactionBag, TransactionEventListener, C

private final ReentrantLock lock = Threading.lock("WalletPocket");

private final static int TX_DEPTH_SAVE_THRESHOLD = 4;

private final CoinType coinType;

private String description;
Expand Down Expand Up @@ -168,7 +176,6 @@ public void run() {
if (wallet != null) wallet.saveNow();
}
};
private int TX_DEPTH_SAVE_THRESHOLD = 4;

public WalletPocket(DeterministicKey rootKey, CoinType coinType,
@Nullable KeyCrypter keyCrypter, @Nullable KeyParameter key) {
Expand Down Expand Up @@ -919,6 +926,7 @@ private void applyState(AddressStatus status, HashMap<Sha256Hash, Transaction> t
}

private void markUnspentTXO(AddressStatus status, Map<Sha256Hash, Transaction> txs) {
checkState(lock.isHeldByCurrentThread());
Set<UnspentTx> utxs = status.getUnspentTxs();
ArrayList<TransactionOutput> unspentOutputs = new ArrayList<TransactionOutput>(utxs.size());
// Mark unspent outputs
Expand Down Expand Up @@ -1966,11 +1974,114 @@ private void resetTxInputs(SendRequest req, List<TransactionInput> originalInput

/** Returns the address used for change outputs. Note: this will probably go away in future. */
Address getChangeAddress() {
return currentAddress(SimpleHDKeyChain.KeyPurpose.CHANGE);
return currentAddress(CHANGE);
}

/**
* Get current receive address, does not mark it as used
*/
public Address getReceiveAddress() {
return currentAddress(SimpleHDKeyChain.KeyPurpose.RECEIVE_FUNDS);
return currentAddress(RECEIVE_FUNDS);
}

/**
* Get a fresh address by marking the current receive address as used. It will throw
* {@link Bip44KeyLookAheadExceededException} if we requested too many addresses that
* exceed the BIP44 look ahead threshold.
*/
public Address getFreshReceiveAddress() throws Bip44KeyLookAheadExceededException {
lock.lock();
try {
DeterministicKey currentUnusedKey = keys.getCurrentUnusedKey(RECEIVE_FUNDS);
int maximumKeyIndex = SimpleHDKeyChain.LOOKAHEAD - 1;

// If there are used keys
if (!addressesStatus.isEmpty()) {
int lastUsedKeyIndex = 0;
// Find the last used key index
for (Map.Entry<Address, String> entry : addressesStatus.entrySet()) {
if (entry.getValue() == null) continue;
DeterministicKey usedKey = keys.findKeyFromPubHash(entry.getKey().getHash160());
if (usedKey != null && keys.isExternal(usedKey) && usedKey.getChildNumber().num() > lastUsedKeyIndex) {
lastUsedKeyIndex = usedKey.getChildNumber().num();
}
}
maximumKeyIndex = lastUsedKeyIndex + SimpleHDKeyChain.LOOKAHEAD;
}

log.info("Maximum key index for new key is {}", maximumKeyIndex);

// If we exceeded the BIP44 look ahead threshold
if (currentUnusedKey.getChildNumber().num() >= maximumKeyIndex) {
throw new Bip44KeyLookAheadExceededException();
}

return keys.getKey(RECEIVE_FUNDS).toAddress(coinType);
} finally {
lock.unlock();
walletSaveNow();
}
}

private static final Comparator<DeterministicKey> HD_KEY_COMPARATOR =
new Comparator<DeterministicKey>() {
@Override
public int compare(final DeterministicKey k1, final DeterministicKey k2) {
return Integer.compare(k2.getChildNumber().num(), k1.getChildNumber().num());
}
};

/**
* Returns the number of issued receiving keys
*/
public int getNumberIssuedReceiveAddresses() {
lock.lock();
try {
return keys.getNumIssuedExternalKeys();
} finally {
lock.unlock();
}
}

/**
* Returns a list of addresses that have been issued.
* The list is sorted in descending chronological order: older in the end
*/
public List<Address> getIssuedReceiveAddresses() {
lock.lock();
try {
ArrayList<DeterministicKey> issuedKeys = keys.getIssuedExternalKeys();
ArrayList<Address> receiveAddresses = new ArrayList<Address>();

Collections.sort(issuedKeys, HD_KEY_COMPARATOR);

for (ECKey key : issuedKeys) {
receiveAddresses.add(key.toAddress(coinType));
}
return receiveAddresses;
} finally {
lock.unlock();
}
}

/**
* Get the currently used receiving and change addresses
*/
public Set<Address> getUsedAddresses() {
lock.lock();
try {
HashSet<Address> usedAddresses = new HashSet<Address>();

for (Map.Entry<Address, String> entry : addressesStatus.entrySet()) {
if (entry.getValue() != null) {
usedAddresses.add(entry.getKey());
}
}

return usedAddresses;
} finally {
lock.unlock();
}
}

@VisibleForTesting Address currentAddress(SimpleHDKeyChain.KeyPurpose purpose) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.coinomi.core.wallet.exceptions;

/**
* @author Giannis Dzegoutanis
*/
public class Bip44KeyLookAheadExceededException extends Throwable {
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ public void deriveCoin() throws Exception {
key4.sign(Sha256Hash.ZERO_HASH);
}

@Test
public void externalKeyCheck() {
assertFalse(chain.isExternal(chain.getKey(KeyChain.KeyPurpose.CHANGE)));
assertTrue(chain.isExternal(chain.getKey(KeyChain.KeyPurpose.RECEIVE_FUNDS)));
}

@Test
public void events() throws Exception {
// Check that we get the right events at the right time.
Expand Down
87 changes: 78 additions & 9 deletions core/src/test/java/com/coinomi/core/wallet/WalletPocketTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import org.bitcoinj.crypto.KeyCrypterScrypt;
import org.bitcoinj.utils.BriefLogFormatter;
import org.bitcoinj.wallet.DeterministicSeed;

import com.coinomi.core.wallet.exceptions.Bip44KeyLookAheadExceededException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;

Expand All @@ -35,14 +37,13 @@
import org.spongycastle.crypto.params.KeyParameter;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
Expand Down Expand Up @@ -82,13 +83,81 @@ public void watchingAddresses() {
}
}

@Test
public void issuedKeys() throws Bip44KeyLookAheadExceededException {
LinkedList<Address> issuedAddresses = new LinkedList<Address>();
assertEquals(0, pocket.getIssuedReceiveAddresses().size());
assertEquals(0, pocket.keys.getNumIssuedExternalKeys());

issuedAddresses.add(0, pocket.getFreshReceiveAddress());
assertEquals(1, pocket.getIssuedReceiveAddresses().size());
assertEquals(1, pocket.keys.getNumIssuedExternalKeys());
assertEquals(issuedAddresses, pocket.getIssuedReceiveAddresses());

issuedAddresses.add(0, pocket.getFreshReceiveAddress());
assertEquals(2, pocket.getIssuedReceiveAddresses().size());
assertEquals(2, pocket.keys.getNumIssuedExternalKeys());
assertEquals(issuedAddresses, pocket.getIssuedReceiveAddresses());
}

@Test
public void issuedKeysLimit() throws Exception {
try {
for (int i = 0; i < 100; i++) {
pocket.getFreshReceiveAddress();
}
} catch (Bip44KeyLookAheadExceededException e) {
// We haven't used any key so the total must be 20 - 1 (the unused key)
assertEquals(19, pocket.getNumberIssuedReceiveAddresses());
assertEquals(19, pocket.getIssuedReceiveAddresses().size());
}

pocket.onConnection(getBlockchainConnection(type));

try {
for (int i = 0; i < 100; i++) {
pocket.getFreshReceiveAddress();
}
} catch (Bip44KeyLookAheadExceededException e) {
try {
pocket.getFreshReceiveAddress();
} catch (Bip44KeyLookAheadExceededException e1) { }
// We used 18, so the total must be (20-1)+18=37
assertEquals(37, pocket.getNumberIssuedReceiveAddresses());
assertEquals(37, pocket.getIssuedReceiveAddresses().size());
}
}

@Test
public void issuedKeysLimit2() throws Exception {
try {
for (int i = 0; i < 100; i++) {
pocket.getFreshReceiveAddress();
}
} catch (Bip44KeyLookAheadExceededException e) {
// We haven't used any key so the total must be 20 - 1 (the unused key)
assertEquals(19, pocket.getNumberIssuedReceiveAddresses());
assertEquals(19, pocket.getIssuedReceiveAddresses().size());
}
}

@Test
public void usedAddresses() throws Exception {
assertEquals(0, pocket.getUsedAddresses().size());

pocket.onConnection(getBlockchainConnection(type));

// Receive and change addresses
assertEquals(13, pocket.getUsedAddresses().size());
}

@Test
public void fillTransactions() throws Exception {
pocket.onConnection(getBlockchainConnection(type));

// Issued keys
assertEquals(18, pocket.keys.getIssuedExternalKeys());
assertEquals(9, pocket.keys.getIssuedInternalKeys());
assertEquals(18, pocket.keys.getNumIssuedExternalKeys());
assertEquals(9, pocket.keys.getNumIssuedInternalKeys());

// No addresses left to subscribe
List<Address> addressesToWatch = pocket.getAddressesToWatch();
Expand All @@ -100,7 +169,7 @@ public void fillTransactions() throws Exception {

Address receiveAddr = pocket.getReceiveAddress();
// This key is not issued
assertEquals(18, pocket.keys.getIssuedExternalKeys());
assertEquals(18, pocket.keys.getNumIssuedExternalKeys());
assertEquals(67, pocket.addressesStatus.size());
assertEquals(67, pocket.addressesSubscribed.size());

Expand Down Expand Up @@ -142,8 +211,8 @@ public void serializeUnencryptedNormal() throws Exception {


// Issued keys
assertEquals(18, newPocket.keys.getIssuedExternalKeys());
assertEquals(9, newPocket.keys.getIssuedInternalKeys());
assertEquals(18, newPocket.keys.getNumIssuedExternalKeys());
assertEquals(9, newPocket.keys.getNumIssuedInternalKeys());

newPocket.onConnection(getBlockchainConnection(type));

Expand All @@ -166,8 +235,8 @@ public void serializeUnencryptedEmpty() throws Exception {
assertEquals(walletPocketProto.toString(), newPocket.toProtobuf().toString());

// Issued keys
assertEquals(0, newPocket.keys.getIssuedExternalKeys());
assertEquals(0, newPocket.keys.getIssuedInternalKeys());
assertEquals(0, newPocket.keys.getNumIssuedExternalKeys());
assertEquals(0, newPocket.keys.getNumIssuedInternalKeys());

// 20 lookahead + 20 lookahead
assertEquals(40, newPocket.keys.getLeafKeys().size());
Expand Down
6 changes: 6 additions & 0 deletions wallet/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@
android:name="android.support.PARENT_ACTIVITY"
android:value="com.coinomi.wallet.ui.WalletActivity" />
</activity>
<activity
android:name="com.coinomi.wallet.ui.PreviousAddressesActivity"
android:label="@string/title_activity_previous_addresses"
android:screenOrientation="portrait"
android:theme="@style/AppThemeCustomActionBar" >
</activity>

<provider
android:name="com.coinomi.wallet.ExchangeRatesProvider"
Expand Down
Loading

0 comments on commit 259b167

Please sign in to comment.