- Change entrySet to CopyOnWriteArraySet to avoid potential ConcurrentModificationExceptions

- Make entrySet final
- Avoind unneeded wrapping
- Remove visibility of entrySet
- Add getAddressEntriesAsListImmutable method. This is the only access for the hashSet
so we ensure it cannot be changed from outside.
- Remove stream() method
- Remove unused return types
- Improve some stream structures
- Renaming, improve comments
This commit is contained in:
chimp1984 2020-09-08 18:53:32 -05:00
parent e197b4ce25
commit fa987d1461
No known key found for this signature in database
GPG key ID: 9801B4EC591F90E3
3 changed files with 94 additions and 95 deletions

View file

@ -29,28 +29,26 @@ import org.bitcoinj.wallet.Wallet;
import com.google.inject.Inject;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import com.google.common.collect.ImmutableList;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.Getter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
/**
* The List supporting our persistence solution.
* The AddressEntries was previously stored as list, now as hashSet. We still keep the old name to reflect the
* associated protobuf message.
*/
@ToString
@Slf4j
public final class AddressEntryList implements UserThreadMappedPersistableEnvelope, PersistedDataHost {
transient private Storage<AddressEntryList> storage;
transient private Wallet wallet;
@Getter
private Set<AddressEntry> entrySet;
private final Set<AddressEntry> entrySet = new CopyOnWriteArraySet<>();
@Inject
public AddressEntryList(Storage<AddressEntryList> storage) {
@ -60,8 +58,10 @@ public final class AddressEntryList implements UserThreadMappedPersistableEnvelo
@Override
public void readPersisted() {
AddressEntryList persisted = storage.initAndGetPersisted(this, 50);
if (persisted != null)
entrySet = new HashSet<>(persisted.getEntrySet());
if (persisted != null) {
entrySet.clear();
entrySet.addAll(persisted.entrySet);
}
}
@ -70,21 +70,21 @@ public final class AddressEntryList implements UserThreadMappedPersistableEnvelo
///////////////////////////////////////////////////////////////////////////////////////////
private AddressEntryList(Set<AddressEntry> entrySet) {
this.entrySet = entrySet;
this.entrySet.addAll(entrySet);
}
public static AddressEntryList fromProto(protobuf.AddressEntryList proto) {
return new AddressEntryList(new HashSet<>(proto.getAddressEntryList().stream().map(AddressEntry::fromProto).collect(Collectors.toList())));
Set<AddressEntry> entrySet = proto.getAddressEntryList().stream()
.map(AddressEntry::fromProto)
.collect(Collectors.toSet());
return new AddressEntryList(entrySet);
}
@Override
public Message toProtoMessage() {
// We clone list as we got ConcurrentModificationExceptions
List<AddressEntry> clone = new ArrayList<>(entrySet);
List<protobuf.AddressEntry> addressEntries = clone.stream()
Set<protobuf.AddressEntry> addressEntries = entrySet.stream()
.map(AddressEntry::toProtoMessage)
.collect(Collectors.toList());
.collect(Collectors.toSet());
return protobuf.PersistableEnvelope.newBuilder()
.setAddressEntryList(protobuf.AddressEntryList.newBuilder()
.addAllAddressEntry(addressEntries))
@ -99,7 +99,7 @@ public final class AddressEntryList implements UserThreadMappedPersistableEnvelo
public void onWalletReady(Wallet wallet) {
this.wallet = wallet;
if (entrySet != null) {
if (!entrySet.isEmpty()) {
entrySet.forEach(addressEntry -> {
DeterministicKey keyFromPubHash = (DeterministicKey) wallet.findKeyFromPubHash(addressEntry.getPubKeyHash());
if (keyFromPubHash != null) {
@ -109,8 +109,7 @@ public final class AddressEntryList implements UserThreadMappedPersistableEnvelo
}
});
} else {
entrySet = new HashSet<>();
add(new AddressEntry(wallet.freshReceiveKey(), AddressEntry.Context.ARBITRATOR));
entrySet.add(new AddressEntry(wallet.freshReceiveKey(), AddressEntry.Context.ARBITRATOR));
// In case we restore from seed words and have balance we need to add the relevant addresses to our list.
// IssuedReceiveAddresses does not contain all addresses where we expect balance so we need to listen to
@ -118,73 +117,51 @@ public final class AddressEntryList implements UserThreadMappedPersistableEnvelo
if (wallet.getBalance().isPositive()) {
wallet.getIssuedReceiveAddresses().forEach(address -> {
log.info("Create AddressEntry for IssuedReceiveAddress. address={}", address.toString());
add(new AddressEntry((DeterministicKey) wallet.findKeyFromPubHash(address.getHash160()), AddressEntry.Context.AVAILABLE));
entrySet.add(new AddressEntry((DeterministicKey) wallet.findKeyFromPubHash(address.getHash160()), AddressEntry.Context.AVAILABLE));
});
}
persist();
}
persist();
// We add those listeners to get notified about potential new transactions and
// add an address entry list in case it does not exist yet. This is mainly needed for restore from seed words
// but can help as well in case the addressEntry list would miss an address where the wallet was received
// funds (e.g. if the user sends funds to an address which has not been provided in the main UI - like from the
// wallet details window).
wallet.addCoinsReceivedEventListener((w, tx, prevBalance, newBalance) -> {
updateList(tx);
wallet.addCoinsReceivedEventListener((wallet1, tx, prevBalance, newBalance) -> {
updateEntrySet(tx);
});
wallet.addCoinsSentEventListener((w, tx, prevBalance, newBalance) -> {
updateList(tx);
wallet.addCoinsSentEventListener((wallet1, tx, prevBalance, newBalance) -> {
updateEntrySet(tx);
});
}
private void updateList(Transaction tx) {
tx.getOutputs().stream()
.filter(output -> output.isMine(wallet))
.map(output -> output.getAddressFromP2PKHScript(wallet.getNetworkParameters()))
.filter(Objects::nonNull)
.filter(address -> !listContainsEntryWithAddress(address.toBase58()))
.map(address -> (DeterministicKey) wallet.findKeyFromPubHash(address.getHash160()))
.filter(Objects::nonNull)
.map(deterministicKey -> new AddressEntry(deterministicKey, AddressEntry.Context.AVAILABLE))
.forEach(this::add);
public ImmutableList<AddressEntry> getAddressEntriesAsListImmutable() {
return ImmutableList.copyOf(entrySet);
}
private boolean listContainsEntryWithAddress(String addressString) {
return entrySet.stream().anyMatch(addressEntry -> Objects.equals(addressEntry.getAddressString(), addressString));
}
private boolean add(AddressEntry addressEntry) {
return entrySet.add(addressEntry);
}
private boolean remove(AddressEntry addressEntry) {
return entrySet.remove(addressEntry);
}
public AddressEntry addAddressEntry(AddressEntry addressEntry) {
boolean changed = add(addressEntry);
if (changed)
public void addAddressEntry(AddressEntry addressEntry) {
boolean replacedEntryOnAdd = entrySet.add(addressEntry);
if (replacedEntryOnAdd)
persist();
return addressEntry;
}
public void swapTradeToSavings(String offerId) {
entrySet.stream().filter(addressEntry -> offerId.equals(addressEntry.getOfferId()))
.findAny().ifPresent(this::swapToAvailable);
}
public void swapToAvailable(AddressEntry addressEntry) {
boolean changed1 = remove(addressEntry);
boolean changed2 = add(new AddressEntry(addressEntry.getKeyPair(), AddressEntry.Context.AVAILABLE));
if (changed1 || changed2)
boolean hadRemovedEntry = entrySet.remove(addressEntry);
boolean replacedEntryOnAdd = entrySet.add(new AddressEntry(addressEntry.getKeyPair(), AddressEntry.Context.AVAILABLE));
if (hadRemovedEntry || replacedEntryOnAdd) {
persist();
}
}
public AddressEntry swapAvailableToAddressEntryWithOfferId(AddressEntry addressEntry, AddressEntry.Context context, String offerId) {
boolean changed1 = remove(addressEntry);
public AddressEntry swapAvailableToAddressEntryWithOfferId(AddressEntry addressEntry,
AddressEntry.Context context,
String offerId) {
boolean hadRemovedEntry = entrySet.remove(addressEntry);
final AddressEntry newAddressEntry = new AddressEntry(addressEntry.getKeyPair(), context, offerId);
boolean changed2 = add(newAddressEntry);
if (changed1 || changed2)
boolean replacedEntryOnAdd = entrySet.add(newAddressEntry);
if (hadRemovedEntry || replacedEntryOnAdd)
persist();
return newAddressEntry;
@ -194,7 +171,25 @@ public final class AddressEntryList implements UserThreadMappedPersistableEnvelo
storage.queueUpForSave(50);
}
public Stream<AddressEntry> stream() {
return entrySet.stream();
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void updateEntrySet(Transaction tx) {
tx.getOutputs().stream()
.filter(output -> output.isMine(wallet))
.map(output -> output.getAddressFromP2PKHScript(wallet.getNetworkParameters()))
.filter(Objects::nonNull)
.filter(address -> !isAddressInEntries(address.toBase58()))
.map(address -> (DeterministicKey) wallet.findKeyFromPubHash(address.getHash160()))
.filter(Objects::nonNull)
.map(deterministicKey -> new AddressEntry(deterministicKey, AddressEntry.Context.AVAILABLE))
.forEach(this::addAddressEntry);
}
private boolean isAddressInEntries(String addressString) {
return entrySet.stream()
.anyMatch(addressEntry -> addressString.equals(addressEntry.getAddressString()));
}
}

View file

@ -59,7 +59,6 @@ import javax.inject.Inject;
import javax.inject.Named;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Service;
import org.apache.commons.lang3.StringUtils;
@ -498,7 +497,7 @@ public class WalletsSetup {
}
public Set<Address> getAddressesByContext(@SuppressWarnings("SameParameterValue") AddressEntry.Context context) {
return ImmutableList.copyOf(addressEntryList.getList()).stream()
return addressEntryList.getAddressEntriesAsListImmutable().stream()
.filter(addressEntry -> addressEntry.getContext() == context)
.map(AddressEntry::getAddress)
.collect(Collectors.toSet());

View file

@ -47,7 +47,6 @@ import org.bitcoinj.wallet.Wallet;
import javax.inject.Inject;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
@ -108,8 +107,8 @@ public class BtcWalletService extends WalletService {
void decryptWallet(@NotNull KeyParameter key) {
super.decryptWallet(key);
addressEntryList.stream().forEach(e -> {
final DeterministicKey keyPair = e.getKeyPair();
addressEntryList.getAddressEntriesAsListImmutable().stream().forEach(e -> {
DeterministicKey keyPair = e.getKeyPair();
if (keyPair.isEncrypted())
e.setDeterministicKey(keyPair.decrypt(key));
});
@ -119,8 +118,8 @@ public class BtcWalletService extends WalletService {
@Override
void encryptWallet(KeyCrypterScrypt keyCrypterScrypt, KeyParameter key) {
super.encryptWallet(keyCrypterScrypt, key);
addressEntryList.stream().forEach(e -> {
final DeterministicKey keyPair = e.getKeyPair();
addressEntryList.getAddressEntriesAsListImmutable().stream().forEach(e -> {
DeterministicKey keyPair = e.getKeyPair();
if (keyPair.isEncrypted())
e.setDeterministicKey(keyPair.encrypt(keyCrypterScrypt, key));
});
@ -157,12 +156,18 @@ public class BtcWalletService extends WalletService {
// Proposal txs
///////////////////////////////////////////////////////////////////////////////////////////
public Transaction completePreparedReimbursementRequestTx(Coin issuanceAmount, Address issuanceAddress, Transaction feeTx, byte[] opReturnData)
public Transaction completePreparedReimbursementRequestTx(Coin issuanceAmount,
Address issuanceAddress,
Transaction feeTx,
byte[] opReturnData)
throws TransactionVerificationException, WalletException, InsufficientMoneyException {
return completePreparedProposalTx(feeTx, opReturnData, issuanceAmount, issuanceAddress);
}
public Transaction completePreparedCompensationRequestTx(Coin issuanceAmount, Address issuanceAddress, Transaction feeTx, byte[] opReturnData)
public Transaction completePreparedCompensationRequestTx(Coin issuanceAmount,
Address issuanceAddress,
Transaction feeTx,
byte[] opReturnData)
throws TransactionVerificationException, WalletException, InsufficientMoneyException {
return completePreparedProposalTx(feeTx, opReturnData, issuanceAmount, issuanceAddress);
}
@ -292,7 +297,8 @@ public class BtcWalletService extends WalletService {
return completePreparedBsqTxWithBtcFee(preparedTx, opReturnData);
}
private Transaction completePreparedBsqTxWithBtcFee(Transaction preparedTx, byte[] opReturnData) throws InsufficientMoneyException, TransactionVerificationException, WalletException {
private Transaction completePreparedBsqTxWithBtcFee(Transaction preparedTx,
byte[] opReturnData) throws InsufficientMoneyException, TransactionVerificationException, WalletException {
// Remember index for first BTC input
int indexOfBtcFirstInput = preparedTx.getInputs().size();
@ -306,7 +312,8 @@ public class BtcWalletService extends WalletService {
return tx;
}
private Transaction addInputsForMinerFee(Transaction preparedTx, byte[] opReturnData) throws InsufficientMoneyException {
private Transaction addInputsForMinerFee(Transaction preparedTx,
byte[] opReturnData) throws InsufficientMoneyException {
// safety check counter to avoid endless loops
int counter = 0;
// estimated size of input sig
@ -421,7 +428,9 @@ public class BtcWalletService extends WalletService {
return completePreparedBsqTx(preparedBsqTx, isSendTx, null);
}
public Transaction completePreparedBsqTx(Transaction preparedBsqTx, boolean useCustomTxFee, @Nullable byte[] opReturnData) throws
public Transaction completePreparedBsqTx(Transaction preparedBsqTx,
boolean useCustomTxFee,
@Nullable byte[] opReturnData) throws
TransactionVerificationException, WalletException, InsufficientMoneyException {
// preparedBsqTx has following structure:
@ -545,7 +554,8 @@ public class BtcWalletService extends WalletService {
// AddressEntry
///////////////////////////////////////////////////////////////////////////////////////////
public Optional<AddressEntry> getAddressEntry(String offerId, @SuppressWarnings("SameParameterValue") AddressEntry.Context context) {
public Optional<AddressEntry> getAddressEntry(String offerId,
@SuppressWarnings("SameParameterValue") AddressEntry.Context context) {
return getAddressEntryListAsImmutableList().stream()
.filter(e -> offerId.equals(e.getOfferId()))
.filter(e -> context == e.getContext())
@ -592,17 +602,14 @@ public class BtcWalletService extends WalletService {
return getOrCreateAddressEntry(context, addressEntry);
}
public AddressEntry getNewAddressEntry(String offerId, AddressEntry.Context context) {
public void getNewAddressEntry(String offerId, AddressEntry.Context context) {
AddressEntry entry = new AddressEntry(wallet.freshReceiveKey(), context, offerId);
addressEntryList.addAddressEntry(entry);
return entry;
}
public AddressEntry recoverAddressEntry(String offerId, String address, AddressEntry.Context context) {
var available = findAddressEntry(address, AddressEntry.Context.AVAILABLE);
if (!available.isPresent())
return null;
return addressEntryList.swapAvailableToAddressEntryWithOfferId(available.get(), context, offerId);
public void recoverAddressEntry(String offerId, String address, AddressEntry.Context context) {
findAddressEntry(address, AddressEntry.Context.AVAILABLE).ifPresent(addressEntry ->
addressEntryList.swapAvailableToAddressEntryWithOfferId(addressEntry, context, offerId));
}
private AddressEntry getOrCreateAddressEntry(AddressEntry.Context context, Optional<AddressEntry> addressEntry) {
@ -655,20 +662,18 @@ public class BtcWalletService extends WalletService {
}
public List<AddressEntry> getAddressEntryListAsImmutableList() {
return ImmutableList.copyOf(addressEntryList.getList());
return addressEntryList.getAddressEntriesAsListImmutable();
}
public void swapTradeEntryToAvailableEntry(String offerId, AddressEntry.Context context) {
Optional<AddressEntry> addressEntryOptional = getAddressEntryListAsImmutableList().stream()
getAddressEntryListAsImmutableList().stream()
.filter(e -> offerId.equals(e.getOfferId()))
.filter(e -> context == e.getContext())
.findAny();
addressEntryOptional.ifPresent(e -> {
log.info("swap addressEntry with address {} and offerId {} from context {} to available",
e.getAddressString(), e.getOfferId(), context);
addressEntryList.swapToAvailable(e);
saveAddressEntryList();
});
.forEach(e -> {
log.info("swap addressEntry with address {} and offerId {} from context {} to available",
e.getAddressString(), e.getOfferId(), context);
addressEntryList.swapToAvailable(e);
});
}
public void resetAddressEntriesForOpenOffer(String offerId) {