Skip to content

feat(mempool): resolve inputs from mempool for chained txs#637

Merged
cryptodj413 merged 4 commits intoblinklabs-io:mainfrom
cryptodj413:feat/mempool-support
Mar 1, 2026
Merged

feat(mempool): resolve inputs from mempool for chained txs#637
cryptodj413 merged 4 commits intoblinklabs-io:mainfrom
cryptodj413:feat/mempool-support

Conversation

@cryptodj413
Copy link
Copy Markdown
Contributor

@cryptodj413 cryptodj413 commented Feb 24, 2026

Closes #627


Summary by cubic

Resolves inputs for chained mempool transactions from a per-poll UTxO, falling back to Kupo only for on-chain outputs. Resets Kupo state on start and emits transactions with any inputs that can be resolved, with clear partial-resolution logging.

  • New Features
    • Build a per-poll mempool UTxO and resolve inputs from it first.
    • Fall back to Kupo for on-chain outputs; if KUPO_URL is unset or Kupo is disabled, chained mempool inputs are still resolved.
    • Reset Kupo client/flags on start; join per-input Kupo errors, emit partial results, disable Kupo on unsupported pattern, and handle nil logger safely with clearer logs.

Written for commit 45d4fe0. Summary will update on new commits.

Summary by CodeRabbit

  • Bug Fixes

    • Prioritizes mempool (pending) inputs when resolving transaction inputs so transactions are emitted with any available resolved inputs; logs partial-resolution issues and notes when on-chain input lookup is disabled.
  • Refactor

    • Reworked input-resolution flow to build mempool UTXO state first, fall back to on-chain resolution progressively, aggregate partial errors, and improve startup and logging to reflect resolution modes.

Signed-off-by: cryptodj413 <shinjirohara2@gmail.com>
@cryptodj413 cryptodj413 requested a review from a team as a code owner February 24, 2026 05:09
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 24, 2026

📝 Walkthrough

Walkthrough

The mempool module adds an unexported pollTx type and a buildMempoolUtxo helper to construct an in-poll UTXO map. resolveTransactionInputs now accepts that mempool UTXO map, resolves inputs from mempool first and falls back to Kupo, and can return partial results plus aggregated errors. pollOnce builds the mempool UTXO, uses it to populate ResolvedInputs, and logs partial resolution failures without dropping successfully resolved inputs. strconv was added for UTXO key construction.

Possibly related PRs

  • blinklabs-io/adder PR 622: Adds Kupo-based resolution and Kupo wiring in input/mempool/mempool.go, directly overlapping the input-resolution flow now extended with mempool-first resolution.
  • blinklabs-io/adder PR 555: Makes input-resolution tolerant to Kupo failures and continues emitting events with available inputs, aligning with the new partial-result and error-aggregation behavior.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(mempool): resolve inputs from mempool for chained txs' directly and accurately summarizes the main change: resolving transaction inputs from the mempool to support chained transactions.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 1 file

@cryptodj413
Copy link
Copy Markdown
Contributor Author

cryptodj413 commented Feb 24, 2026

Closes #627

Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@input/mempool/mempool.go`:
- Around line 340-357: The code discards partially-resolved inputs when
resolveTransactionInputs fails on a Kupo fetch; change resolveTransactionInputs
to accumulate and return any successfully resolved inputs even if some Kupo
lookups error (log and skip failing inputs rather than returning nil), and
update the caller loop in mempool.go (the section building ctx/payload and
calling resolveTransactionInputs) to set payload.ResolvedInputs when the
returned slice has entries regardless of a non-fatal error, while still logging
the error; specifically modify resolveTransactionInputs to continue on per-input
Kupo errors (or return (resolvedSlice, err) instead of nil) and adjust the
caller to use resolved inputs when len(resolvedInputs)>0 even if err != nil.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e2394da and 7725444.

📒 Files selected for processing (1)
  • input/mempool/mempool.go

Signed-off-by: cryptodj413 <shinjirohara2@gmail.com>
Copy link
Copy Markdown

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
input/mempool/mempool.go (1)

442-502: ⚠️ Potential issue | 🟠 Major

Pipeline failure: error return is always nil — either drop it or use it.

The linter (unparam) flags that the second return value is always nil, which also makes the caller's if err != nil branch (lines 351-354) dead code. Since the current design intentionally continues past every failure to preserve partial results, the cleanest fix is to remove the error return.

Option 1 (preferred): drop the error return

In resolveTransactionInputs:

-func (m *Mempool) resolveTransactionInputs(tx ledger.Transaction, mempoolUtxo map[string]ledger.TransactionOutput) ([]ledger.TransactionOutput, error) {
+func (m *Mempool) resolveTransactionInputs(tx ledger.Transaction, mempoolUtxo map[string]ledger.TransactionOutput) []ledger.TransactionOutput {
-	return resolvedInputs, nil
+	return resolvedInputs

And in the caller (pollOnce):

-		resolvedInputs, err := m.resolveTransactionInputs(p.tx, mempoolUtxo)
-		if err != nil {
-			if m.logger != nil {
-				m.logger.Warn("failed to resolve transaction inputs, emitting without resolved inputs", "error", err)
-			}
-		} else if len(resolvedInputs) > 0 {
+		resolvedInputs := m.resolveTransactionInputs(p.tx, mempoolUtxo)
+		if len(resolvedInputs) > 0 {
 			payload.ResolvedInputs = resolvedInputs
 		}
Option 2: accumulate and return a joined error for observability

If you want the caller to know that some inputs couldn't be resolved (e.g. for metrics or future use), accumulate errors and return them:

+	var errs []error
 	// ... inside the loop, where you currently `continue` on Kupo failures:
-		continue
+		errs = append(errs, fmt.Errorf("input %s:%d: %w", txID, txIndex, err))
+		continue
 	// ...
-	return resolvedInputs, nil
+	return resolvedInputs, errors.Join(errs...)

The caller would then handle a non-nil error as a partial-failure warning.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@input/mempool/mempool.go` around lines 442 - 502, The function
resolveTransactionInputs currently returns ([]ledger.TransactionOutput, error)
but always returns nil error; change its signature to return only
[]ledger.TransactionOutput and remove all error handling/returns inside it (keep
the logic that continues on Kupo/client failures), then update its callers
(notably pollOnce which checks "if err != nil" around the
resolveTransactionInputs call) to accept the single return value and drop the
dead err handling branch; ensure you update all references to
resolveTransactionInputs and rebuild to fix compile errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@input/mempool/mempool.go`:
- Around line 442-502: The function resolveTransactionInputs currently returns
([]ledger.TransactionOutput, error) but always returns nil error; change its
signature to return only []ledger.TransactionOutput and remove all error
handling/returns inside it (keep the logic that continues on Kupo/client
failures), then update its callers (notably pollOnce which checks "if err !=
nil" around the resolveTransactionInputs call) to accept the single return value
and drop the dead err handling branch; ensure you update all references to
resolveTransactionInputs and rebuild to fix compile errors.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7725444 and bbe5fba.

📒 Files selected for processing (1)
  • input/mempool/mempool.go

Signed-off-by: cryptodj413 <shinjirohara2@gmail.com>
Copy link
Copy Markdown

@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

🧹 Nitpick comments (1)
input/mempool/mempool.go (1)

441-503: Well-structured mempool-first resolution with graceful Kupo fallback.

The refactored method correctly prioritizes the in-poll mempool UTXO map before falling back to Kupo, and the error-accumulation pattern (lines 447, 465-466, 484-485, 490-491, 499-501) ensures partial results are preserved. This is a solid improvement over the previous hard-return-on-error behavior.

One minor observation: when Kupo is not configured and an input isn't found in the mempool UTXO (lines 460-462), the input is silently skipped. This is fine for the "mempool-only" mode since there's nothing else to try, but there's no diagnostic logging for this case. Consider a Debug-level log so operators can distinguish "input not in mempool, no Kupo" from "input resolved from mempool."

💡 Optional: add debug log for unresolvable inputs in mempool-only mode
 		// Fall back to Kupo for on-chain outputs.
 		if m.kupoUrl == "" || m.kupoDisabled {
+			if m.logger != nil {
+				m.logger.Debug("input not in mempool UTXO and Kupo unavailable, skipping", "txId", txID, "index", txIndex)
+			}
 			continue
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@input/mempool/mempool.go` around lines 441 - 503, The method
resolveTransactionInputs silently skips inputs when Kupo is unavailable
(m.kupoUrl == "" || m.kupoDisabled) and the input isn't in mempoolUtxo; add a
Debug log via m.logger (if non-nil) inside that branch to record the skipped
input (include txID, txIndex and key or pattern) so operators can distinguish
"not in mempool" vs resolved cases; update only the branch in
resolveTransactionInputs that currently does "continue" when Kupo is disabled to
emit this debug message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@input/mempool/mempool.go`:
- Around line 114-118: The two logger.Info calls that reference m.kupoUrl are
missing a nil check on m.logger and can cause a nil-pointer panic because New()
doesn't set a default logger; update the code around the m.kupoUrl check to
guard both branches with if m.logger != nil before calling m.logger.Info (i.e.,
ensure any logging that uses m.logger in this block checks m.logger != nil),
keeping the same messages for the empty and non-empty m.kupoUrl branches so
behavior is unchanged when a logger is present.

---

Nitpick comments:
In `@input/mempool/mempool.go`:
- Around line 441-503: The method resolveTransactionInputs silently skips inputs
when Kupo is unavailable (m.kupoUrl == "" || m.kupoDisabled) and the input isn't
in mempoolUtxo; add a Debug log via m.logger (if non-nil) inside that branch to
record the skipped input (include txID, txIndex and key or pattern) so operators
can distinguish "not in mempool" vs resolved cases; update only the branch in
resolveTransactionInputs that currently does "continue" when Kupo is disabled to
emit this debug message.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bbe5fba and c09f89e.

📒 Files selected for processing (1)
  • input/mempool/mempool.go

Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@input/mempool/mempool.go`:
- Around line 111-125: Reset the Kupo-related state at the top of Start(): clear
m.kupoDisabled, m.kupoInvalidPatternLogged and set m.kupoClient = nil (and
re-evaluate m.kupoUrl if needed) so a prior Stop() or previous run that disabled
Kupo doesn't persist across restarts; update the Start() method to explicitly
reinitialize these fields before any logic that checks or initializes the kupo
client.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c09f89e and f057bd6.

📒 Files selected for processing (1)
  • input/mempool/mempool.go

Signed-off-by: cryptodj413 <shinjirohara2@gmail.com>
Copy link
Copy Markdown

@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

🧹 Nitpick comments (1)
input/mempool/mempool.go (1)

447-447: Extract a shared UTxO-key builder to prevent format drift.

The same key format is duplicated in two locations; centralizing it makes future changes safer.

♻️ Suggested refactor
+func mempoolUtxoKey(txID string, index int) string {
+	return txID + ":" + strconv.Itoa(index)
+}
+
 func (m *Mempool) buildMempoolUtxo(pollTxs []pollTx) map[string]ledger.TransactionOutput {
 	utxo := make(map[string]ledger.TransactionOutput)
 	for _, p := range pollTxs {
 		txID := p.hash
 		for idx, out := range p.tx.Outputs() {
-			key := txID + ":" + strconv.Itoa(idx)
+			key := mempoolUtxoKey(txID, idx)
 			utxo[key] = out
 		}
 	}
 	return utxo
 }
@@
-		key := txID + ":" + strconv.Itoa(txIndex)
+		key := mempoolUtxoKey(txID, txIndex)

Also applies to: 464-464

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@input/mempool/mempool.go` at line 447, Extract the UTxO key construction into
a single helper function (e.g., buildUTxOKey or makeUTxOKey) and replace the
inline expressions like txID + ":" + strconv.Itoa(idx) in mempool.go
(occurrences around the current key assignment and the other duplicate at the
second location) with calls to that helper; implement the helper to consistently
format the key (for example using fmt.Sprintf("%s:%d", txID, idx)) and update
any imports if needed so all places use the single function to prevent format
drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@input/mempool/mempool.go`:
- Around line 476-480: The code calls m.getKupoClient() inside the per-input
loop, causing repeated client initialization/health checks when Kupo is down;
move the call out of the loop so you obtain the Kupo client once per transaction
resolution (e.g., call k, err := m.getKupoClient() before iterating inputs),
handle a single error case by appending to resolveErrs and skipping the whole
input-processing loop for that tx, and then reuse the same k variable inside the
loop where resolveErrs currently appends errors (references: m.getKupoClient(),
resolveErrs, txID, txIndex).

---

Nitpick comments:
In `@input/mempool/mempool.go`:
- Line 447: Extract the UTxO key construction into a single helper function
(e.g., buildUTxOKey or makeUTxOKey) and replace the inline expressions like txID
+ ":" + strconv.Itoa(idx) in mempool.go (occurrences around the current key
assignment and the other duplicate at the second location) with calls to that
helper; implement the helper to consistently format the key (for example using
fmt.Sprintf("%s:%d", txID, idx)) and update any imports if needed so all places
use the single function to prevent format drift.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6ec4309 and 45d4fe0.

📒 Files selected for processing (1)
  • input/mempool/mempool.go

Comment on lines +476 to +480
k, err := m.getKupoClient()
if err != nil {
resolveErrs = append(resolveErrs, fmt.Errorf("input %s:%d kupo client: %w", txID, txIndex, err))
continue
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid per-input Kupo client initialization retries.

m.getKupoClient() is called inside the input loop. When Kupo is down, each unresolved input repeats client init/health-check work, amplifying latency and error volume for a single tx.

🔧 Suggested fix (fetch Kupo client once per transaction resolution)
 func (m *Mempool) resolveTransactionInputs(tx ledger.Transaction, mempoolUtxo map[string]ledger.TransactionOutput) ([]ledger.TransactionOutput, error) {
 	var resolvedInputs []ledger.TransactionOutput
 	var resolveErrs []error
+	var kupoClient *kugo.Client
+	var kupoClientErr error
+	kupoChecked := false
 	for _, input := range tx.Inputs() {
 		txID := input.Id().String()
 		txIndex := int(input.Index())
 		key := txID + ":" + strconv.Itoa(txIndex)

 		// Resolve from mempool first (chained txs: both in same poll).
 		if out, ok := mempoolUtxo[key]; ok {
 			resolvedInputs = append(resolvedInputs, out)
 			continue
 		}

 		// Fall back to Kupo for on-chain outputs.
 		if m.kupoUrl == "" || m.kupoDisabled {
 			continue
 		}
-		k, err := m.getKupoClient()
-		if err != nil {
-			resolveErrs = append(resolveErrs, fmt.Errorf("input %s:%d kupo client: %w", txID, txIndex, err))
-			continue
-		}
+		if !kupoChecked {
+			kupoChecked = true
+			kupoClient, kupoClientErr = m.getKupoClient()
+			if kupoClientErr != nil {
+				resolveErrs = append(resolveErrs, fmt.Errorf("kupo client: %w", kupoClientErr))
+			}
+		}
+		if kupoClientErr != nil {
+			continue
+		}
 		pattern := fmt.Sprintf("%d@%s", txIndex, txID)
 		ctx, cancel := context.WithTimeout(context.Background(), defaultKupoTimeout)
-		matches, err := k.Matches(ctx, kugo.Pattern(pattern))
+		matches, err := kupoClient.Matches(ctx, kugo.Pattern(pattern))
 		cancel()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@input/mempool/mempool.go` around lines 476 - 480, The code calls
m.getKupoClient() inside the per-input loop, causing repeated client
initialization/health checks when Kupo is down; move the call out of the loop so
you obtain the Kupo client once per transaction resolution (e.g., call k, err :=
m.getKupoClient() before iterating inputs), handle a single error case by
appending to resolveErrs and skipping the whole input-processing loop for that
tx, and then reuse the same k variable inside the loop where resolveErrs
currently appends errors (references: m.getKupoClient(), resolveErrs, txID,
txIndex).

@cryptodj413 cryptodj413 merged commit 17676cb into blinklabs-io:main Mar 1, 2026
7 checks passed
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.

Mempool support for resolving inputs in mempool input plugin

2 participants