Skip to content

feat(mempool): kupo support for resolving inputs in mempool input plugin#622

Merged
cryptodj413 merged 2 commits intoblinklabs-io:mainfrom
cryptodj413:feat/kupo-support-in-mempool
Feb 20, 2026
Merged

feat(mempool): kupo support for resolving inputs in mempool input plugin#622
cryptodj413 merged 2 commits intoblinklabs-io:mainfrom
cryptodj413:feat/kupo-support-in-mempool

Conversation

@cryptodj413
Copy link
Copy Markdown
Contributor

@cryptodj413 cryptodj413 commented Feb 17, 2026

Closes #497


Summary by cubic

Adds Kupo support to resolve mempool transaction inputs and include them in transaction events. Adds a --kupo-url (KUPO_URL) option with health checks, timeouts, and graceful fallback when Kupo is unavailable or patterns aren’t supported.

  • New Features

    • Resolve inputs via Kupo when --kupo-url or KUPO_URL is set; emits ResolvedInputs in TransactionEvent.
    • Startup health check and request timeouts; auto-disable resolution if Kupo is unreachable or rejects the output-reference pattern.
  • Bug Fixes

    • Omit non-JSON-serializable Witnesses when marshaling TransactionEvent to prevent logging/serialization errors.
    • Decode asset names from hex in Kupo matches so JSON includes the correct UTF-8 name and nameHex.

Written for commit 6137c31. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Kupo-based transaction input resolution with health checks, timeouts, and graceful fallback when unavailable.
  • Configuration

    • Added kupo-url command-line option and WithKupoUrl() configuration method to enable Kupo integration (optional, disabled by default).
  • Improvements

    • Transaction events now omit non-serializable fields from JSON output.
    • Asset names parsed from hex are decoded and represented more accurately in outputs.

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

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

Adds Kupo-based input resolution to the mempool input plugin, including new Mempool fields (kupoUrl, kupoClient, kupoDisabled, kupoInvalidPatternLogged), getKupoClient and resolveTransactionInputs implementations, CLI/config wiring via WithKupoUrl and a new kupo-url flag, and polling logic to attach ResolvedInputs when available. Also adds TransactionEvent.MarshalJSON to omit non-serializable Witnesses during JSON encoding, and changes asset-name handling to attempt hex decoding before constructing CBOR ByteStrings.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ❓ Inconclusive All changes are directly related to implementing Kupo support. However, bug fixes for JSON marshaling of TransactionEvent and asset name decoding appear tangential to the core Kupo feature. Clarify whether the bug fixes (Witnesses field omission and asset name hex decoding) are essential blockers for Kupo integration or should be separated into distinct PRs.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main feature being added: Kupo support for resolving inputs in the mempool input plugin.
Linked Issues check ✅ Passed All code changes directly implement the objective from issue #497 to support optional resolution of transaction inputs using an external Kupo service.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 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 5 files

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.

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

79-84: Go naming convention: exported acronyms should be all-caps.

Per Go conventions, acronyms in exported identifiers should be fully capitalized — WithKupoURL rather than WithKupoUrl. The same applies to the kupo-url CLI option's wiring in plugin.go. The unexported field kupoUrl is fine as-is.

Proposed rename
-// WithKupoUrl specifies the Kupo API URL for resolving transaction inputs (e.g. http://localhost:1442).
-func WithKupoUrl(kupoUrl string) MempoolOptionFunc {
+// WithKupoURL specifies the Kupo API URL for resolving transaction inputs (e.g. http://localhost:1442).
+func WithKupoURL(kupoUrl string) MempoolOptionFunc {
 	return func(m *Mempool) {
 		m.kupoUrl = kupoUrl
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@input/mempool/options.go` around lines 79 - 84, Rename the exported option
function WithKupoUrl to WithKupoURL and update all call sites and plugin wiring
that reference it (including the CLI wiring in plugin.go that currently uses the
`kupo-url` flag) so they call WithKupoURL instead; leave the unexported field
kupoUrl unchanged, run go vet/gofmt to ensure exports are consistent, and update
any tests or imports that referenced WithKupoUrl to the new WithKupoURL
identifier.
input/mempool/mempool.go (5)

427-462: Kupo queries are not cancellable on shutdown — potential shutdown delay.

Each resolveTransactionInputs call creates contexts from context.Background(). If Stop() is called mid-resolution, the in-flight HTTP calls to Kupo will block for up to defaultKupoTimeout (30s) per input before the poll goroutine can exit. For transactions with many inputs, this could significantly delay graceful shutdown.

Consider deriving the context from a parent that is cancelled when m.doneChan is closed.

Sketch: pass a cancellable context

One approach is to store a context.CancelFunc alongside doneChan so that all in-flight work is cancelled on Stop():

-func (m *Mempool) resolveTransactionInputs(tx ledger.Transaction) ([]ledger.TransactionOutput, error) {
+func (m *Mempool) resolveTransactionInputs(ctx context.Context, tx ledger.Transaction) ([]ledger.TransactionOutput, error) {
 	var resolvedInputs []ledger.TransactionOutput
 	k, err := m.getKupoClient()
 	if err != nil {
 		return nil, err
 	}
 	for _, input := range tx.Inputs() {
 		txID := input.Id().String()
 		txIndex := int(input.Index())
 		pattern := fmt.Sprintf("%d@%s", txIndex, txID)
-		ctx, cancel := context.WithTimeout(context.Background(), defaultKupoTimeout)
+		queryCtx, cancel := context.WithTimeout(ctx, defaultKupoTimeout)
-		matches, err := k.Matches(ctx, kugo.Pattern(pattern))
+		matches, err := k.Matches(queryCtx, kugo.Pattern(pattern))
 		cancel()

Then in pollOnce, derive ctx from m.doneChan (e.g., via a helper or a stored cancel context).

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

In `@input/mempool/mempool.go` around lines 427 - 462, The
resolveTransactionInputs code uses context.Background() for each Kupo query
which prevents shutdown from cancelling in-flight requests; change it to derive
per-request contexts from a cancellable parent context stored on the mempool
struct (e.g., m.ctx/m.cancel) that Stop() cancels: add a context.Context and
context.CancelFunc fields to the mempool, initialize them when starting/polling,
call m.cancel() in Stop(), and replace context.WithTimeout(context.Background(),
defaultKupoTimeout) with context.WithTimeout(m.ctx, defaultKupoTimeout) so
k.Matches(ctx, ...) is cancelled on shutdown; keep existing timeout/error
handling and behavior for m.kupoDisabled and logging intact.

373-376: Cached Kupo client is never invalidated on connection loss.

getKupoClient caches the client after the first successful health check. If Kupo becomes unreachable later, match queries will fail repeatedly but the health check won't re-run (the client is already cached). This is acceptable because resolveTransactionInputs handles errors gracefully, but be aware that the health check only validates initial reachability.

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

In `@input/mempool/mempool.go` around lines 373 - 376, getKupoClient currently
returns a cached m.kupoClient without re-validating connectivity; update
getKupoClient to perform a lightweight health/ping check on the cached client
and if that check fails clear m.kupoClient (set to nil) and return an error so a
fresh client will be created on the next call. Specifically, in getKupoClient
check the existing m.kupoClient via its Health/Ping method (or perform a trivial
request), and on any failure assign m.kupoClient = nil and return the error;
keep the existing flow of creating and caching a client when no cached client
exists. Ensure you reference m.kupoClient and getKupoClient when making the
change so future connection losses cause revalidation and recreation.

391-418: Redundant timeout: both http.Client.Timeout and request context impose the same deadline.

Line 392 sets a context timeout of kupoHealthTimeout and line 398 sets http.Client{Timeout: kupoHealthTimeout}. These are redundant — the shorter one wins, but since they're identical, one can be dropped for clarity. Typically the context-based approach is preferred as it's more composable.

Simplify by removing the client-level timeout
-	httpClient := &http.Client{Timeout: kupoHealthTimeout}
+	httpClient := &http.Client{}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@input/mempool/mempool.go` around lines 391 - 418, The health check sets a
deadline twice (context.WithTimeout using kupoHealthTimeout and
http.Client{Timeout: kupoHealthTimeout}); remove the redundant client-level
timeout and rely on the request context instead. Specifically, delete or change
the instantiation of httpClient that sets Timeout (the &http.Client{Timeout:
kupoHealthTimeout} creation used with httpClient.Do(req)) so the client does not
impose its own timeout (use the default client or a plain &http.Client{}),
keeping the context-based timeout via context.WithTimeout and preserving the
existing error handling around http.NewRequestWithContext, httpClient.Do, resp
handling, and assignment to m.kupoClient / return k.

436-446: String-based error detection is fragile but pragmatic.

Matching on "Invalid pattern!" and JSON unmarshal error text is brittle — upstream library changes could silently break this detection. Consider documenting this coupling (e.g., a comment noting the kugo library version this was tested against) so future maintainers know to re-verify after kugo upgrades.

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

In `@input/mempool/mempool.go` around lines 436 - 446, The string-based error
detection block that checks err.Error() for "Invalid pattern!" and the JSON
unmarshal text is fragile; update the code around m.kupoInvalidPatternLogged /
m.kupoDisabled and the logger.Debug call by adding a clear comment that
documents the exact kugo library version (and commit/tag) this behavior was
validated against, why we match these substrings, and a TODO to re-verify or
replace with a sentinel error when upgrading kugo; optionally extract the
literal substrings into named constants to make the coupling explicit for future
maintainers.

104-111: Dual configuration path — CLI option vs global config fallback.

The Kupo URL is sourced from two places: the mempool-specific WithKupoUrl option (via CLI/env) and the global config.GetConfig().KupoUrl. The chainsync plugin does the same (see input/chainsync/chainsync.go lines 137). While this provides convenience, it could surprise users if the global config value unexpectedly overrides an intentionally empty CLI option — the current code cannot distinguish "user didn't set it" from "user explicitly left it empty."

This is consistent with the existing chainsync approach, so probably fine for now, but worth noting.

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

In `@input/mempool/mempool.go` around lines 104 - 111, The code currently falls
back to config.GetConfig().KupoUrl whenever m.kupoUrl is empty, which makes it
impossible to tell if the user explicitly provided an empty value or never set
the option; change the behavior so fallback only happens when the user did NOT
set the mempool option. Add a presence flag (e.g., m.kupoUrlSet) that is set by
the WithKupoUrl option (or detect presence via the option builder), then update
the logic around m.kupoUrl to: if !m.kupoUrlSet { m.kupoUrl =
config.GetConfig().KupoUrl } and keep the current logging using m.kupoUrl;
mirror the same pattern used by the chainsync plugin (see
input/chainsync/chainsync.go) to avoid surprising overrides.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@input/mempool/mempool.go`:
- Around line 427-462: The resolveTransactionInputs code uses
context.Background() for each Kupo query which prevents shutdown from cancelling
in-flight requests; change it to derive per-request contexts from a cancellable
parent context stored on the mempool struct (e.g., m.ctx/m.cancel) that Stop()
cancels: add a context.Context and context.CancelFunc fields to the mempool,
initialize them when starting/polling, call m.cancel() in Stop(), and replace
context.WithTimeout(context.Background(), defaultKupoTimeout) with
context.WithTimeout(m.ctx, defaultKupoTimeout) so k.Matches(ctx, ...) is
cancelled on shutdown; keep existing timeout/error handling and behavior for
m.kupoDisabled and logging intact.
- Around line 373-376: getKupoClient currently returns a cached m.kupoClient
without re-validating connectivity; update getKupoClient to perform a
lightweight health/ping check on the cached client and if that check fails clear
m.kupoClient (set to nil) and return an error so a fresh client will be created
on the next call. Specifically, in getKupoClient check the existing m.kupoClient
via its Health/Ping method (or perform a trivial request), and on any failure
assign m.kupoClient = nil and return the error; keep the existing flow of
creating and caching a client when no cached client exists. Ensure you reference
m.kupoClient and getKupoClient when making the change so future connection
losses cause revalidation and recreation.
- Around line 391-418: The health check sets a deadline twice
(context.WithTimeout using kupoHealthTimeout and http.Client{Timeout:
kupoHealthTimeout}); remove the redundant client-level timeout and rely on the
request context instead. Specifically, delete or change the instantiation of
httpClient that sets Timeout (the &http.Client{Timeout: kupoHealthTimeout}
creation used with httpClient.Do(req)) so the client does not impose its own
timeout (use the default client or a plain &http.Client{}), keeping the
context-based timeout via context.WithTimeout and preserving the existing error
handling around http.NewRequestWithContext, httpClient.Do, resp handling, and
assignment to m.kupoClient / return k.
- Around line 436-446: The string-based error detection block that checks
err.Error() for "Invalid pattern!" and the JSON unmarshal text is fragile;
update the code around m.kupoInvalidPatternLogged / m.kupoDisabled and the
logger.Debug call by adding a clear comment that documents the exact kugo
library version (and commit/tag) this behavior was validated against, why we
match these substrings, and a TODO to re-verify or replace with a sentinel error
when upgrading kugo; optionally extract the literal substrings into named
constants to make the coupling explicit for future maintainers.
- Around line 104-111: The code currently falls back to
config.GetConfig().KupoUrl whenever m.kupoUrl is empty, which makes it
impossible to tell if the user explicitly provided an empty value or never set
the option; change the behavior so fallback only happens when the user did NOT
set the mempool option. Add a presence flag (e.g., m.kupoUrlSet) that is set by
the WithKupoUrl option (or detect presence via the option builder), then update
the logic around m.kupoUrl to: if !m.kupoUrlSet { m.kupoUrl =
config.GetConfig().KupoUrl } and keep the current logging using m.kupoUrl;
mirror the same pattern used by the chainsync plugin (see
input/chainsync/chainsync.go) to avoid surprising overrides.

In `@input/mempool/options.go`:
- Around line 79-84: Rename the exported option function WithKupoUrl to
WithKupoURL and update all call sites and plugin wiring that reference it
(including the CLI wiring in plugin.go that currently uses the `kupo-url` flag)
so they call WithKupoURL instead; leave the unexported field kupoUrl unchanged,
run go vet/gofmt to ensure exports are consistent, and update any tests or
imports that referenced WithKupoUrl to the new WithKupoURL identifier.

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.

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

373-419: No negative caching — failed health checks are retried on every resolveTransactionInputs call.

When Kupo is unreachable, getKupoClient is called for every new transaction in every poll cycle (every 5 s by default). Each call performs a full HTTP health check that will time out after 3 s, adding significant latency and generating repetitive warn logs.

Consider caching the failure with a short backoff (e.g., skip retries for 30–60 s after a health-check failure) so the plugin doesn't repeatedly block on a dead endpoint:

♻️ Sketch: add a cooldown after health-check failure
 type Mempool struct {
     // ...existing fields...
     kupoClient               *kugo.Client
     kupoDisabled             bool
     kupoInvalidPatternLogged bool
+    kupoLastHealthFail       time.Time
 }

 func (m *Mempool) getKupoClient() (*kugo.Client, error) {
     if m.kupoClient != nil {
         return m.kupoClient, nil
     }
+    // Back off for 60s after a failed health check
+    if !m.kupoLastHealthFail.IsZero() && time.Since(m.kupoLastHealthFail) < 60*time.Second {
+        return nil, errors.New("kupo health check cooling down")
+    }
     // ...existing URL validation and health check code...
     resp, err := httpClient.Do(req)
     if err != nil {
+        m.kupoLastHealthFail = time.Now()
         // ...existing error handling...
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@input/mempool/mempool.go` around lines 373 - 419, getKupoClient currently
retries a full 3s health check on every call when Kupo is down, causing repeated
latency and logs; add short negative-caching: introduce a small cooldown field
(e.g., m.kupoFailUntil time.Time) and check at the top of getKupoClient (if
time.Now().Before(m.kupoFailUntil) return nil, fmt.Errorf("kupo unavailable
until %v", m.kupoFailUntil)); on any health-check failure set m.kupoFailUntil =
time.Now().Add(30*time.Second) (or 60s) and return the error immediately;
protect reads/writes with the existing mutex (or add one, e.g., m.mu) around
accesses to m.kupoClient and m.kupoFailUntil to avoid races.

436-446: Fragile string-based error detection for disabling Kupo.

The "Invalid pattern!" and JSON unmarshal error substrings depend on implementation details of Kupo's HTTP API response format and Go's encoding/json. If Kupo changes its error response body, this detection silently breaks — instead of disabling gracefully, each input resolution would fail repeatedly and log warnings every poll cycle.

Consider at minimum adding a comment documenting why these specific strings are matched and which Kupo versions they apply to. The JSON unmarshal error ("cannot unmarshal object into Go value of type []kugo.Match") is stable (from Go's standard library), but the "Invalid pattern!" string is not. Ideally, kugo could expose a typed error (e.g., kugo.ErrInvalidPattern) upstream, but kugo v1.3.0 currently does not provide this.

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

In `@input/mempool/mempool.go` around lines 436 - 446, Replace the fragile
substring checks for disabling Kupo with a more robust approach: in the block
that currently inspects err.Error() (around m.kupoInvalidPatternLogged,
m.kupoDisabled, and logger.Debug), first try to detect a typed sentinel from the
kupo client (e.g., kugo.ErrInvalidPattern or any exported error on the kupo
package) and use that to disable resolution; if no typed error exists, fall back
to a documented regex match or HTTP-status-based check (and keep the existing
JSON unmarshal substring only as a secondary fallback), update the logger.Debug
call to include the full error, and add a clear comment referencing kupo v1.3.0
and why the specific strings/regex are used so future maintainers know the
rationale and can remove the fallback when kupo provides a typed error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@input/mempool/mempool.go`:
- Around line 373-419: getKupoClient currently retries a full 3s health check on
every call when Kupo is down, causing repeated latency and logs; add short
negative-caching: introduce a small cooldown field (e.g., m.kupoFailUntil
time.Time) and check at the top of getKupoClient (if
time.Now().Before(m.kupoFailUntil) return nil, fmt.Errorf("kupo unavailable
until %v", m.kupoFailUntil)); on any health-check failure set m.kupoFailUntil =
time.Now().Add(30*time.Second) (or 60s) and return the error immediately;
protect reads/writes with the existing mutex (or add one, e.g., m.mu) around
accesses to m.kupoClient and m.kupoFailUntil to avoid races.
- Around line 436-446: Replace the fragile substring checks for disabling Kupo
with a more robust approach: in the block that currently inspects err.Error()
(around m.kupoInvalidPatternLogged, m.kupoDisabled, and logger.Debug), first try
to detect a typed sentinel from the kupo client (e.g., kugo.ErrInvalidPattern or
any exported error on the kupo package) and use that to disable resolution; if
no typed error exists, fall back to a documented regex match or
HTTP-status-based check (and keep the existing JSON unmarshal substring only as
a secondary fallback), update the logger.Debug call to include the full error,
and add a clear comment referencing kupo v1.3.0 and why the specific
strings/regex are used so future maintainers know the rationale and can remove
the fallback when kupo provides a typed error.

@cryptodj413 cryptodj413 requested review from wolf31o2 and removed request for a team February 17, 2026 06:39
@cryptodj413 cryptodj413 merged commit 8391c4a into blinklabs-io:main Feb 20, 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.

Kupo support for resolving inputs in mempool input plugin

2 participants