Skip to content

Conversation

@HashEngineering
Copy link
Collaborator

@HashEngineering HashEngineering commented Dec 26, 2025

Issue being fixed or feature implemented

What was done?

How Has This Been Tested?

Breaking Changes

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

Summary by CodeRabbit

  • Bug Fixes

    • Prevented null-pointer issues during shutdown/cleanup and synchronization checks in networking and quorum components.
    • Adjusted coinbase script-length validation to accept a narrower minimum in specific coinbase cases while keeping the existing maximum.
  • New Features

    • Per-wallet management for manual transaction confidence-change notifications.
    • APIs to enable/disable automatic notify-on-next-block, validate transactions, and refresh transaction depths; add/remove manual notify transactions.
  • Tests

    • One wallet test is now skipped (annotated to be ignored).

✏️ Tip: You can customize this high-level summary in your review settings.

@HashEngineering HashEngineering self-assigned this Dec 26, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 26, 2025

📝 Walkthrough

Walkthrough

Defensive null-checks were added to several manager close() methods and a masternode sync guard. Transaction coinbase script-length validation was refactored to compute a dynamic minimum length. Wallet gained per-wallet manual confidence-change tracking, a control flag for block-driven notifications, and APIs to validate/update transaction depths.

Changes

Cohort / File(s) Change Summary
Null-safety guards (resource cleanup & sync checks)
core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java, core/src/main/java/org/bitcoinj/quorums/ChainLocksHandler.java, core/src/main/java/org/bitcoinj/quorums/InstantSendManager.java
Added null checks before removing listeners or invoking handlers in close(). Added null guard in shouldProcessMNListDiff to ensure masternodeSync is non-null before accessing sync flags.
Coinbase validation refactor
core/src/main/java/org/bitcoinj/core/Transaction.java
In verify(), compute coinbase input script length once, introduce scriptMaxLength (default 2, set to 1 for TRANSACTION_COINBASE), and validate script length against [scriptMaxLength, 100] replacing the prior hard-coded lower bound.
Manual confidence-change notifications & depth APIs
core/src/main/java/org/bitcoinj/wallet/Wallet.java
Added manualConfidenceChangeTransactions list and notifyTxOnNextBlock flag with setter/getter. Added validateTransaction(Transaction), updateTransactionDepth(), addManualNotifyConfidenceChangeTransaction(Transaction), and removeManualNotifyConfidenceChangeTransaction(Transaction). notifyNewBestBlock() behavior now branches based on notifyTxOnNextBlock.
Test adjustment
core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java
walletToStringTest() annotated with @Ignore in addition to @Test, causing the test to be skipped.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through code with bright-eyed cheer,
Added guards so stray NPEs fear,
Slimmed coinbase checks to fit the day,
Wallets mark confidence a new way,
A joyful hop — changes here! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: make tx notify on blocks optional and other fixes' partially captures the PR's changes. It directly addresses the main Wallet.java feature (notifyTxOnNextBlock), but uses 'and other fixes' which is vague and doesn't specify the null-check improvements across multiple files or the Transaction coinbase validation refactoring.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/sync-notify

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
core/src/main/java/org/bitcoinj/wallet/Wallet.java (2)

271-271: Clarify and de-duplicate depth-update logic around notifyTxOnNextBlock / manual tx list

The new notifyTxOnNextBlock flag and manual list give good control over which transactions get depth updates on each best block, but there are a few points worth tightening:

  • When notifyTxOnNextBlock is false, most BUILDING transactions will not have their depth incremented in notifyNewBestBlock, and ignoreNextNewBlock entries for those txs are not cleared until the flag is re-enabled. Callers must explicitly keep manualConfidenceChangeTransactions in sync or invoke updateTransactionDepth() to bring all depths back in line with lastBlockSeenHeight. This contract is subtle and should be documented in the new setter/javadoc so future callers understand they’re opting out of automatic depth maintenance.
  • The depth-update logic (BUILDING check, event-horizon clear, confidenceChanged.put(...)) is now duplicated three times:
    • notifyNewBestBlock full-wallet branch
    • notifyNewBestBlock manual-list branch
    • updateTransactionDepth()
      Extracting a small helper like updateDepthForTransactions(Iterable<Transaction> txs, boolean useIncrement) would reduce the risk of future divergence.

Consider adding brief Javadoc for:

  • setNotifyTxOnNextBlock(boolean) describing how it interacts with notifyNewBestBlock, ignoreNextNewBlock, and updateTransactionDepth().
  • updateTransactionDepth() clarifying when callers should use it (e.g. after long sync or when auto notifications are suppressed).

Also applies to: 1869-1875, 2632-2677, 2693-2720


1975-2012: Validate per-transaction consistency vs full-wallet checks – confirm coverage and remove dead comment

Replacing isConsistentOrThrow() in receive(...) with validateTransaction(tx) is a good performance win, but it also changes what invariants are checked:

  • validateTransaction only checks the single tx’s pool membership vs isTxConsistent(...).
  • It no longer verifies global invariants that isConsistentOrThrow() handled (e.g., duplicate tx hashes across pools, overall map sizes).

Because isConsistentOrThrow() is still called in other hot paths (maybeCommitTx, cleanup, reorganize), this is probably fine, but it’d be good to confirm you’re comfortable with not running a full wallet consistency check on every receiveFromBlock/notifyTransactionIsInBlock path anymore, and that tests cover regressions here.

Also, the commented-out call left in place is just noise now:

//        isConsistentOrThrow();
validateTransaction(tx);

It can be safely removed.

Also applies to: 2545-2547

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f04e6f4 and 2b09d9d.

📒 Files selected for processing (5)
  • core/src/main/java/org/bitcoinj/core/Transaction.java
  • core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java
  • core/src/main/java/org/bitcoinj/quorums/ChainLocksHandler.java
  • core/src/main/java/org/bitcoinj/quorums/InstantSendManager.java
  • core/src/main/java/org/bitcoinj/wallet/Wallet.java
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: HashEngineering
Repo: dashpay/dashj PR: 284
File: core/src/main/java/org/bitcoinj/wallet/WalletEx.java:342-429
Timestamp: 2025-08-25T15:26:22.477Z
Learning: In WalletEx.java, the mapOutpointRoundsCache for CoinJoin rounds should only be cleared during blockchain reorganizations via the reorganize() method, not during normal wallet operations like adding transactions or moving transactions between pools. This design choice prioritizes performance for large wallets while ensuring correctness when the blockchain structure changes.
📚 Learning: 2025-08-25T15:26:22.477Z
Learnt from: HashEngineering
Repo: dashpay/dashj PR: 284
File: core/src/main/java/org/bitcoinj/wallet/WalletEx.java:342-429
Timestamp: 2025-08-25T15:26:22.477Z
Learning: In WalletEx.java, the mapOutpointRoundsCache for CoinJoin rounds should only be cleared during blockchain reorganizations via the reorganize() method, not during normal wallet operations like adding transactions or moving transactions between pools. This design choice prioritizes performance for large wallets while ensuring correctness when the blockchain structure changes.

Applied to files:

  • core/src/main/java/org/bitcoinj/wallet/Wallet.java
🧬 Code graph analysis (2)
core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java (1)
core/src/main/java/org/bitcoinj/core/MasternodeSync.java (1)
  • MasternodeSync (30-616)
core/src/main/java/org/bitcoinj/wallet/Wallet.java (1)
core/src/main/java/org/bitcoinj/core/TransactionConfidence.java (1)
  • TransactionConfidence (66-714)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (java)
  • GitHub Check: JAVA 11 OS macOS-latest Gradle
  • GitHub Check: JAVA 11 OS ubuntu-latest Gradle
🔇 Additional comments (4)
core/src/main/java/org/bitcoinj/quorums/ChainLocksHandler.java (1)

133-135: LGTM! Defensive null check prevents NPE during cleanup.

The null check on peerGroup before removing the listener is appropriate and prevents potential NullPointerException if close() is called when peerGroup was never initialized or already set to null.

core/src/main/java/org/bitcoinj/quorums/InstantSendManager.java (1)

148-150: LGTM! Null guard prevents NPE during listener cleanup.

The null check on chainLocksHandler before removing the chain lock listener is appropriate, as chainLocksHandler is conditionally initialized in setBlockChain() (line 130-133) and may be null when close() is called.

core/src/main/java/org/bitcoinj/evolution/SimplifiedMasternodeListManager.java (2)

303-304: LGTM! Null guard prevents NPE when checking sync flags.

The null check on masternodeSync before evaluating sync flags is appropriate. Since masternodeSync is initialized externally via setBlockChain() (line 422), this guard prevents NullPointerException if shouldProcessMNListDiff() is called before initialization.


446-448: LGTM! Defensive null check prevents NPE during cleanup.

The null check on peerGroup before removing the listener is appropriate and consistent with similar cleanup patterns in ChainLocksHandler.java and InstantSendManager.java. This prevents potential NullPointerException during resource cleanup.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
core/src/main/java/org/bitcoinj/wallet/Wallet.java (2)

239-239: Guard manualConfidenceChangeTransactions with lock to avoid races/ConcurrentModificationException

manualConfidenceChangeTransactions is a plain ArrayList written from addManualNotifyConfidenceChangeTransaction / removeManualNotifyConfidenceChangeTransaction without taking lock, but it is iterated in notifyNewBestBlock while the wallet lock is held. If these methods are ever called from a different thread than the block-processing thread, this is a data race and can throw ConcurrentModificationException or yield stale views.

This list should follow the same locking discipline as the rest of the wallet state.

Proposed fix: guard list with wallet lock
-    private final ArrayList<Transaction> manualConfidenceChangeTransactions = Lists.newArrayList();
+    @GuardedBy("lock")
+    private final List<Transaction> manualConfidenceChangeTransactions = Lists.newArrayList();
@@
-    public void addManualNotifyConfidenceChangeTransaction(Transaction tx) {
-        manualConfidenceChangeTransactions.add(tx);
-    }
+    public void addManualNotifyConfidenceChangeTransaction(Transaction tx) {
+        lock.lock();
+        try {
+            manualConfidenceChangeTransactions.add(tx);
+        } finally {
+            lock.unlock();
+        }
+    }
@@
-    public void removeManualNotifyConfidenceChangeTransaction(Transaction tx) {
-        manualConfidenceChangeTransactions.remove(tx);
-    }
+    public void removeManualNotifyConfidenceChangeTransaction(Transaction tx) {
+        lock.lock();
+        try {
+            manualConfidenceChangeTransactions.remove(tx);
+        } finally {
+            lock.unlock();
+        }
+    }

Also applies to: 2633-2676, 6585-6591


2694-2721: Fix off‑by‑one and negative depth risk in updateTransactionDepth

updateTransactionDepth() currently sets:

confidence.setDepthInBlocks(lastBlockSeenHeight - confidence.getAppearedAtChainHeight());

This yields depth 0 when a tx has just appeared in the block at appearedHeight == lastBlockSeenHeight, and is one less than expected for existing BUILDING transactions. It can also go negative if lastBlockSeenHeight is behind appearedAtChainHeight. Depth semantics elsewhere treat the appearance height as depth 1.

Align this with the usual formula and guard against out-of-order heights.

Proposed fix for depth calculation
-                    confidence.setDepthInBlocks(lastBlockSeenHeight - confidence.getAppearedAtChainHeight());
+                    int appearedHeight = confidence.getAppearedAtChainHeight();
+                    if (lastBlockSeenHeight >= appearedHeight) {
+                        confidence.setDepthInBlocks(lastBlockSeenHeight - appearedHeight + 1);
+                    }
                     if (confidence.getDepthInBlocks() > context.getEventHorizon())
                         confidence.clearBroadcastBy();
🧹 Nitpick comments (2)
core/src/main/java/org/bitcoinj/wallet/Wallet.java (2)

1975-2013: Make validateTransaction operate on the canonical wallet transaction instance

validateTransaction determines pool membership by txHash but runs isTxConsistent(...) on the Transaction instance passed in. If a caller provides a logically equivalent Transaction that is not the same instance as the one stored in the pools, the spent/unspent wiring (getOutputs(), getSpentBy(), isAvailableForSpending()) may not reflect actual wallet state, leading to spurious IllegalStateExceptions or missed inconsistencies.

To make this helper robust and self-contained, resolve the canonical transaction from the appropriate pool map before validating, and fall back early when the hash is not tracked by the wallet.

Proposed refactor to use canonical pool instance
     public boolean validateTransaction(Transaction tx) {
         lock.lock();
         try {
-            Sha256Hash txHash = tx.getTxId();
-
-            // Determine which pool the transaction is in and validate accordingly
-            if (spent.containsKey(txHash)) {
-                if (!isTxConsistent(tx, true)) {
-                    throw new IllegalStateException("Inconsistent spent tx: " + txHash);
-                }
-            } else if (unspent.containsKey(txHash)) {
-                if (!isTxConsistent(tx, false)) {
-                    throw new IllegalStateException("Inconsistent unspent tx: " + txHash);
-                }
-            } else if (pending.containsKey(txHash) || dead.containsKey(txHash)) {
-                // For pending/dead transactions, check as unspent
-                if (!isTxConsistent(tx, false)) {
-                    throw new IllegalStateException("Inconsistent pending/dead tx: " +
-                            txHash);
-                }
-            }
-            // If not in any pool, nothing to validate
-
-            return true;
+            Sha256Hash txHash = tx.getTxId();
+
+            Transaction walletTx = spent.get(txHash);
+            if (walletTx != null) {
+                if (!isTxConsistent(walletTx, true)) {
+                    throw new IllegalStateException("Inconsistent spent tx: " + txHash);
+                }
+                return true;
+            }
+
+            walletTx = unspent.get(txHash);
+            if (walletTx != null) {
+                if (!isTxConsistent(walletTx, false)) {
+                    throw new IllegalStateException("Inconsistent unspent tx: " + txHash);
+                }
+                return true;
+            }
+
+            walletTx = pending.get(txHash);
+            if (walletTx != null) {
+                if (!isTxConsistent(walletTx, false)) {
+                    throw new IllegalStateException("Inconsistent pending tx: " + txHash);
+                }
+                return true;
+            }
+
+            walletTx = dead.get(txHash);
+            if (walletTx != null) {
+                if (!isTxConsistent(walletTx, false)) {
+                    throw new IllegalStateException("Inconsistent dead tx: " + txHash);
+                }
+                return true;
+            }
+
+            // If not in any pool, nothing to validate against this wallet
+            return true;
         } finally {
             lock.unlock();
         }
     }

2547-2547: Avoid leaving commented-out validateTransaction(tx) call

The commented-out // validateTransaction(tx); in receive(...) is easy to forget and can become stale. Either remove it entirely or replace it with a more explicit TODO referencing the failing test or issue so future maintainers know why it’s intentionally disabled.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2b09d9d and 2984148.

📒 Files selected for processing (1)
  • core/src/main/java/org/bitcoinj/wallet/Wallet.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-25T15:26:22.477Z
Learnt from: HashEngineering
Repo: dashpay/dashj PR: 284
File: core/src/main/java/org/bitcoinj/wallet/WalletEx.java:342-429
Timestamp: 2025-08-25T15:26:22.477Z
Learning: In WalletEx.java, the mapOutpointRoundsCache for CoinJoin rounds should only be cleared during blockchain reorganizations via the reorganize() method, not during normal wallet operations like adding transactions or moving transactions between pools. This design choice prioritizes performance for large wallets while ensuring correctness when the blockchain structure changes.

Applied to files:

  • core/src/main/java/org/bitcoinj/wallet/Wallet.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (java)
  • GitHub Check: JAVA 11 OS ubuntu-latest Gradle
  • GitHub Check: JAVA 11 OS macOS-latest Gradle
🔇 Additional comments (1)
core/src/main/java/org/bitcoinj/wallet/Wallet.java (1)

271-271: notifyTxOnNextBlock flag and accessors look correct

The new notifyTxOnNextBlock toggle defaults to true, preserving existing behavior, and the simple getter/setter cleanly expose control over whether notifyNewBestBlock walks all transactions or only the manual list. No issues with this change as written.

Also applies to: 1869-1875

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
core/src/main/java/org/bitcoinj/wallet/Wallet.java (2)

239-239: Manual confidence-change tracking now correctly locked; consider Set + @GuardedBy for stronger invariants

The race/ConcurrentModificationException risk around manualConfidenceChangeTransactions is addressed by taking lock in addManualNotifyConfidenceChangeTransaction/removeManualNotifyConfidenceChangeTransaction, and by iterating under lock in notifyNewBestBlock(). That aligns this state with the rest of the wallet’s locking model.

Two small robustness improvements you may want to fold in:

  • Mark the field as guarded by lock to document the invariant and help static analysis:
    • @GuardedBy("lock") private final … manualConfidenceChangeTransactions
  • Use a Set<Transaction> instead of a List to avoid accidental duplicate registrations, which would currently double-increment depth per block for that tx when notifyTxOnNextBlock == false.
Proposed refactor for `manualConfidenceChangeTransactions`
-    private final ArrayList<Transaction> manualConfidenceChangeTransactions = Lists.newArrayList();
+    @GuardedBy("lock")
+    private final Set<Transaction> manualConfidenceChangeTransactions = Sets.newHashSet();
@@
-            manualConfidenceChangeTransactions.add(tx);
+            manualConfidenceChangeTransactions.add(tx);
@@
-            manualConfidenceChangeTransactions.remove(tx);
+            manualConfidenceChangeTransactions.remove(tx);

Also applies to: 2633-2675, 6585-6602


2694-2721: Depth recomputation fix is correct; optional defensive guard on heights

updateTransactionDepth() now uses:

confidence.setDepthInBlocks(lastBlockSeenHeight - confidence.getAppearedAtChainHeight() + 1);

which matches the setAppearedAtChainHeight() semantics (depth 1 at appearance height) and fixes the previous off‑by‑one.

To make this more robust against any pathological state (e.g., if lastBlockSeenHeight were ever behind appearedAtChainHeight), you might add a simple guard:

-                    confidence.setDepthInBlocks(lastBlockSeenHeight - confidence.getAppearedAtChainHeight() + 1);
+                    int appeared = confidence.getAppearedAtChainHeight();
+                    if (lastBlockSeenHeight >= appeared && appeared > 0) {
+                        confidence.setDepthInBlocks(lastBlockSeenHeight - appeared + 1);
+                    }

Functionally, though, the current change resolves the main correctness issue.

🧹 Nitpick comments (1)
core/src/main/java/org/bitcoinj/wallet/Wallet.java (1)

271-271: notifyTxOnNextBlock flagging looks good; consider memory-visibility tightening

The new notifyTxOnNextBlock flag and branching in notifyNewBestBlock() behave as intended: existing users retain the original “update all BUILDING txs per block” path, and callers can opt into manual per-tx updates via manualConfidenceChangeTransactions.

If setNotifyTxOnNextBlock(boolean) can be called from arbitrary threads while block processing is running, you may want to either:

  • Guard the flag with lock in both setter and reads, or
  • Declare it volatile

to guarantee timely visibility across threads. The rest of the logic is fine as-is.

Also applies to: 1869-1875, 2633-2675

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2984148 and 92b277d.

📒 Files selected for processing (1)
  • core/src/main/java/org/bitcoinj/wallet/Wallet.java
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: HashEngineering
Repo: dashpay/dashj PR: 288
File: core/src/main/java/org/bitcoinj/core/Transaction.java:1572-1577
Timestamp: 2025-12-26T19:03:14.265Z
Learning: In Dash, TRANSACTION_COINBASE (type 5, CbTx) coinbase transactions require only 1 byte minimum scriptSig length (not 2), because the block height is encoded in the CbTx special payload rather than the coinbase scriptSig. Regular coinbase transactions still require 2–100 bytes.
📚 Learning: 2025-08-25T15:26:22.477Z
Learnt from: HashEngineering
Repo: dashpay/dashj PR: 284
File: core/src/main/java/org/bitcoinj/wallet/WalletEx.java:342-429
Timestamp: 2025-08-25T15:26:22.477Z
Learning: In WalletEx.java, the mapOutpointRoundsCache for CoinJoin rounds should only be cleared during blockchain reorganizations via the reorganize() method, not during normal wallet operations like adding transactions or moving transactions between pools. This design choice prioritizes performance for large wallets while ensuring correctness when the blockchain structure changes.

Applied to files:

  • core/src/main/java/org/bitcoinj/wallet/Wallet.java
🧬 Code graph analysis (1)
core/src/main/java/org/bitcoinj/wallet/Wallet.java (1)
core/src/main/java/org/bitcoinj/core/TransactionConfidence.java (1)
  • TransactionConfidence (66-714)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: JAVA 11 OS macOS-latest Gradle
  • GitHub Check: Analyze (java)
  • GitHub Check: JAVA 11 OS ubuntu-latest Gradle

Comment on lines +1975 to +2013
/**
* Validates that a single transaction is internally consistent within the wallet.
* This is a lightweight alternative to {@link #isConsistentOrThrow()} for cases where
* the wallet is already known to be consistent and we only need to verify one
transaction.
*
* @param tx The transaction to validate
* @return true if the transaction is consistent with its current pool state
* @throws IllegalStateException if the transaction is in an inconsistent state
*/
// TODO: a test fails when using this in receive()
public boolean validateTransaction(Transaction tx) {
lock.lock();
try {
Sha256Hash txHash = tx.getTxId();

// Determine which pool the transaction is in and validate accordingly
if (spent.containsKey(txHash)) {
if (!isTxConsistent(tx, true)) {
throw new IllegalStateException("Inconsistent spent tx: " + txHash);
}
} else if (unspent.containsKey(txHash)) {
if (!isTxConsistent(tx, false)) {
throw new IllegalStateException("Inconsistent unspent tx: " + txHash);
}
} else if (pending.containsKey(txHash) || dead.containsKey(txHash)) {
// For pending/dead transactions, check as unspent
if (!isTxConsistent(tx, false)) {
throw new IllegalStateException("Inconsistent pending/dead tx: " +
txHash);
}
}
// If not in any pool, nothing to validate

return true;
} finally {
lock.unlock();
}
}
Copy link

@coderabbitai coderabbitai bot Dec 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

validateTransaction misclassifies some pending/dead txs; either relax or narrow its scope

validateTransaction() currently treats any tx found in pending or dead as “unspent” by calling:

} else if (pending.containsKey(txHash) || dead.containsKey(txHash)) {
    // For pending/dead transactions, check as unspent
    if (!isTxConsistent(tx, false)) {
        throw new IllegalStateException("Inconsistent pending/dead tx: " + txHash);
    }
}

For pending transactions that are pure spends (no wallet-owned outputs), isTxConsistent(tx, false) will return false even though the tx state is perfectly valid, which matches the TODO comment about a test failing when using this from receive().

Given this method is public, throwing in such normal cases is confusing for callers. Two concrete options:

  1. Restrict it to SPENT/UNSPENT pools only (simplest and safest now):
-        } else if (pending.containsKey(txHash) || dead.containsKey(txHash)) {
-            // For pending/dead transactions, check as unspent
-            if (!isTxConsistent(tx, false)) {
-                throw new IllegalStateException("Inconsistent pending/dead tx: " + txHash);
-            }
-        }
+        }
+        // For PENDING/DEAD we currently skip pool-shape checks and rely on
+        // full-wallet validation via isConsistentOrThrow().
  1. Or keep validating PENDING/DEAD but base the isSpent expectation on actual owned-output state, not pool name:
-        } else if (pending.containsKey(txHash) || dead.containsKey(txHash)) {
-            // For pending/dead transactions, check as unspent
-            if (!isTxConsistent(tx, false)) {
-                throw new IllegalStateException("Inconsistent pending/dead tx: " +
-                        txHash);
-            }
-        }
+        } else if (pending.containsKey(txHash) || dead.containsKey(txHash)) {
+            // For pending/dead, only assert internal spent/unspent invariants,
+            // not a specific pool shape.
+            boolean expectedSpent = tx.isEveryOwnedOutputSpent(this);
+            if (!isTxConsistent(tx, expectedSpent)) {
+                throw new IllegalStateException("Inconsistent pending/dead tx: " + txHash);
+            }
+        }

In either case, until this API is fully exercised, keeping the call in receive() commented out (as it is now) is reasonable.

Also applies to: 2547-2547

🤖 Prompt for AI Agents
In core/src/main/java/org/bitcoinj/wallet/Wallet.java around lines 1975 to 2013,
validateTransaction incorrectly treats transactions in pending or dead as
unspent which causes false failures for pending pure-spend transactions; change
the logic to only validate transactions that are in the spent or unspent pools
(i.e., remove or skip the pending||dead branch), so pending/dead txs are not
validated here, and update the nearby TODO/comment to reflect this narrowed
scope.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will address this in the next PR

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HashEngineering Sounds good—thanks for the heads‑up. Would you like me to open a follow‑up GitHub issue to track this for the next PR? I can include:

  • Decision and change: either restrict validateTransaction() to SPENT/UNSPENT or derive expectedSpent for PENDING/DEAD.
  • Javadoc/TODO update to reflect scope.
  • Tests: pending pure-spend, mixed, dead, and SPENT/UNSPENT cases.
  • Call site review (receive()): keep commented or re-enable once fixed.

If you say yes, I’ll open the issue referencing PR #288 and this discussion, and assign it to @HashEngineering.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java (1)

99-99: Track this ignored test for future resolution.

Ignoring the test is a reasonable short-term fix for the OutOfMemoryError. However, ensure this is tracked so the underlying issue can be addressed later—either by optimizing the toString() implementation for large wallets, increasing the test's heap allocation, or redesigning the test approach.

Would you like me to open a GitHub issue to track this test failure, or help investigate potential fixes such as JVM configuration for this specific test or memory optimization in the toString() methods?

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 92b277d and b84056c.

📒 Files selected for processing (1)
  • core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: HashEngineering
Repo: dashpay/dashj PR: 288
File: core/src/main/java/org/bitcoinj/core/Transaction.java:1572-1577
Timestamp: 2025-12-26T19:03:14.265Z
Learning: In Dash, TRANSACTION_COINBASE (type 5, CbTx) coinbase transactions require only 1 byte minimum scriptSig length (not 2), because the block height is encoded in the CbTx special payload rather than the coinbase scriptSig. Regular coinbase transactions still require 2–100 bytes.
📚 Learning: 2025-08-25T15:26:22.477Z
Learnt from: HashEngineering
Repo: dashpay/dashj PR: 284
File: core/src/main/java/org/bitcoinj/wallet/WalletEx.java:342-429
Timestamp: 2025-08-25T15:26:22.477Z
Learning: In WalletEx.java, the mapOutpointRoundsCache for CoinJoin rounds should only be cleared during blockchain reorganizations via the reorganize() method, not during normal wallet operations like adding transactions or moving transactions between pools. This design choice prioritizes performance for large wallets while ensuring correctness when the blockchain structure changes.

Applied to files:

  • core/src/test/java/org/bitcoinj/wallet/LargeCoinJoinWalletTest.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: JAVA 11 OS macOS-latest Gradle
  • GitHub Check: Analyze (java)
  • GitHub Check: JAVA 11 OS ubuntu-latest Gradle

extension.processTransaction(tx, block, blockType);
}
isConsistentOrThrow();
// validateTransaction(tx);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HashEngineering you introduced validateTransaction, but it's never used. Is it needed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants