p : removeISLocks.entrySet()) {
- updateWalletTransaction(p.getValue().txid, null);
+ updateWalletTransaction(p.getValue().txid, p.getValue());
}
}
static final String INPUTLOCK_REQUESTID_PREFIX = "inlock";
@@ -947,13 +911,11 @@ Sha256Hash getRequestId(TransactionOutPoint outpoint) {
}
}
- void removeMempoolConflictsForLock(Sha256Hash hash, InstantSendLock islock)
- {
+ void removeMempoolConflictsForLock(Sha256Hash hash, InstantSendLock islock) {
//TODO: should full verification mode have this?
}
- public InstantSendLock getInstantSendLockByHash(Sha256Hash hash)
- {
+ public InstantSendLock getInstantSendLockByHash(Sha256Hash hash) {
if (!isInstantSendEnabled()) {
return null;
}
@@ -967,8 +929,7 @@ public InstantSendLock getInstantSendLockByHash(Sha256Hash hash)
}
}
- public InstantSendLock getInstantSendLockByTxId(Sha256Hash hash)
- {
+ public InstantSendLock getInstantSendLockByTxId(Sha256Hash hash) {
if (!isInstantSendEnabled()) {
return null;
}
@@ -1000,8 +961,7 @@ public InstantSendLock getInstantSendLockByTxId(Sha256Hash hash)
}
}
- boolean isLocked(Sha256Hash txHash)
- {
+ boolean isLocked(Sha256Hash txHash) {
if (!isInstantSendEnabled()) {
return false;
}
@@ -1023,8 +983,7 @@ public boolean isConflicted(Transaction tx) {
}
}
- public Sha256Hash getConflictingTx(Transaction tx)
- {
+ public Sha256Hash getConflictingTx(Transaction tx) {
if (!isInstantSendEnabled()) {
return null;
}
@@ -1077,19 +1036,29 @@ public boolean notifyTransactionIsInBlock(Sha256Hash txHash, StoredBlock block,
}
};
- //TODO: finish connecting this. Should we keep a list of transactions sent and received in spv mode?
- OnTransactionBroadcastListener transactionBroadcastListener = new OnTransactionBroadcastListener() {
- @Override
- public void onTransaction(Peer peer, Transaction t) {
- syncTransaction(t, null, -1);
- }
- };
+ OnTransactionBroadcastListener transactionBroadcastListener = (peer, t) -> syncTransaction(t, null, -1);
WalletCoinsSentEventListener coinsSentEventListener = (wallet, tx, prevBalance, newBalance) -> syncTransaction(tx, null, -1);
public void addWallet(Wallet wallet) {
wallet.addCoinsSentEventListener(coinsSentEventListener);
wallets.add(wallet);
+ // Backfill any ISLocks that were verified before this wallet was attached
+ for (Transaction tx : wallet.getTransactions(false)) {
+ if (tx.getConfidence().getIXType() == TransactionConfidence.IXType.IX_LOCKED) {
+ continue;
+ }
+ Sha256Hash islockHash = db.getInstantSendLockHashByTxid(tx.getTxId());
+ if (islockHash != null && !islockHash.isZero()) {
+ InstantSendLock islock = db.getInstantSendLockByHash(islockHash);
+ if (islock != null) {
+ TransactionConfidence confidence = tx.getConfidence();
+ confidence.setInstantSendLock(islock);
+ confidence.setIXType(TransactionConfidence.IXType.IX_LOCKED);
+ confidence.queueListeners(TransactionConfidence.Listener.ChangeReason.IX_TYPE);
+ }
+ }
+ }
}
public void removeWallet(Wallet wallet) {
diff --git a/core/src/main/java/org/bitcoinj/quorums/LLMQBackgroundThread.java b/core/src/main/java/org/bitcoinj/quorums/LLMQBackgroundThread.java
index 67eb2e3b4..0943b4f46 100644
--- a/core/src/main/java/org/bitcoinj/quorums/LLMQBackgroundThread.java
+++ b/core/src/main/java/org/bitcoinj/quorums/LLMQBackgroundThread.java
@@ -34,6 +34,7 @@ public LLMQBackgroundThread(Context context, InstantSendManager instantSendManag
this.chainLocksHandler = chainLocksHandler;
this.signingManager = signingManager;
this.masternodeListManager = masternodeListManager;
+ this.setName("LLMQBackgroundThread");
}
int debugTimer = 0;
@@ -73,11 +74,16 @@ public void run() {
}
}
- } catch (BlockStoreException x) {
- log.info("stopping LLMQBackgroundThread via BlockStoreException");
+ } catch (RuntimeException x) {
+ if (x.getCause() instanceof BlockStoreException) {
+ log.info("stopping LLMQBackgroundThread via BlockStoreException");
+ } else {
+ log.error("stopping LLMQBackgroundThread via RuntimeException", x);
+ }
} catch (InterruptedException x) {
log.info("stopping LLMQBackgroundThread via InterruptedException");
//let the thread stop
+ Thread.currentThread().interrupt();
} finally {
signingManager.removeRecoveredSignatureListener(instantSendManager);
signingManager.removeRecoveredSignatureListener(chainLocksHandler);
diff --git a/core/src/main/java/org/bitcoinj/wallet/DefaultRiskAnalysis.java b/core/src/main/java/org/bitcoinj/wallet/DefaultRiskAnalysis.java
index c633583fa..f5b9666cc 100644
--- a/core/src/main/java/org/bitcoinj/wallet/DefaultRiskAnalysis.java
+++ b/core/src/main/java/org/bitcoinj/wallet/DefaultRiskAnalysis.java
@@ -28,6 +28,7 @@
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.script.ScriptChunk;
+import org.bitcoinj.script.ScriptPattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -170,6 +171,9 @@ public static RuleViolation isStandard(Transaction tx) {
* Checks the output to see if the script violates a standardness rule. Not complete.
*/
public static RuleViolation isOutputStandard(TransactionOutput output) {
+ // OP_RETURN outputs are unspendable data-carrier outputs; zero value is standard for them.
+ if (ScriptPattern.isOpReturn(output.getScriptPubKey()))
+ return RuleViolation.NONE;
if (output.getValue().compareTo(MIN_ANALYSIS_NONDUST_OUTPUT) < 0)
return RuleViolation.DUST;
for (ScriptChunk chunk : output.getScriptPubKey().getChunks()) {
diff --git a/core/src/main/java/org/bitcoinj/wallet/WalletProtobufSerializer.java b/core/src/main/java/org/bitcoinj/wallet/WalletProtobufSerializer.java
index 99d77d1be..0839be7ff 100644
--- a/core/src/main/java/org/bitcoinj/wallet/WalletProtobufSerializer.java
+++ b/core/src/main/java/org/bitcoinj/wallet/WalletProtobufSerializer.java
@@ -56,6 +56,8 @@
import java.net.UnknownHostException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -100,6 +102,7 @@ public class WalletProtobufSerializer {
private boolean requireAllExtensionsKnown = false;
private int walletWriteBufferSize = CodedOutputStream.DEFAULT_BUFFER_SIZE;
private boolean useAdaptiveBufferSizing = true;
+ private boolean parallelLoad = true;
public interface WalletFactory {
Wallet create(NetworkParameters params, KeyChainGroup keyChainGroup);
@@ -107,6 +110,7 @@ public interface WalletFactory {
private final WalletFactory factory;
private KeyChainFactory keyChainFactory;
+ private final Object factoryLock = new Object();
private volatile boolean lastSaveParallel = false;
public WalletProtobufSerializer() {
@@ -125,7 +129,9 @@ public WalletProtobufSerializer(WalletFactory factory) {
}
public void setKeyChainFactory(KeyChainFactory keyChainFactory) {
- this.keyChainFactory = keyChainFactory;
+ synchronized (factoryLock) {
+ this.keyChainFactory = keyChainFactory;
+ }
}
/**
@@ -162,6 +168,21 @@ public void setUseAdaptiveBufferSizing(boolean useAdaptiveBufferSizing) {
this.useAdaptiveBufferSizing = useAdaptiveBufferSizing;
}
+ /**
+ * Enable or disable parallel loading of large wallets. When enabled, key chain parsing and
+ * transaction deserialization are performed concurrently across multiple threads.
+ * Disable this if benchmarking or if parallel loading is slower on the target device.
+ * Default is true.
+ * @param parallelLoad true to enable parallel loading, false to use single-threaded loading
+ */
+ public void setParallelLoad(boolean parallelLoad) {
+ this.parallelLoad = parallelLoad;
+ }
+
+ public boolean isParallelLoad() {
+ return parallelLoad;
+ }
+
/**
* Calculate optimal buffer size based on wallet characteristics.
* Small wallets: 8KB, Medium wallets: 16KB, Large wallets: 64KB+
@@ -197,6 +218,16 @@ private static int getComplexityScore(Wallet wallet) {
return transactionCount + (watchedScriptCount * 2) + keyCount;
}
+ private static int getProtoComplexityScore(Protos.Wallet walletProto) {
+ int score = walletProto.getTransactionCount() + walletProto.getKeyCount()
+ + walletProto.getKeysForFriendsCount() + walletProto.getKeysFromFriendsCount();
+
+ for (int i = 0; i < walletProto.getExtensionCount(); ++i) {
+ score += walletProto.getExtension(i).getData().size() / 200;
+ }
+ return score;
+ }
+
/**
* Formats the given wallet (transactions and keys) to the given output stream in protocol buffer format.
*
@@ -897,114 +928,391 @@ public Wallet readWallet(NetworkParameters params, @Nullable WalletExtension[] e
if (!walletProto.getNetworkIdentifier().equals(params.getId()))
throw new UnreadableWalletException.WrongNetwork();
- // Read the scrypt parameters that specify how encryption and decryption is performed.
- KeyChainGroup keyChainGroup;
- if (walletProto.hasEncryptionParameters()) {
- Protos.ScryptParameters encryptionParameters = walletProto.getEncryptionParameters();
- final KeyCrypterScrypt keyCrypter = new KeyCrypterScrypt(encryptionParameters);
- keyChainGroup = KeyChainGroup.fromProtobufEncrypted(params, walletProto.getKeyList(), keyCrypter, keyChainFactory);
- } else {
- keyChainGroup = KeyChainGroup.fromProtobufUnencrypted(params, walletProto.getKeyList(), keyChainFactory);
+ // Snapshot the factory once under the lock so concurrent setKeyChainFactory calls
+ // cannot affect an in-flight parse (especially important for the parallel path).
+ final KeyChainFactory effectiveFactory;
+ synchronized (factoryLock) {
+ effectiveFactory = this.keyChainFactory;
}
- Wallet wallet = factory.create(params, keyChainGroup);
- List